From 41d97837ab1e5a35fdcfd7f6af9b5d56af62e92a Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Mon, 28 Jul 2025 22:05:19 +0300 Subject: [PATCH 001/371] xdiff: refactor xdl_hash_record() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inline the check for whitespace flags so that the compiler can hoist it out of the loop in xdl_prepare_ctx(). This improves the performance by 8%. $ hyperfine --warmup=1 -L rev HEAD,HEAD^ --setup='git checkout {rev} -- :/ && make git' ': {rev}; GIT_CONFIG_GLOBAL=/dev/null ./git log --oneline --shortstat v2.0.0..v2.5.0' Benchmark 1: : HEAD; GIT_CONFIG_GLOBAL=/dev/null ./git log --oneline --shortstat v2.0.0..v2.5.0 Time (mean ± σ): 1.670 s ± 0.044 s [User: 1.473 s, System: 0.196 s] Range (min … max): 1.619 s … 1.754 s 10 runs Benchmark 2: : HEAD^; GIT_CONFIG_GLOBAL=/dev/null ./git log --oneline --shortstat v2.0.0..v2.5.0 Time (mean ± σ): 1.801 s ± 0.021 s [User: 1.605 s, System: 0.192 s] Range (min … max): 1.766 s … 1.831 s 10 runs Summary ': HEAD^; GIT_CONFIG_GLOBAL=/dev/null ./git log --oneline --shortstat v2.0.0..v2.5.0' ran 1.08 ± 0.03 times faster than ': HEAD^^; GIT_CONFIG_GLOBAL=/dev/null ./git log --oneline --shortstat v2.0.0..v2.5.0' Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- xdiff/xutils.c | 7 ++----- xdiff/xutils.h | 10 +++++++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/xdiff/xutils.c b/xdiff/xutils.c index 444a108f87c..e070ed649ff 100644 --- a/xdiff/xutils.c +++ b/xdiff/xutils.c @@ -249,7 +249,7 @@ int xdl_recmatch(const char *l1, long s1, const char *l2, long s2, long flags) return 1; } -static unsigned long xdl_hash_record_with_whitespace(char const **data, +unsigned long xdl_hash_record_with_whitespace(char const **data, char const *top, long flags) { unsigned long ha = 5381; char const *ptr = *data; @@ -294,13 +294,10 @@ static unsigned long xdl_hash_record_with_whitespace(char const **data, return ha; } -unsigned long xdl_hash_record(char const **data, char const *top, long flags) { +unsigned long xdl_hash_record_verbatim(char const **data, char const *top) { unsigned long ha = 5381; char const *ptr = *data; - if (flags & XDF_WHITESPACE_FLAGS) - return xdl_hash_record_with_whitespace(data, top, flags); - for (; ptr < top && *ptr != '\n'; ptr++) { ha += (ha << 5); ha ^= (unsigned long) *ptr; diff --git a/xdiff/xutils.h b/xdiff/xutils.h index fd0bba94e8b..13f68310472 100644 --- a/xdiff/xutils.h +++ b/xdiff/xutils.h @@ -34,7 +34,15 @@ void *xdl_cha_alloc(chastore_t *cha); long xdl_guess_lines(mmfile_t *mf, long sample); int xdl_blankline(const char *line, long size, long flags); int xdl_recmatch(const char *l1, long s1, const char *l2, long s2, long flags); -unsigned long xdl_hash_record(char const **data, char const *top, long flags); +unsigned long xdl_hash_record_verbatim(char const **data, char const *top); +unsigned long xdl_hash_record_with_whitespace(char const **data, char const *top, long flags); +static inline unsigned long xdl_hash_record(char const **data, char const *top, long flags) +{ + if (flags & XDF_WHITESPACE_FLAGS) + return xdl_hash_record_with_whitespace(data, top, flags); + else + return xdl_hash_record_verbatim(data, top); +} unsigned int xdl_hashbits(unsigned int size); int xdl_num_out(char *out, long val); int xdl_emit_hunk_hdr(long s1, long c1, long s2, long c2, -- GitLab From 4f9c8d896397a1748132060d3465e8573c861633 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 1 Aug 2025 15:04:17 -0700 Subject: [PATCH 002/371] string-list: report programming error with BUG Passing a string list that has .strdup_strings bit unset to string_list_split(), or one that has .strdup_strings bit set to string_list_split_in_place(), is a programmer error. Do not use die() to abort the execution. Use BUG() instead. As a developer-facing message, the message string itself should be a lot more concise, but let's keep the original one for now. Signed-off-by: Junio C Hamano --- string-list.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/string-list.c b/string-list.c index 53faaa84207..0cb920e9b0d 100644 --- a/string-list.c +++ b/string-list.c @@ -283,7 +283,7 @@ int string_list_split(struct string_list *list, const char *string, const char *p = string, *end; if (!list->strdup_strings) - die("internal error in string_list_split(): " + BUG("internal error in string_list_split(): " "list->strdup_strings must be set"); for (;;) { count++; @@ -309,7 +309,7 @@ int string_list_split_in_place(struct string_list *list, char *string, char *p = string, *end; if (list->strdup_strings) - die("internal error in string_list_split_in_place(): " + BUG("internal error in string_list_split_in_place(): " "list->strdup_strings must not be set"); for (;;) { count++; -- GitLab From 9f6dfe43c8a55b833ae16486bcafe29b543461f9 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 1 Aug 2025 15:04:18 -0700 Subject: [PATCH 003/371] string-list: align string_list_split() with its _in_place() counterpart The string_list_split_in_place() function was updated by 52acddf3 (string-list: multi-delimiter `string_list_split_in_place()`, 2023-04-24) to take more than one delimiter characters, hoping that we can later use it to replace our uses of strtok(). We however did not make a matching change to the string_list_split() function, which is very similar. Before giving both functions more features in future commits, allow string_list_split() to also take more than one delimiter characters to make them closer to each other. Signed-off-by: Junio C Hamano --- builtin/blame.c | 2 +- builtin/merge.c | 2 +- builtin/var.c | 2 +- connect.c | 2 +- diff.c | 2 +- fetch-pack.c | 2 +- notes.c | 2 +- parse-options.c | 2 +- pathspec.c | 2 +- protocol.c | 2 +- ref-filter.c | 4 ++-- setup.c | 3 ++- string-list.c | 4 ++-- string-list.h | 16 ++++++++-------- t/helper/test-path-utils.c | 3 ++- t/helper/test-ref-store.c | 2 +- t/unit-tests/u-string-list.c | 16 ++++++++-------- transport.c | 2 +- upload-pack.c | 2 +- 19 files changed, 37 insertions(+), 35 deletions(-) diff --git a/builtin/blame.c b/builtin/blame.c index 91586e6852b..70a64604018 100644 --- a/builtin/blame.c +++ b/builtin/blame.c @@ -420,7 +420,7 @@ static void parse_color_fields(const char *s) colorfield_nr = 0; /* Ideally this would be stripped and split at the same time? */ - string_list_split(&l, s, ',', -1); + string_list_split(&l, s, ",", -1); ALLOC_GROW(colorfield, colorfield_nr + 1, colorfield_alloc); for_each_string_list_item(item, &l) { diff --git a/builtin/merge.c b/builtin/merge.c index 18b22c0a26d..893f8950bfc 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -875,7 +875,7 @@ static void add_strategies(const char *string, unsigned attr) if (string) { struct string_list list = STRING_LIST_INIT_DUP; struct string_list_item *item; - string_list_split(&list, string, ' ', -1); + string_list_split(&list, string, " ", -1); for_each_string_list_item(item, &list) append_strategy(get_strategy(item->string)); string_list_clear(&list, 0); diff --git a/builtin/var.c b/builtin/var.c index ada642a9fe5..4ae7af0eff9 100644 --- a/builtin/var.c +++ b/builtin/var.c @@ -181,7 +181,7 @@ static void list_vars(void) if (ptr->multivalued && *val) { struct string_list list = STRING_LIST_INIT_DUP; - string_list_split(&list, val, '\n', -1); + string_list_split(&list, val, "\n", -1); for (size_t i = 0; i < list.nr; i++) printf("%s=%s\n", ptr->name, list.items[i].string); string_list_clear(&list, 0); diff --git a/connect.c b/connect.c index e77287f426c..867b12bde5a 100644 --- a/connect.c +++ b/connect.c @@ -407,7 +407,7 @@ static int process_ref_v2(struct packet_reader *reader, struct ref ***list, * name. Subsequent fields (symref-target and peeled) are optional and * don't have a particular order. */ - if (string_list_split(&line_sections, line, ' ', -1) < 2) { + if (string_list_split(&line_sections, line, " ", -1) < 2) { ret = 0; goto out; } diff --git a/diff.c b/diff.c index dca87e164fb..a81949a4220 100644 --- a/diff.c +++ b/diff.c @@ -327,7 +327,7 @@ static unsigned parse_color_moved_ws(const char *arg) struct string_list l = STRING_LIST_INIT_DUP; struct string_list_item *i; - string_list_split(&l, arg, ',', -1); + string_list_split(&l, arg, ",", -1); for_each_string_list_item(i, &l) { struct strbuf sb = STRBUF_INIT; diff --git a/fetch-pack.c b/fetch-pack.c index c1be9b76eb6..98662706968 100644 --- a/fetch-pack.c +++ b/fetch-pack.c @@ -1914,7 +1914,7 @@ static void fetch_pack_config(void) char *str; if (!git_config_get_string("fetch.uriprotocols", &str) && str) { - string_list_split(&uri_protocols, str, ',', -1); + string_list_split(&uri_protocols, str, ",", -1); free(str); } } diff --git a/notes.c b/notes.c index 97b995f3f2d..6afcf088b97 100644 --- a/notes.c +++ b/notes.c @@ -892,7 +892,7 @@ static int string_list_add_note_lines(struct string_list *list, * later, along with any empty strings that came from empty * lines within the file. */ - string_list_split(list, data, '\n', -1); + string_list_split(list, data, "\n", -1); free(data); return 0; } diff --git a/parse-options.c b/parse-options.c index 5224203ffe7..9e7cb751927 100644 --- a/parse-options.c +++ b/parse-options.c @@ -1338,7 +1338,7 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t if (!saw_empty_line && !*str) saw_empty_line = 1; - string_list_split(&list, str, '\n', -1); + string_list_split(&list, str, "\n", -1); for (j = 0; j < list.nr; j++) { const char *line = list.items[j].string; diff --git a/pathspec.c b/pathspec.c index a3ddd701c74..de325f7ef99 100644 --- a/pathspec.c +++ b/pathspec.c @@ -201,7 +201,7 @@ static void parse_pathspec_attr_match(struct pathspec_item *item, const char *va if (!value || !*value) die(_("attr spec must not be empty")); - string_list_split(&list, value, ' ', -1); + string_list_split(&list, value, " ", -1); string_list_remove_empty_items(&list, 0); item->attr_check = attr_check_alloc(); diff --git a/protocol.c b/protocol.c index bae7226ff40..54b9f49c01b 100644 --- a/protocol.c +++ b/protocol.c @@ -61,7 +61,7 @@ enum protocol_version determine_protocol_version_server(void) if (git_protocol) { struct string_list list = STRING_LIST_INIT_DUP; const struct string_list_item *item; - string_list_split(&list, git_protocol, ':', -1); + string_list_split(&list, git_protocol, ":", -1); for_each_string_list_item(item, &list) { const char *value; diff --git a/ref-filter.c b/ref-filter.c index f9f2c512a8c..4edfb9c83b2 100644 --- a/ref-filter.c +++ b/ref-filter.c @@ -435,7 +435,7 @@ static int remote_ref_atom_parser(struct ref_format *format UNUSED, } atom->u.remote_ref.nobracket = 0; - string_list_split(¶ms, arg, ',', -1); + string_list_split(¶ms, arg, ",", -1); for (i = 0; i < params.nr; i++) { const char *s = params.items[i].string; @@ -831,7 +831,7 @@ static int align_atom_parser(struct ref_format *format UNUSED, align->position = ALIGN_LEFT; - string_list_split(¶ms, arg, ',', -1); + string_list_split(¶ms, arg, ",", -1); for (i = 0; i < params.nr; i++) { const char *s = params.items[i].string; int position; diff --git a/setup.c b/setup.c index 6f52dab64ca..b9f5eb8b51e 100644 --- a/setup.c +++ b/setup.c @@ -1460,8 +1460,9 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir, if (env_ceiling_dirs) { int empty_entry_found = 0; + static const char path_sep[] = { PATH_SEP, '\0' }; - string_list_split(&ceiling_dirs, env_ceiling_dirs, PATH_SEP, -1); + string_list_split(&ceiling_dirs, env_ceiling_dirs, path_sep, -1); filter_string_list(&ceiling_dirs, 0, canonicalize_ceiling_entry, &empty_entry_found); ceil_offset = longest_ancestor_length(dir->buf, &ceiling_dirs); diff --git a/string-list.c b/string-list.c index 0cb920e9b0d..2284a009cba 100644 --- a/string-list.c +++ b/string-list.c @@ -277,7 +277,7 @@ void unsorted_string_list_delete_item(struct string_list *list, int i, int free_ } int string_list_split(struct string_list *list, const char *string, - int delim, int maxsplit) + const char *delim, int maxsplit) { int count = 0; const char *p = string, *end; @@ -291,7 +291,7 @@ int string_list_split(struct string_list *list, const char *string, string_list_append(list, p); return count; } - end = strchr(p, delim); + end = strpbrk(p, delim); if (end) { string_list_append_nodup(list, xmemdupz(p, end - p)); p = end + 1; diff --git a/string-list.h b/string-list.h index 122b3186419..6c8650efde0 100644 --- a/string-list.h +++ b/string-list.h @@ -254,7 +254,7 @@ struct string_list_item *unsorted_string_list_lookup(struct string_list *list, void unsorted_string_list_delete_item(struct string_list *list, int i, int free_util); /** - * Split string into substrings on character `delim` and append the + * Split string into substrings on characters in `delim` and append the * substrings to `list`. The input string is not modified. * list->strdup_strings must be set, as new memory needs to be * allocated to hold the substrings. If maxsplit is non-negative, @@ -262,15 +262,15 @@ void unsorted_string_list_delete_item(struct string_list *list, int i, int free_ * appended to list. * * Examples: - * string_list_split(l, "foo:bar:baz", ':', -1) -> ["foo", "bar", "baz"] - * string_list_split(l, "foo:bar:baz", ':', 0) -> ["foo:bar:baz"] - * string_list_split(l, "foo:bar:baz", ':', 1) -> ["foo", "bar:baz"] - * string_list_split(l, "foo:bar:", ':', -1) -> ["foo", "bar", ""] - * string_list_split(l, "", ':', -1) -> [""] - * string_list_split(l, ":", ':', -1) -> ["", ""] + * string_list_split(l, "foo:bar:baz", ":", -1) -> ["foo", "bar", "baz"] + * string_list_split(l, "foo:bar:baz", ":", 0) -> ["foo:bar:baz"] + * string_list_split(l, "foo:bar:baz", ":", 1) -> ["foo", "bar:baz"] + * string_list_split(l, "foo:bar:", ":", -1) -> ["foo", "bar", ""] + * string_list_split(l, "", ":", -1) -> [""] + * string_list_split(l, ":", ":", -1) -> ["", ""] */ int string_list_split(struct string_list *list, const char *string, - int delim, int maxsplit); + const char *delim, int maxsplit); /* * Like string_list_split(), except that string is split in-place: the diff --git a/t/helper/test-path-utils.c b/t/helper/test-path-utils.c index 086238c826a..f5f33751da6 100644 --- a/t/helper/test-path-utils.c +++ b/t/helper/test-path-utils.c @@ -348,6 +348,7 @@ int cmd__path_utils(int argc, const char **argv) if (argc == 4 && !strcmp(argv[1], "longest_ancestor_length")) { int len; struct string_list ceiling_dirs = STRING_LIST_INIT_DUP; + const char path_sep[] = { PATH_SEP, '\0' }; char *path = xstrdup(argv[2]); /* @@ -362,7 +363,7 @@ int cmd__path_utils(int argc, const char **argv) */ if (normalize_path_copy(path, path)) die("Path \"%s\" could not be normalized", argv[2]); - string_list_split(&ceiling_dirs, argv[3], PATH_SEP, -1); + string_list_split(&ceiling_dirs, argv[3], path_sep, -1); filter_string_list(&ceiling_dirs, 0, normalize_ceiling_entry, NULL); len = longest_ancestor_length(path, &ceiling_dirs); diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c index 8d9a271845c..aa1cb9b4acf 100644 --- a/t/helper/test-ref-store.c +++ b/t/helper/test-ref-store.c @@ -29,7 +29,7 @@ static unsigned int parse_flags(const char *str, struct flag_definition *defs) if (!strcmp(str, "0")) return 0; - string_list_split(&masks, str, ',', 64); + string_list_split(&masks, str, ",", 64); for (size_t i = 0; i < masks.nr; i++) { const char *name = masks.items[i].string; struct flag_definition *def = defs; diff --git a/t/unit-tests/u-string-list.c b/t/unit-tests/u-string-list.c index d4ba5f9fa52..150a5f505f5 100644 --- a/t/unit-tests/u-string-list.c +++ b/t/unit-tests/u-string-list.c @@ -43,7 +43,7 @@ static void t_string_list_equal(struct string_list *list, expected_strings->items[i].string); } -static void t_string_list_split(const char *data, int delim, int maxsplit, ...) +static void t_string_list_split(const char *data, const char *delim, int maxsplit, ...) { struct string_list expected_strings = STRING_LIST_INIT_DUP; struct string_list list = STRING_LIST_INIT_DUP; @@ -65,13 +65,13 @@ static void t_string_list_split(const char *data, int delim, int maxsplit, ...) void test_string_list__split(void) { - t_string_list_split("foo:bar:baz", ':', -1, "foo", "bar", "baz", NULL); - t_string_list_split("foo:bar:baz", ':', 0, "foo:bar:baz", NULL); - t_string_list_split("foo:bar:baz", ':', 1, "foo", "bar:baz", NULL); - t_string_list_split("foo:bar:baz", ':', 2, "foo", "bar", "baz", NULL); - t_string_list_split("foo:bar:", ':', -1, "foo", "bar", "", NULL); - t_string_list_split("", ':', -1, "", NULL); - t_string_list_split(":", ':', -1, "", "", NULL); + t_string_list_split("foo:bar:baz", ":", -1, "foo", "bar", "baz", NULL); + t_string_list_split("foo:bar:baz", ":", 0, "foo:bar:baz", NULL); + t_string_list_split("foo:bar:baz", ":", 1, "foo", "bar:baz", NULL); + t_string_list_split("foo:bar:baz", ":", 2, "foo", "bar", "baz", NULL); + t_string_list_split("foo:bar:", ":", -1, "foo", "bar", "", NULL); + t_string_list_split("", ":", -1, "", NULL); + t_string_list_split(":", ":", -1, "", "", NULL); } static void t_string_list_split_in_place(const char *data, const char *delim, diff --git a/transport.c b/transport.c index c123ac1e38b..76487b54530 100644 --- a/transport.c +++ b/transport.c @@ -1042,7 +1042,7 @@ static const struct string_list *protocol_allow_list(void) if (enabled < 0) { const char *v = getenv("GIT_ALLOW_PROTOCOL"); if (v) { - string_list_split(&allowed, v, ':', -1); + string_list_split(&allowed, v, ":", -1); string_list_sort(&allowed); enabled = 1; } else { diff --git a/upload-pack.c b/upload-pack.c index 4f26f6afc77..91fcdcad9b5 100644 --- a/upload-pack.c +++ b/upload-pack.c @@ -1685,7 +1685,7 @@ static void process_args(struct packet_reader *request, if (data->uri_protocols.nr) send_err_and_die(data, "multiple packfile-uris lines forbidden"); - string_list_split(&data->uri_protocols, p, ',', -1); + string_list_split(&data->uri_protocols, p, ",", -1); continue; } -- GitLab From 527535fcdd2d9dec56877435f609852d0f2bf163 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 1 Aug 2025 15:04:19 -0700 Subject: [PATCH 004/371] string-list: unify string_list_split* functions Thanks to the previous step, the only difference between these two related functions is that string_list_split() works on a string without modifying its contents (i.e. taking "const char *") and the resulting pieces of strings are their own copies in a string list, while string_list_split_in_place() works on a mutable string and the resulting pieces of strings come from the original string. Consolidate their implementations into a single helper function, and make them a thin wrapper around it. We can later add an extra flags parameter to extend both of these functions by updating only the internal helper function. Signed-off-by: Junio C Hamano --- string-list.c | 96 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/string-list.c b/string-list.c index 2284a009cba..65b6ceb2591 100644 --- a/string-list.c +++ b/string-list.c @@ -276,55 +276,71 @@ void unsorted_string_list_delete_item(struct string_list *list, int i, int free_ list->nr--; } -int string_list_split(struct string_list *list, const char *string, - const char *delim, int maxsplit) +/* + * append a substring [p..end] to list; return number of things it + * appended to the list. + */ +static int append_one(struct string_list *list, + const char *p, const char *end, + int in_place) +{ + if (!end) + end = p + strlen(p); + + if (in_place) { + *((char *)end) = '\0'; + string_list_append(list, p); + } else { + string_list_append_nodup(list, xmemdupz(p, end - p)); + } + return 1; +} + +/* + * Unfortunately this cannot become a public interface, as _in_place() + * wants to have "const char *string" while the other variant wants to + * have "char *string" for type safety. + * + * This accepts "const char *string" to allow both wrappers to use it; + * it internally casts away the constness when in_place is true by + * taking advantage of strpbrk() that takes a "const char *" arg and + * returns "char *" pointer into that const string. Yucky but works ;-). + */ +static int split_string(struct string_list *list, const char *string, const char *delim, + int maxsplit, int in_place) { int count = 0; - const char *p = string, *end; + const char *p = string; + + if (in_place && list->strdup_strings) + BUG("string_list_split_in_place() called with strdup_strings"); + else if (!in_place && !list->strdup_strings) + BUG("string_list_split() called without strdup_strings"); - if (!list->strdup_strings) - BUG("internal error in string_list_split(): " - "list->strdup_strings must be set"); for (;;) { - count++; - if (maxsplit >= 0 && count > maxsplit) { - string_list_append(list, p); - return count; - } - end = strpbrk(p, delim); - if (end) { - string_list_append_nodup(list, xmemdupz(p, end - p)); - p = end + 1; - } else { - string_list_append(list, p); + char *end; + + if (0 <= maxsplit && maxsplit <= count) + end = NULL; + else + end = strpbrk(p, delim); + + count += append_one(list, p, end, in_place); + + if (!end) return count; - } + p = end + 1; } } +int string_list_split(struct string_list *list, const char *string, + const char *delim, int maxsplit) +{ + return split_string(list, string, delim, maxsplit, 0); +} + int string_list_split_in_place(struct string_list *list, char *string, const char *delim, int maxsplit) { - int count = 0; - char *p = string, *end; - - if (list->strdup_strings) - BUG("internal error in string_list_split_in_place(): " - "list->strdup_strings must not be set"); - for (;;) { - count++; - if (maxsplit >= 0 && count > maxsplit) { - string_list_append(list, p); - return count; - } - end = strpbrk(p, delim); - if (end) { - *end = '\0'; - string_list_append(list, p); - p = end + 1; - } else { - string_list_append(list, p); - return count; - } - } + return split_string(list, string, delim, maxsplit, 1); } -- GitLab From 576454974165d51b7e39c0608cde1c84978f1a8a Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 1 Aug 2025 15:04:20 -0700 Subject: [PATCH 005/371] string-list: optionally trim string pieces split by string_list_split*() Teach the unified split_string() to take an optional "flags" word, and define the first flag STRING_LIST_SPLIT_TRIM to cause the split pieces to be trimmed before they are placed in the string list. Signed-off-by: Junio C Hamano --- string-list.c | 35 +++++++++++++++++--- string-list.h | 15 +++++++++ t/unit-tests/u-string-list.c | 64 ++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 5 deletions(-) diff --git a/string-list.c b/string-list.c index 65b6ceb2591..86a309f8fbd 100644 --- a/string-list.c +++ b/string-list.c @@ -282,11 +282,18 @@ void unsorted_string_list_delete_item(struct string_list *list, int i, int free_ */ static int append_one(struct string_list *list, const char *p, const char *end, - int in_place) + int in_place, unsigned flags) { if (!end) end = p + strlen(p); + if ((flags & STRING_LIST_SPLIT_TRIM)) { + /* rtrim */ + for (; p < end; end--) + if (!isspace(end[-1])) + break; + } + if (in_place) { *((char *)end) = '\0'; string_list_append(list, p); @@ -307,7 +314,7 @@ static int append_one(struct string_list *list, * returns "char *" pointer into that const string. Yucky but works ;-). */ static int split_string(struct string_list *list, const char *string, const char *delim, - int maxsplit, int in_place) + int maxsplit, int in_place, unsigned flags) { int count = 0; const char *p = string; @@ -320,12 +327,18 @@ static int split_string(struct string_list *list, const char *string, const char for (;;) { char *end; + if (flags & STRING_LIST_SPLIT_TRIM) { + /* ltrim */ + while (*p && isspace(*p)) + p++; + } + if (0 <= maxsplit && maxsplit <= count) end = NULL; else end = strpbrk(p, delim); - count += append_one(list, p, end, in_place); + count += append_one(list, p, end, in_place, flags); if (!end) return count; @@ -336,11 +349,23 @@ static int split_string(struct string_list *list, const char *string, const char int string_list_split(struct string_list *list, const char *string, const char *delim, int maxsplit) { - return split_string(list, string, delim, maxsplit, 0); + return split_string(list, string, delim, maxsplit, 0, 0); } int string_list_split_in_place(struct string_list *list, char *string, const char *delim, int maxsplit) { - return split_string(list, string, delim, maxsplit, 1); + return split_string(list, string, delim, maxsplit, 1, 0); +} + +int string_list_split_f(struct string_list *list, const char *string, + const char *delim, int maxsplit, unsigned flags) +{ + return split_string(list, string, delim, maxsplit, 0, flags); +} + +int string_list_split_in_place_f(struct string_list *list, char *string, + const char *delim, int maxsplit, unsigned flags) +{ + return split_string(list, string, delim, maxsplit, 1, flags); } diff --git a/string-list.h b/string-list.h index 6c8650efde0..40e148712da 100644 --- a/string-list.h +++ b/string-list.h @@ -281,4 +281,19 @@ int string_list_split(struct string_list *list, const char *string, */ int string_list_split_in_place(struct string_list *list, char *string, const char *delim, int maxsplit); + +/* Flag bits for split_f and split_in_place_f functions */ +enum { + /* + * trim whitespaces around resulting string piece before adding + * it to the list + */ + STRING_LIST_SPLIT_TRIM = (1 << 0), +}; + +int string_list_split_f(struct string_list *, const char *string, + const char *delim, int maxsplit, unsigned flags); + +int string_list_split_in_place_f(struct string_list *, char *string, + const char *delim, int maxsplit, unsigned flags); #endif /* STRING_LIST_H */ diff --git a/t/unit-tests/u-string-list.c b/t/unit-tests/u-string-list.c index 150a5f505f5..daa9307e45e 100644 --- a/t/unit-tests/u-string-list.c +++ b/t/unit-tests/u-string-list.c @@ -63,6 +63,70 @@ static void t_string_list_split(const char *data, const char *delim, int maxspli string_list_clear(&list, 0); } +static void t_string_list_split_f(const char *data, const char *delim, + int maxsplit, unsigned flags, ...) +{ + struct string_list expected_strings = STRING_LIST_INIT_DUP; + struct string_list list = STRING_LIST_INIT_DUP; + va_list ap; + int len; + + va_start(ap, flags); + t_vcreate_string_list_dup(&expected_strings, 0, ap); + va_end(ap); + + string_list_clear(&list, 0); + len = string_list_split_f(&list, data, delim, maxsplit, flags); + cl_assert_equal_i(len, expected_strings.nr); + t_string_list_equal(&list, &expected_strings); + + string_list_clear(&expected_strings, 0); + string_list_clear(&list, 0); +} + +void test_string_list__split_f(void) +{ + t_string_list_split_f("::foo:bar:baz:", ":", -1, 0, + "", "", "foo", "bar", "baz", "", NULL); + t_string_list_split_f(" foo:bar : baz", ":", -1, STRING_LIST_SPLIT_TRIM, + "foo", "bar", "baz", NULL); + t_string_list_split_f(" a b c ", " ", 1, STRING_LIST_SPLIT_TRIM, + "a", "b c", NULL); +} + +static void t_string_list_split_in_place_f(const char *data_, const char *delim, + int maxsplit, unsigned flags, ...) +{ + struct string_list expected_strings = STRING_LIST_INIT_DUP; + struct string_list list = STRING_LIST_INIT_NODUP; + char *data = xstrdup(data_); + va_list ap; + int len; + + va_start(ap, flags); + t_vcreate_string_list_dup(&expected_strings, 0, ap); + va_end(ap); + + string_list_clear(&list, 0); + len = string_list_split_in_place_f(&list, data, delim, maxsplit, flags); + cl_assert_equal_i(len, expected_strings.nr); + t_string_list_equal(&list, &expected_strings); + + free(data); + string_list_clear(&expected_strings, 0); + string_list_clear(&list, 0); +} + +void test_string_list__split_in_place_f(void) +{ + t_string_list_split_in_place_f("::foo:bar:baz:", ":", -1, 0, + "", "", "foo", "bar", "baz", "", NULL); + t_string_list_split_in_place_f(" foo:bar : baz", ":", -1, STRING_LIST_SPLIT_TRIM, + "foo", "bar", "baz", NULL); + t_string_list_split_in_place_f(" a b c ", " ", 1, STRING_LIST_SPLIT_TRIM, + "a", "b c", NULL); +} + void test_string_list__split(void) { t_string_list_split("foo:bar:baz", ":", -1, "foo", "bar", "baz", NULL); -- GitLab From f3a303aef017ad6e53fa44643d832a1fa0de0d91 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 1 Aug 2025 15:04:21 -0700 Subject: [PATCH 006/371] diff: simplify parsing of diff.colormovedws The code to parse this configuration variable, whose value is a comma-separated list of known tokens like "ignore-space-change" and "ignore-all-space", uses string_list_split() to split the value into pieces, and then places each piece of string in a strbuf to trim, before comparing the result with the list of known tokens. Thanks to the previous steps, now string_list_split() can trim the resulting pieces before it places them in the string list. Use it to simplify the code. Signed-off-by: Junio C Hamano --- diff.c | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/diff.c b/diff.c index a81949a4220..70666ad2cd1 100644 --- a/diff.c +++ b/diff.c @@ -327,29 +327,23 @@ static unsigned parse_color_moved_ws(const char *arg) struct string_list l = STRING_LIST_INIT_DUP; struct string_list_item *i; - string_list_split(&l, arg, ",", -1); + string_list_split_f(&l, arg, ",", -1, STRING_LIST_SPLIT_TRIM); for_each_string_list_item(i, &l) { - struct strbuf sb = STRBUF_INIT; - strbuf_addstr(&sb, i->string); - strbuf_trim(&sb); - - if (!strcmp(sb.buf, "no")) + if (!strcmp(i->string, "no")) ret = 0; - else if (!strcmp(sb.buf, "ignore-space-change")) + else if (!strcmp(i->string, "ignore-space-change")) ret |= XDF_IGNORE_WHITESPACE_CHANGE; - else if (!strcmp(sb.buf, "ignore-space-at-eol")) + else if (!strcmp(i->string, "ignore-space-at-eol")) ret |= XDF_IGNORE_WHITESPACE_AT_EOL; - else if (!strcmp(sb.buf, "ignore-all-space")) + else if (!strcmp(i->string, "ignore-all-space")) ret |= XDF_IGNORE_WHITESPACE; - else if (!strcmp(sb.buf, "allow-indentation-change")) + else if (!strcmp(i->string, "allow-indentation-change")) ret |= COLOR_MOVED_WS_ALLOW_INDENTATION_CHANGE; else { ret |= COLOR_MOVED_WS_ERROR; - error(_("unknown color-moved-ws mode '%s', possible values are 'ignore-space-change', 'ignore-space-at-eol', 'ignore-all-space', 'allow-indentation-change'"), sb.buf); + error(_("unknown color-moved-ws mode '%s', possible values are 'ignore-space-change', 'ignore-space-at-eol', 'ignore-all-space', 'allow-indentation-change'"), i->string); } - - strbuf_release(&sb); } if ((ret & COLOR_MOVED_WS_ALLOW_INDENTATION_CHANGE) && -- GitLab From 27531efa41cfa882473513dd93e696a16f6eb87b Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 1 Aug 2025 15:04:22 -0700 Subject: [PATCH 007/371] string-list: optionally omit empty string pieces in string_list_split*() Teach the unified split_string() machinery a new flag bit, STRING_LIST_SPLIT_NONEMPTY, to cause empty split pieces to be omitted from the resulting string list. Signed-off-by: Junio C Hamano --- string-list.c | 3 +++ string-list.h | 2 ++ t/unit-tests/u-string-list.c | 15 +++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/string-list.c b/string-list.c index 86a309f8fbd..343cf1ca90d 100644 --- a/string-list.c +++ b/string-list.c @@ -294,6 +294,9 @@ static int append_one(struct string_list *list, break; } + if ((flags & STRING_LIST_SPLIT_NONEMPTY) && (end <= p)) + return 0; + if (in_place) { *((char *)end) = '\0'; string_list_append(list, p); diff --git a/string-list.h b/string-list.h index 40e148712da..2b438c7733d 100644 --- a/string-list.h +++ b/string-list.h @@ -289,6 +289,8 @@ enum { * it to the list */ STRING_LIST_SPLIT_TRIM = (1 << 0), + /* omit adding empty string piece to the resulting list */ + STRING_LIST_SPLIT_NONEMPTY = (1 << 1), }; int string_list_split_f(struct string_list *, const char *string, diff --git a/t/unit-tests/u-string-list.c b/t/unit-tests/u-string-list.c index daa9307e45e..a2457d7b1ec 100644 --- a/t/unit-tests/u-string-list.c +++ b/t/unit-tests/u-string-list.c @@ -92,6 +92,13 @@ void test_string_list__split_f(void) "foo", "bar", "baz", NULL); t_string_list_split_f(" a b c ", " ", 1, STRING_LIST_SPLIT_TRIM, "a", "b c", NULL); + t_string_list_split_f("::foo::bar:baz:", ":", -1, STRING_LIST_SPLIT_NONEMPTY, + "foo", "bar", "baz", NULL); + t_string_list_split_f("foo:baz", ":", -1, STRING_LIST_SPLIT_NONEMPTY, + "foo", "baz", NULL); + t_string_list_split_f("foo :: : baz", ":", -1, + STRING_LIST_SPLIT_NONEMPTY | STRING_LIST_SPLIT_TRIM, + "foo", "baz", NULL); } static void t_string_list_split_in_place_f(const char *data_, const char *delim, @@ -125,6 +132,14 @@ void test_string_list__split_in_place_f(void) "foo", "bar", "baz", NULL); t_string_list_split_in_place_f(" a b c ", " ", 1, STRING_LIST_SPLIT_TRIM, "a", "b c", NULL); + t_string_list_split_in_place_f("::foo::bar:baz:", ":", -1, + STRING_LIST_SPLIT_NONEMPTY, + "foo", "bar", "baz", NULL); + t_string_list_split_in_place_f("foo:baz", ":", -1, STRING_LIST_SPLIT_NONEMPTY, + "foo", "baz", NULL); + t_string_list_split_in_place_f("foo :: : baz", ":", -1, + STRING_LIST_SPLIT_NONEMPTY | STRING_LIST_SPLIT_TRIM, + "foo", "baz", NULL); } void test_string_list__split(void) -- GitLab From 2ab2aac73d234ae75096e2186b07cc14c57d2586 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 1 Aug 2025 15:04:23 -0700 Subject: [PATCH 008/371] string-list: split-then-remove-empty can be done while splitting Thanks to the new STRING_LIST_SPLIT_NONEMPTY flag, a common pattern to split a string into a string list and then remove empty items in the resulting list is no longer needed. Instead, just tell the string_list_split*() to omit empty ones while splitting. Signed-off-by: Junio C Hamano --- notes.c | 4 ++-- pathspec.c | 3 +-- t/helper/test-hashmap.c | 4 ++-- t/helper/test-json-writer.c | 4 ++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/notes.c b/notes.c index 6afcf088b97..3603c4a42bd 100644 --- a/notes.c +++ b/notes.c @@ -970,8 +970,8 @@ void string_list_add_refs_from_colon_sep(struct string_list *list, char *globs_copy = xstrdup(globs); int i; - string_list_split_in_place(&split, globs_copy, ":", -1); - string_list_remove_empty_items(&split, 0); + string_list_split_in_place_f(&split, globs_copy, ":", -1, + STRING_LIST_SPLIT_NONEMPTY); for (i = 0; i < split.nr; i++) string_list_add_refs_by_glob(list, split.items[i].string); diff --git a/pathspec.c b/pathspec.c index de325f7ef99..5993c4afa0e 100644 --- a/pathspec.c +++ b/pathspec.c @@ -201,8 +201,7 @@ static void parse_pathspec_attr_match(struct pathspec_item *item, const char *va if (!value || !*value) die(_("attr spec must not be empty")); - string_list_split(&list, value, " ", -1); - string_list_remove_empty_items(&list, 0); + string_list_split_f(&list, value, " ", -1, STRING_LIST_SPLIT_NONEMPTY); item->attr_check = attr_check_alloc(); CALLOC_ARRAY(item->attr_match, list.nr); diff --git a/t/helper/test-hashmap.c b/t/helper/test-hashmap.c index 7782ae585e6..e4dc02bd7a0 100644 --- a/t/helper/test-hashmap.c +++ b/t/helper/test-hashmap.c @@ -149,8 +149,8 @@ int cmd__hashmap(int argc UNUSED, const char **argv UNUSED) /* break line into command and up to two parameters */ string_list_setlen(&parts, 0); - string_list_split_in_place(&parts, line.buf, DELIM, 2); - string_list_remove_empty_items(&parts, 0); + string_list_split_in_place_f(&parts, line.buf, DELIM, 2, + STRING_LIST_SPLIT_NONEMPTY); /* ignore empty lines */ if (!parts.nr) diff --git a/t/helper/test-json-writer.c b/t/helper/test-json-writer.c index a288069b04c..f8316a7d29c 100644 --- a/t/helper/test-json-writer.c +++ b/t/helper/test-json-writer.c @@ -492,8 +492,8 @@ static int scripted(void) /* break line into command and zero or more tokens */ string_list_setlen(&parts, 0); - string_list_split_in_place(&parts, line, " ", -1); - string_list_remove_empty_items(&parts, 0); + string_list_split_in_place_f(&parts, line, " ", -1, + STRING_LIST_SPLIT_NONEMPTY); /* ignore empty lines */ if (!parts.nr || !*parts.items[0].string) -- GitLab From 2efe707054d184565f081f9d882940381b2645ca Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 31 Jul 2025 15:54:23 -0700 Subject: [PATCH 009/371] wt-status: avoid strbuf_split*() strbuf is a very good data structure to work with string data without having to worry about running past the end of the string, but strbuf_split() is a wrong API and an array of strbuf that the function produces is a wrong thing to use in general. You do not edit these N strings split out of a single strbuf simultaneously. Often it is much better off to split a string into string_list and work with the resulting strings. wt-status.c:abbrev_oid_in_line() takes one line of rebase todo list (like "pick e813a0200a7121b97fec535f0d0b460b0a33356c title"), and for instructions that has an object name as the second token on the line, replace the object name with its unique abbreviation. After splitting these tokens out of a single line, no simultaneous edit on any of these pieces of string that takes advantage of strbuf API takes place. The final string is composed with strbuf API, but these split pieces are merely used as pieces of strings and there is no need for them to be stored in individual strbuf. Instead, split the line into a string_list, and compose the final string using these pieces. Signed-off-by: Junio C Hamano --- wt-status.c | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/wt-status.c b/wt-status.c index 454601afa15..a34dc144ee3 100644 --- a/wt-status.c +++ b/wt-status.c @@ -1351,8 +1351,8 @@ static int split_commit_in_progress(struct wt_status *s) */ static void abbrev_oid_in_line(struct strbuf *line) { - struct strbuf **split; - int i; + struct string_list split = STRING_LIST_INIT_DUP; + struct object_id oid; if (starts_with(line->buf, "exec ") || starts_with(line->buf, "x ") || @@ -1360,26 +1360,15 @@ static void abbrev_oid_in_line(struct strbuf *line) starts_with(line->buf, "l ")) return; - split = strbuf_split_max(line, ' ', 3); - if (split[0] && split[1]) { - struct object_id oid; - - /* - * strbuf_split_max left a space. Trim it and re-add - * it after abbreviation. - */ - strbuf_trim(split[1]); - if (!repo_get_oid(the_repository, split[1]->buf, &oid)) { - strbuf_reset(split[1]); - strbuf_add_unique_abbrev(split[1], &oid, - DEFAULT_ABBREV); - strbuf_addch(split[1], ' '); - strbuf_reset(line); - for (i = 0; split[i]; i++) - strbuf_addbuf(line, split[i]); - } + if ((2 <= string_list_split(&split, line->buf, " ", 2)) && + !repo_get_oid(the_repository, split.items[1].string, &oid)) { + strbuf_reset(line); + strbuf_addf(line, "%s ", split.items[0].string); + strbuf_add_unique_abbrev(line, &oid, DEFAULT_ABBREV); + for (size_t i = 2; i < split.nr; i++) + strbuf_addf(line, " %s", split.items[i].string); } - strbuf_list_free(split); + string_list_clear(&split, 0); } static int read_rebase_todolist(const char *fname, struct string_list *lines) -- GitLab From 899ff9c1755a84925704c18250fb7ac1afb302c0 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 31 Jul 2025 15:54:24 -0700 Subject: [PATCH 010/371] clean: do not pass strbuf by value When you pass a structure by value, the callee can modify the contents of the structure that was passed in without having to worry about changing the structure the caller has. Passing structure by value sometimes (but not very often) can be a valid way to give callee a temporary variable it can freely modify. But not a structure with members that are pointers, like a strbuf. builtin/clean.c:list_and_choose() reads a line interactively from the user, and passes the line (in a strbuf) to parse_choice() by value, which then munges by replacing ',' with ' ' (to accept both comma and space separated list of choices). But because the strbuf passed by value still shares the underlying character array buf[], this ends up munging the caller's strbuf contents. This is a catastrophe waiting to happen. If the callee causes the strbuf to be reallocated, the buf[] the caller has will become dangling, and when the caller does strbuf_release(), it would result in double-free. Stop calling the function with misleading call-by-value with strbuf. Signed-off-by: Junio C Hamano --- builtin/clean.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/builtin/clean.c b/builtin/clean.c index 053c94fc6bd..224551537e3 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -477,7 +477,7 @@ static int find_unique(const char *choice, struct menu_stuff *menu_stuff) */ static int parse_choice(struct menu_stuff *menu_stuff, int is_single, - struct strbuf input, + struct strbuf *input, int **chosen) { struct strbuf **choice_list, **ptr; @@ -485,14 +485,14 @@ static int parse_choice(struct menu_stuff *menu_stuff, int i; if (is_single) { - choice_list = strbuf_split_max(&input, '\n', 0); + choice_list = strbuf_split_max(input, '\n', 0); } else { - char *p = input.buf; + char *p = input->buf; do { if (*p == ',') *p = ' '; } while (*p++); - choice_list = strbuf_split_max(&input, ' ', 0); + choice_list = strbuf_split_max(input, ' ', 0); } for (ptr = choice_list; *ptr; ptr++) { @@ -630,7 +630,7 @@ static int *list_and_choose(struct menu_opts *opts, struct menu_stuff *stuff) nr = parse_choice(stuff, opts->flags & MENU_OPTS_SINGLETON, - choice, + &choice, &chosen); if (opts->flags & MENU_OPTS_SINGLETON) { -- GitLab From 7a4acc360782c9eb0e53f51a5cf3147fa88f973e Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 31 Jul 2025 15:54:25 -0700 Subject: [PATCH 011/371] clean: do not use strbuf_split*() [part 1] builtin/clean.c:parse_choice() is fed a single line of input, which is space or comma separated list of tokens, and a list of menu items. It parses the tokens into number ranges (e.g. 1-3 that means the first three items) or string prefix (e.g. 's' to choose the menu item "(s)elect") that specify the elements in the menu item list, and tells the caller which ones are chosen. For parsing the input string, it uses strbuf_split() to split it into bunch of strbufs. Instead use string_list_split_in_place(), for a few reasons. * strbuf_split() is a bad API function to use, that yields an array of strbuf that is a bad data structure to use in general. * string_list_split_in_place() allows you to split with "comma or space"; the current code has to preprocess the input string to replace comma with space because strbuf_split() does not allow this. Signed-off-by: Junio C Hamano --- builtin/clean.c | 50 +++++++++++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/builtin/clean.c b/builtin/clean.c index 224551537e3..708cd9344ca 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -480,40 +480,36 @@ static int parse_choice(struct menu_stuff *menu_stuff, struct strbuf *input, int **chosen) { - struct strbuf **choice_list, **ptr; + struct string_list choice = STRING_LIST_INIT_NODUP; + struct string_list_item *item; int nr = 0; int i; - if (is_single) { - choice_list = strbuf_split_max(input, '\n', 0); - } else { - char *p = input->buf; - do { - if (*p == ',') - *p = ' '; - } while (*p++); - choice_list = strbuf_split_max(input, ' ', 0); - } + string_list_split_in_place_f(&choice, input->buf, + is_single ? "\n" : ", ", -1, + STRING_LIST_SPLIT_TRIM); - for (ptr = choice_list; *ptr; ptr++) { - char *p; - int choose = 1; + for_each_string_list_item(item, &choice) { + const char *string; + int choose; int bottom = 0, top = 0; int is_range, is_number; - strbuf_trim(*ptr); - if (!(*ptr)->len) + string = item->string; + if (!*string) continue; /* Input that begins with '-'; unchoose */ - if (*(*ptr)->buf == '-') { + if (string[0] == '-') { choose = 0; - strbuf_remove((*ptr), 0, 1); + string++; + } else { + choose = 1; } is_range = 0; is_number = 1; - for (p = (*ptr)->buf; *p; p++) { + for (const char *p = string; *p; p++) { if ('-' == *p) { if (!is_range) { is_range = 1; @@ -531,27 +527,27 @@ static int parse_choice(struct menu_stuff *menu_stuff, } if (is_number) { - bottom = atoi((*ptr)->buf); + bottom = atoi(string); top = bottom; } else if (is_range) { - bottom = atoi((*ptr)->buf); + bottom = atoi(string); /* a range can be specified like 5-7 or 5- */ - if (!*(strchr((*ptr)->buf, '-') + 1)) + if (!*(strchr(string, '-') + 1)) top = menu_stuff->nr; else - top = atoi(strchr((*ptr)->buf, '-') + 1); - } else if (!strcmp((*ptr)->buf, "*")) { + top = atoi(strchr(string, '-') + 1); + } else if (!strcmp(string, "*")) { bottom = 1; top = menu_stuff->nr; } else { - bottom = find_unique((*ptr)->buf, menu_stuff); + bottom = find_unique(string, menu_stuff); top = bottom; } if (top <= 0 || bottom <= 0 || top > menu_stuff->nr || bottom > top || (is_single && bottom != top)) { clean_print_color(CLEAN_COLOR_ERROR); - printf(_("Huh (%s)?\n"), (*ptr)->buf); + printf(_("Huh (%s)?\n"), string); clean_print_color(CLEAN_COLOR_RESET); continue; } @@ -560,7 +556,7 @@ static int parse_choice(struct menu_stuff *menu_stuff, (*chosen)[i-1] = choose; } - strbuf_list_free(choice_list); + string_list_clear(&choice, 0); for (i = 0; i < menu_stuff->nr; i++) nr += (*chosen)[i]; -- GitLab From 4985f72ea5133441c2e9ba808bdea861a2d9f042 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sat, 2 Aug 2025 22:42:29 -0700 Subject: [PATCH 012/371] clean: do not pass the whole structure when it is not necessary The callee parse_choice() only needs to access a NUL-terminated string; instead of insisting to take a pointer to a strbuf, just take a pointer to a character array. Signed-off-by: Junio C Hamano --- builtin/clean.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/clean.c b/builtin/clean.c index 708cd9344ca..9bb920e7fdc 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -477,7 +477,7 @@ static int find_unique(const char *choice, struct menu_stuff *menu_stuff) */ static int parse_choice(struct menu_stuff *menu_stuff, int is_single, - struct strbuf *input, + char *input, int **chosen) { struct string_list choice = STRING_LIST_INIT_NODUP; @@ -485,7 +485,7 @@ static int parse_choice(struct menu_stuff *menu_stuff, int nr = 0; int i; - string_list_split_in_place_f(&choice, input->buf, + string_list_split_in_place_f(&choice, input, is_single ? "\n" : ", ", -1, STRING_LIST_SPLIT_TRIM); @@ -626,7 +626,7 @@ static int *list_and_choose(struct menu_opts *opts, struct menu_stuff *stuff) nr = parse_choice(stuff, opts->flags & MENU_OPTS_SINGLETON, - &choice, + choice.buf, &chosen); if (opts->flags & MENU_OPTS_SINGLETON) { -- GitLab From 4f60672f6f7cbc61fb704c993c54187860f1e9c8 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 31 Jul 2025 15:54:26 -0700 Subject: [PATCH 013/371] clean: do not use strbuf_split*() [part 2] builtin/clean.c:filter_by_patterns_cmd() interactively reads a line that has exclude patterns from the user and splits the line into a list of patterns. It uses the strbuf_split() so that each split piece can then trimmed. There is no need to use strbuf anymore, thanks to the recent enhancement to string_list_split*() family that allows us to trim the pieces split into a string_list. Signed-off-by: Junio C Hamano --- builtin/clean.c | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/builtin/clean.c b/builtin/clean.c index 9bb920e7fdc..38780edc395 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -674,12 +674,13 @@ static int filter_by_patterns_cmd(void) { struct dir_struct dir = DIR_INIT; struct strbuf confirm = STRBUF_INIT; - struct strbuf **ignore_list; - struct string_list_item *item; struct pattern_list *pl; int changed = -1, i; for (;;) { + struct string_list ignore_list = STRING_LIST_INIT_NODUP; + struct string_list_item *item; + if (!del_list.nr) break; @@ -697,14 +698,15 @@ static int filter_by_patterns_cmd(void) break; pl = add_pattern_list(&dir, EXC_CMDL, "manual exclude"); - ignore_list = strbuf_split_max(&confirm, ' ', 0); - for (i = 0; ignore_list[i]; i++) { - strbuf_trim(ignore_list[i]); - if (!ignore_list[i]->len) - continue; + string_list_split_in_place_f(&ignore_list, confirm.buf, " ", -1, + STRING_LIST_SPLIT_TRIM); - add_pattern(ignore_list[i]->buf, "", 0, pl, -(i+1)); + for (i = 0; i < ignore_list.nr; i++) { + item = &ignore_list.items[i]; + if (!*item->string) + continue; + add_pattern(item->string, "", 0, pl, -(i+1)); } changed = 0; @@ -725,7 +727,7 @@ static int filter_by_patterns_cmd(void) clean_print_color(CLEAN_COLOR_RESET); } - strbuf_list_free(ignore_list); + string_list_clear(&ignore_list, 0); dir_clear(&dir); } -- GitLab From d33091220dadedfcb874d179fe164f507d5f09b2 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 31 Jul 2025 15:54:27 -0700 Subject: [PATCH 014/371] merge-tree: do not use strbuf_split*() When reading merge instructions from the standard input, the program reads from the standard input, splits the line into tokens at whitespace, and trims each of them before using. We no longer need to use strbuf just for trimming, as string_list_split*() family can trim while splitting a string. Signed-off-by: Junio C Hamano --- builtin/merge-tree.c | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/builtin/merge-tree.c b/builtin/merge-tree.c index cf8b06cadc7..70235856d7a 100644 --- a/builtin/merge-tree.c +++ b/builtin/merge-tree.c @@ -618,32 +618,34 @@ int cmd_merge_tree(int argc, "--merge-base", "--stdin"); line_termination = '\0'; while (strbuf_getline_lf(&buf, stdin) != EOF) { - struct strbuf **split; + struct string_list split = STRING_LIST_INIT_NODUP; const char *input_merge_base = NULL; - split = strbuf_split(&buf, ' '); - if (!split[0] || !split[1]) + string_list_split_in_place_f(&split, buf.buf, " ", -1, + STRING_LIST_SPLIT_TRIM); + + if (split.nr < 2) die(_("malformed input line: '%s'."), buf.buf); - strbuf_rtrim(split[0]); - strbuf_rtrim(split[1]); /* parse the merge-base */ - if (!strcmp(split[1]->buf, "--")) { - input_merge_base = split[0]->buf; + if (!strcmp(split.items[1].string, "--")) { + input_merge_base = split.items[0].string; } - if (input_merge_base && split[2] && split[3] && !split[4]) { - strbuf_rtrim(split[2]); - strbuf_rtrim(split[3]); - real_merge(&o, input_merge_base, split[2]->buf, split[3]->buf, prefix); - } else if (!input_merge_base && !split[2]) { - real_merge(&o, NULL, split[0]->buf, split[1]->buf, prefix); + if (input_merge_base && split.nr == 4) { + real_merge(&o, input_merge_base, + split.items[2].string, split.items[3].string, + prefix); + } else if (!input_merge_base && split.nr == 2) { + real_merge(&o, NULL, + split.items[0].string, split.items[1].string, + prefix); } else { die(_("malformed input line: '%s'."), buf.buf); } maybe_flush_or_die(stdout, "stdout"); - strbuf_list_free(split); + string_list_clear(&split, 0); } strbuf_release(&buf); -- GitLab From 566e91049558cf9837e2f760877437b929fbb232 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 31 Jul 2025 15:54:28 -0700 Subject: [PATCH 015/371] notes: do not use strbuf_split*() When reading copy instructions from the standard input, the program reads a line, splits it into tokens at whitespace, and trims each of the tokens before using. We no longer need to use strbuf just to be able to trim, as string_list_split*() family now can trim while splitting a string. Retire the use of strbuf_split() from this code path. Note that this loop is a bit sloppy in that it ensures at least there are two tokens on each line, but ignores if there are extra tokens on the line. Tightening it is outside the scope of this series. Signed-off-by: Junio C Hamano --- builtin/notes.c | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/builtin/notes.c b/builtin/notes.c index a9529b1696a..4fb36a743cd 100644 --- a/builtin/notes.c +++ b/builtin/notes.c @@ -375,18 +375,19 @@ static int notes_copy_from_stdin(int force, const char *rewrite_cmd) while (strbuf_getline_lf(&buf, stdin) != EOF) { struct object_id from_obj, to_obj; - struct strbuf **split; + struct string_list split = STRING_LIST_INIT_NODUP; int err; - split = strbuf_split(&buf, ' '); - if (!split[0] || !split[1]) + string_list_split_in_place_f(&split, buf.buf, " ", -1, + STRING_LIST_SPLIT_TRIM); + if (split.nr < 2) die(_("malformed input line: '%s'."), buf.buf); - strbuf_rtrim(split[0]); - strbuf_rtrim(split[1]); - if (repo_get_oid(the_repository, split[0]->buf, &from_obj)) - die(_("failed to resolve '%s' as a valid ref."), split[0]->buf); - if (repo_get_oid(the_repository, split[1]->buf, &to_obj)) - die(_("failed to resolve '%s' as a valid ref."), split[1]->buf); + if (repo_get_oid(the_repository, split.items[0].string, &from_obj)) + die(_("failed to resolve '%s' as a valid ref."), + split.items[0].string); + if (repo_get_oid(the_repository, split.items[1].string, &to_obj)) + die(_("failed to resolve '%s' as a valid ref."), + split.items[1].string); if (rewrite_cmd) err = copy_note_for_rewrite(c, &from_obj, &to_obj); @@ -396,11 +397,11 @@ static int notes_copy_from_stdin(int force, const char *rewrite_cmd) if (err) { error(_("failed to copy notes from '%s' to '%s'"), - split[0]->buf, split[1]->buf); + split.items[0].string, split.items[1].string); ret = 1; } - strbuf_list_free(split); + string_list_clear(&split, 0); } if (!rewrite_cmd) { -- GitLab From dcecac2580ef871186fdc4e9efc87815a4ce4c66 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 31 Jul 2025 15:54:29 -0700 Subject: [PATCH 016/371] config: do not use strbuf_split() When parsing an old-style GIT_CONFIG_PARAMETERS environment variable, the code parses key=value pairs by splitting them at '=' into an array of strbuf's. As strbuf_split() leaves the delimiter at the end of the split piece, the code has to manually trim it. If we split with string_list_split(), that becomes unnecessary. Retire the use of strbuf_split() from this code path. Note that the max parameter of string_list_split() is of an ergonomically iffy design---it specifies the maximum number of times the function is allowed to split, which means that in order to split a text into up to 2 pieces, you have to pass 1, not 2. Signed-off-by: Junio C Hamano --- config.c | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/config.c b/config.c index 8a2d0b79164..1769f15ee31 100644 --- a/config.c +++ b/config.c @@ -638,31 +638,28 @@ int git_config_parse_parameter(const char *text, config_fn_t fn, void *data) { const char *value; - struct strbuf **pair; + struct string_list pair = STRING_LIST_INIT_DUP; int ret; struct key_value_info kvi = KVI_INIT; kvi_from_param(&kvi); - pair = strbuf_split_str(text, '=', 2); - if (!pair[0]) + string_list_split(&pair, text, "=", 1); + if (!pair.nr) return error(_("bogus config parameter: %s"), text); - if (pair[0]->len && pair[0]->buf[pair[0]->len - 1] == '=') { - strbuf_setlen(pair[0], pair[0]->len - 1); - value = pair[1] ? pair[1]->buf : ""; - } else { + if (pair.nr == 1) value = NULL; - } + else + value = pair.items[1].string; - strbuf_trim(pair[0]); - if (!pair[0]->len) { - strbuf_list_free(pair); + if (!*pair.items[0].string) { + string_list_clear(&pair, 0); return error(_("bogus config parameter: %s"), text); } - ret = config_parse_pair(pair[0]->buf, value, &kvi, fn, data); - strbuf_list_free(pair); + ret = config_parse_pair(pair.items[0].string, value, &kvi, fn, data); + string_list_clear(&pair, 0); return ret; } -- GitLab From b894d4481f4068a84323dfc7048f007b3df5234d Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 31 Jul 2025 15:54:30 -0700 Subject: [PATCH 017/371] environment: do not use strbuf_split*() environment.c:get_git_namespace() learns the raw namespace from an environment variable, splits it at "/", and appends them after "refs/namespaces/"; the reason why it splits first is so that an empty string resulting from double slashes can be omitted. The split pieces do not need to be edited in any way, so an array of strbufs is a wrong data structure to use. Instead split into a string list and use the pieces from there. Signed-off-by: Junio C Hamano --- environment.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/environment.c b/environment.c index 7c2480b22e5..ab3ed08433d 100644 --- a/environment.c +++ b/environment.c @@ -163,10 +163,10 @@ int have_git_dir(void) const char *get_git_namespace(void) { static const char *namespace; - struct strbuf buf = STRBUF_INIT; - struct strbuf **components, **c; const char *raw_namespace; + struct string_list components = STRING_LIST_INIT_DUP; + struct string_list_item *item; if (namespace) return namespace; @@ -178,12 +178,17 @@ const char *get_git_namespace(void) } strbuf_addstr(&buf, raw_namespace); - components = strbuf_split(&buf, '/'); + + string_list_split(&components, buf.buf, "/", -1); strbuf_reset(&buf); - for (c = components; *c; c++) - if (strcmp((*c)->buf, "/") != 0) - strbuf_addf(&buf, "refs/namespaces/%s", (*c)->buf); - strbuf_list_free(components); + + for_each_string_list_item(item, &components) { + if (item->string[0]) + strbuf_addf(&buf, "refs/namespaces/%s/", item->string); + } + string_list_clear(&components, 0); + + strbuf_trim_trailing_dir_sep(&buf); if (check_refname_format(buf.buf, 0)) die(_("bad git namespace path \"%s\""), raw_namespace); strbuf_addch(&buf, '/'); -- GitLab From d6fd08bd760711d51b98f9ad98c3cd94d90d2618 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 31 Jul 2025 15:54:31 -0700 Subject: [PATCH 018/371] sub-process: do not use strbuf_split*() The code to read status from subprocess reads one packet line and tries to find "status=". It is way overkill to split the line into an array of two strbufs to extract . Signed-off-by: Junio C Hamano --- sub-process.c | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/sub-process.c b/sub-process.c index 1daf5a97525..83bf0a0e82e 100644 --- a/sub-process.c +++ b/sub-process.c @@ -30,23 +30,20 @@ struct subprocess_entry *subprocess_find_entry(struct hashmap *hashmap, const ch int subprocess_read_status(int fd, struct strbuf *status) { - struct strbuf **pair; - char *line; int len; for (;;) { + char *line; + const char *value; + len = packet_read_line_gently(fd, NULL, &line); if ((len < 0) || !line) break; - pair = strbuf_split_str(line, '=', 2); - if (pair[0] && pair[0]->len && pair[1]) { + if (skip_prefix(line, "status=", &value)) { /* the last "status=" line wins */ - if (!strcmp(pair[0]->buf, "status=")) { - strbuf_reset(status); - strbuf_addbuf(status, pair[1]); - } + strbuf_reset(status); + strbuf_addstr(status, value); } - strbuf_list_free(pair); } return (len < 0) ? len : 0; -- GitLab From cb8e82a6414653d5dbda81eedb8ca0cd9ce34c68 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 31 Jul 2025 15:54:32 -0700 Subject: [PATCH 019/371] trace2: trim_trailing_newline followed by trim is a no-op strbuf_trim_trailing_newline() removes a LF or a CRLF from the tail of a string. If the code plans to call strbuf_trim() immediately after doing so, the code is better off skipping the EOL trimming in the first place. After all, LF/CRLF at the end is a mere special case of whitespaces at the end of the string, which will be removed by strbuf_rtrim() anyway. Signed-off-by: Junio C Hamano --- trace2/tr2_cfg.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/trace2/tr2_cfg.c b/trace2/tr2_cfg.c index 22a99a0682a..2b7cfcd10c7 100644 --- a/trace2/tr2_cfg.c +++ b/trace2/tr2_cfg.c @@ -39,7 +39,6 @@ static int tr2_cfg_load_patterns(void) if (buf->len && buf->buf[buf->len - 1] == ',') strbuf_setlen(buf, buf->len - 1); - strbuf_trim_trailing_newline(*s); strbuf_trim(*s); } @@ -78,7 +77,6 @@ static int tr2_load_env_vars(void) if (buf->len && buf->buf[buf->len - 1] == ',') strbuf_setlen(buf, buf->len - 1); - strbuf_trim_trailing_newline(*s); strbuf_trim(*s); } -- GitLab From 838fe56920684bf0ab734f7ddf2bad69cb5f5d45 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 31 Jul 2025 15:54:33 -0700 Subject: [PATCH 020/371] trace2: do not use strbuf_split*() tr2_cfg_load_patterns() and tr2_load_env_vars() functions are functions with very similar structure that each reads an environment variable, splits its value at the ',' boundaries, and trims the resulting string pieces into an array of strbufs. But the code paths that later use these strbufs take no advantage of the strbuf-ness of the result (they do not benefit from representation to avoid having to run strlen(), for example). Simplify the code by teaching these functions to split into a string list instead; even the trimming comes for free ;-). Signed-off-by: Junio C Hamano --- trace2/tr2_cfg.c | 78 +++++++++++++++++------------------------------- 1 file changed, 27 insertions(+), 51 deletions(-) diff --git a/trace2/tr2_cfg.c b/trace2/tr2_cfg.c index 2b7cfcd10c7..bbcfeda60af 100644 --- a/trace2/tr2_cfg.c +++ b/trace2/tr2_cfg.c @@ -8,87 +8,65 @@ #include "trace2/tr2_sysenv.h" #include "wildmatch.h" -static struct strbuf **tr2_cfg_patterns; -static int tr2_cfg_count_patterns; +static struct string_list tr2_cfg_patterns = STRING_LIST_INIT_DUP; static int tr2_cfg_loaded; -static struct strbuf **tr2_cfg_env_vars; -static int tr2_cfg_env_vars_count; +static struct string_list tr2_cfg_env_vars = STRING_LIST_INIT_DUP; static int tr2_cfg_env_vars_loaded; /* * Parse a string containing a comma-delimited list of config keys - * or wildcard patterns into a list of strbufs. + * or wildcard patterns into a string list. */ -static int tr2_cfg_load_patterns(void) +static size_t tr2_cfg_load_patterns(void) { - struct strbuf **s; const char *envvar; if (tr2_cfg_loaded) - return tr2_cfg_count_patterns; + return tr2_cfg_patterns.nr; tr2_cfg_loaded = 1; envvar = tr2_sysenv_get(TR2_SYSENV_CFG_PARAM); if (!envvar || !*envvar) - return tr2_cfg_count_patterns; + return tr2_cfg_patterns.nr; - tr2_cfg_patterns = strbuf_split_buf(envvar, strlen(envvar), ',', -1); - for (s = tr2_cfg_patterns; *s; s++) { - struct strbuf *buf = *s; - - if (buf->len && buf->buf[buf->len - 1] == ',') - strbuf_setlen(buf, buf->len - 1); - strbuf_trim(*s); - } - - tr2_cfg_count_patterns = s - tr2_cfg_patterns; - return tr2_cfg_count_patterns; + string_list_split_f(&tr2_cfg_patterns, envvar, ",", -1, + STRING_LIST_SPLIT_TRIM); + return tr2_cfg_patterns.nr; } void tr2_cfg_free_patterns(void) { - if (tr2_cfg_patterns) - strbuf_list_free(tr2_cfg_patterns); - tr2_cfg_count_patterns = 0; + if (tr2_cfg_patterns.nr) + string_list_clear(&tr2_cfg_patterns, 0); tr2_cfg_loaded = 0; } /* * Parse a string containing a comma-delimited list of environment variable - * names into a list of strbufs. + * names into a string list. */ -static int tr2_load_env_vars(void) +static size_t tr2_load_env_vars(void) { - struct strbuf **s; const char *varlist; if (tr2_cfg_env_vars_loaded) - return tr2_cfg_env_vars_count; + return tr2_cfg_env_vars.nr; tr2_cfg_env_vars_loaded = 1; varlist = tr2_sysenv_get(TR2_SYSENV_ENV_VARS); if (!varlist || !*varlist) - return tr2_cfg_env_vars_count; - - tr2_cfg_env_vars = strbuf_split_buf(varlist, strlen(varlist), ',', -1); - for (s = tr2_cfg_env_vars; *s; s++) { - struct strbuf *buf = *s; - - if (buf->len && buf->buf[buf->len - 1] == ',') - strbuf_setlen(buf, buf->len - 1); - strbuf_trim(*s); - } + return tr2_cfg_env_vars.nr; - tr2_cfg_env_vars_count = s - tr2_cfg_env_vars; - return tr2_cfg_env_vars_count; + string_list_split_f(&tr2_cfg_env_vars, varlist, ",", -1, + STRING_LIST_SPLIT_TRIM); + return tr2_cfg_env_vars.nr; } void tr2_cfg_free_env_vars(void) { - if (tr2_cfg_env_vars) - strbuf_list_free(tr2_cfg_env_vars); - tr2_cfg_env_vars_count = 0; + if (tr2_cfg_env_vars.nr) + string_list_clear(&tr2_cfg_env_vars, 0); tr2_cfg_env_vars_loaded = 0; } @@ -103,12 +81,11 @@ struct tr2_cfg_data { static int tr2_cfg_cb(const char *key, const char *value, const struct config_context *ctx, void *d) { - struct strbuf **s; + struct string_list_item *item; struct tr2_cfg_data *data = (struct tr2_cfg_data *)d; - for (s = tr2_cfg_patterns; *s; s++) { - struct strbuf *buf = *s; - int wm = wildmatch(buf->buf, key, WM_CASEFOLD); + for_each_string_list_item(item, &tr2_cfg_patterns) { + int wm = wildmatch(item->string, key, WM_CASEFOLD); if (wm == WM_MATCH) { trace2_def_param_fl(data->file, data->line, key, value, ctx->kvi); @@ -130,17 +107,16 @@ void tr2_cfg_list_config_fl(const char *file, int line) void tr2_list_env_vars_fl(const char *file, int line) { struct key_value_info kvi = KVI_INIT; - struct strbuf **s; + struct string_list_item *item; kvi_from_param(&kvi); if (tr2_load_env_vars() <= 0) return; - for (s = tr2_cfg_env_vars; *s; s++) { - struct strbuf *buf = *s; - const char *val = getenv(buf->buf); + for_each_string_list_item(item, &tr2_cfg_env_vars) { + const char *val = getenv(item->string); if (val && *val) - trace2_def_param_fl(file, line, buf->buf, val, &kvi); + trace2_def_param_fl(file, line, item->string, val, &kvi); } } -- GitLab From 66e2adb8f6fe97bb480d96205fb3473b8c1fe4df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Sun, 3 Aug 2025 13:38:29 +0200 Subject: [PATCH 021/371] describe: use prio_queue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the use a list-based priority queue whose order is maintained by commit_list_insert_by_date() with a prio_queue. This avoids quadratic worst-case complexity. And in the somewhat contrived example of describing the 4751 commits from v2.41.0 to v2.47.0 in one go (to get a sizable chunk of describe work with minimal ref loading overhead) it's significantly faster: Benchmark 1: ./git_2.50.1 describe $(git rev-list v2.41.0..v2.47.0) Time (mean ± σ): 1.558 s ± 0.002 s [User: 1.492 s, System: 0.051 s] Range (min … max): 1.557 s … 1.562 s 10 runs Benchmark 2: ./git describe $(git rev-list v2.41.0..v2.47.0) Time (mean ± σ): 1.209 s ± 0.006 s [User: 1.143 s, System: 0.051 s] Range (min … max): 1.201 s … 1.219 s 10 runs Summary ./git describe $(git rev-list v2.41.0..v2.47.0) ran 1.29 ± 0.01 times faster than ./git_2.50.1 describe $(git rev-list v2.41.0..v2.47.0) Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- builtin/describe.c | 51 ++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/builtin/describe.c b/builtin/describe.c index fbf305d7624..80722ae0c04 100644 --- a/builtin/describe.c +++ b/builtin/describe.c @@ -23,6 +23,7 @@ #include "list-objects.h" #include "commit-slab.h" #include "wildmatch.h" +#include "prio-queue.h" #define MAX_TAGS (FLAG_BITS - 1) #define DEFAULT_CANDIDATES 10 @@ -249,24 +250,26 @@ static int compare_pt(const void *a_, const void *b_) return 0; } -static unsigned long finish_depth_computation( - struct commit_list **list, - struct possible_tag *best) +static bool all_have_flag(const struct prio_queue *queue, unsigned flag) +{ + for (size_t i = 0; i < queue->nr; i++) { + struct commit *commit = queue->array[i].data; + if (!(commit->object.flags & flag)) + return false; + } + return true; +} + +static unsigned long finish_depth_computation(struct prio_queue *queue, + struct possible_tag *best) { unsigned long seen_commits = 0; - while (*list) { - struct commit *c = pop_commit(list); + while (queue->nr) { + struct commit *c = prio_queue_get(queue); struct commit_list *parents = c->parents; seen_commits++; if (c->object.flags & best->flag_within) { - struct commit_list *a = *list; - while (a) { - struct commit *i = a->item; - if (!(i->object.flags & best->flag_within)) - break; - a = a->next; - } - if (!a) + if (all_have_flag(queue, best->flag_within)) break; } else best->depth++; @@ -274,7 +277,7 @@ static unsigned long finish_depth_computation( struct commit *p = parents->item; repo_parse_commit(the_repository, p); if (!(p->object.flags & SEEN)) - commit_list_insert_by_date(p, list); + prio_queue_put(queue, p); p->object.flags |= c->object.flags; parents = parents->next; } @@ -316,7 +319,7 @@ static void append_suffix(int depth, const struct object_id *oid, struct strbuf static void describe_commit(struct object_id *oid, struct strbuf *dst) { struct commit *cmit, *gave_up_on = NULL; - struct commit_list *list; + struct prio_queue queue = { compare_commits_by_commit_date }; struct commit_name *n; struct possible_tag all_matches[MAX_TAGS]; unsigned int match_cnt = 0, annotated_cnt = 0, cur_match; @@ -359,11 +362,10 @@ static void describe_commit(struct object_id *oid, struct strbuf *dst) have_util = 1; } - list = NULL; cmit->object.flags = SEEN; - commit_list_insert(cmit, &list); - while (list) { - struct commit *c = pop_commit(&list); + prio_queue_put(&queue, cmit); + while (queue.nr) { + struct commit *c = prio_queue_get(&queue); struct commit_list *parents = c->parents; struct commit_name **slot; @@ -397,7 +399,7 @@ static void describe_commit(struct object_id *oid, struct strbuf *dst) t->depth++; } /* Stop if last remaining path already covered by best candidate(s) */ - if (annotated_cnt && !list) { + if (annotated_cnt && !queue.nr) { int best_depth = INT_MAX; unsigned best_within = 0; for (cur_match = 0; cur_match < match_cnt; cur_match++) { @@ -420,7 +422,7 @@ static void describe_commit(struct object_id *oid, struct strbuf *dst) struct commit *p = parents->item; repo_parse_commit(the_repository, p); if (!(p->object.flags & SEEN)) - commit_list_insert_by_date(p, &list); + prio_queue_put(&queue, p); p->object.flags |= c->object.flags; parents = parents->next; @@ -435,6 +437,7 @@ static void describe_commit(struct object_id *oid, struct strbuf *dst) strbuf_add_unique_abbrev(dst, cmit_oid, abbrev); if (suffix) strbuf_addstr(dst, suffix); + clear_prio_queue(&queue); return; } if (unannotated_cnt) @@ -450,11 +453,11 @@ static void describe_commit(struct object_id *oid, struct strbuf *dst) QSORT(all_matches, match_cnt, compare_pt); if (gave_up_on) { - commit_list_insert_by_date(gave_up_on, &list); + prio_queue_put(&queue, gave_up_on); seen_commits--; } - seen_commits += finish_depth_computation(&list, &all_matches[0]); - free_commit_list(list); + seen_commits += finish_depth_computation(&queue, &all_matches[0]); + clear_prio_queue(&queue); if (debug) { static int label_width = -1; -- GitLab From 08bb69d70f55cb6b44cdc6aefa7bc1d9cf4eb3f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Sun, 3 Aug 2025 13:49:11 +0200 Subject: [PATCH 022/371] describe: use prio_queue_replace() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Optimize the sequence get+put to peek+replace to avoid one unnecessary heap rebalance. Do that by tracking partial get operations in a prio_queue wrapper, struct lazy_queue, and using wrapper functions that turn get into peek and put into replace as needed. This is simpler than tracking the state explicitly in the calling code. We get a nice speedup on top of the previous patch's conversion to prio_queue: Benchmark 1: ./git_2.50.1 describe $(git rev-list v2.41.0..v2.47.0) Time (mean ± σ): 1.559 s ± 0.002 s [User: 1.493 s, System: 0.051 s] Range (min … max): 1.556 s … 1.563 s 10 runs Benchmark 2: ./git_describe_pq describe $(git rev-list v2.41.0..v2.47.0) Time (mean ± σ): 1.204 s ± 0.001 s [User: 1.138 s, System: 0.051 s] Range (min … max): 1.202 s … 1.205 s 10 runs Benchmark 3: ./git describe $(git rev-list v2.41.0..v2.47.0) Time (mean ± σ): 850.9 ms ± 1.6 ms [User: 786.6 ms, System: 49.8 ms] Range (min … max): 849.1 ms … 854.1 ms 10 runs Summary ./git describe $(git rev-list v2.41.0..v2.47.0) ran 1.41 ± 0.00 times faster than ./git_describe_pq describe $(git rev-list v2.41.0..v2.47.0) 1.83 ± 0.00 times faster than ./git_2.50.1 describe $(git rev-list v2.41.0..v2.47.0) Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- builtin/describe.c | 68 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/builtin/describe.c b/builtin/describe.c index 80722ae0c04..c18e4b3e4b7 100644 --- a/builtin/describe.c +++ b/builtin/describe.c @@ -250,22 +250,58 @@ static int compare_pt(const void *a_, const void *b_) return 0; } -static bool all_have_flag(const struct prio_queue *queue, unsigned flag) +struct lazy_queue { + struct prio_queue queue; + bool get_pending; +}; + +#define LAZY_QUEUE_INIT { { compare_commits_by_commit_date }, false } + +static void *lazy_queue_get(struct lazy_queue *queue) +{ + if (queue->get_pending) + prio_queue_get(&queue->queue); + else + queue->get_pending = true; + return prio_queue_peek(&queue->queue); +} + +static void lazy_queue_put(struct lazy_queue *queue, void *thing) +{ + if (queue->get_pending) + prio_queue_replace(&queue->queue, thing); + else + prio_queue_put(&queue->queue, thing); + queue->get_pending = false; +} + +static bool lazy_queue_empty(const struct lazy_queue *queue) +{ + return queue->queue.nr == (queue->get_pending ? 1 : 0); +} + +static void lazy_queue_clear(struct lazy_queue *queue) +{ + clear_prio_queue(&queue->queue); + queue->get_pending = false; +} + +static bool all_have_flag(const struct lazy_queue *queue, unsigned flag) { - for (size_t i = 0; i < queue->nr; i++) { - struct commit *commit = queue->array[i].data; + for (size_t i = queue->get_pending ? 1 : 0; i < queue->queue.nr; i++) { + struct commit *commit = queue->queue.array[i].data; if (!(commit->object.flags & flag)) return false; } return true; } -static unsigned long finish_depth_computation(struct prio_queue *queue, +static unsigned long finish_depth_computation(struct lazy_queue *queue, struct possible_tag *best) { unsigned long seen_commits = 0; - while (queue->nr) { - struct commit *c = prio_queue_get(queue); + while (!lazy_queue_empty(queue)) { + struct commit *c = lazy_queue_get(queue); struct commit_list *parents = c->parents; seen_commits++; if (c->object.flags & best->flag_within) { @@ -277,7 +313,7 @@ static unsigned long finish_depth_computation(struct prio_queue *queue, struct commit *p = parents->item; repo_parse_commit(the_repository, p); if (!(p->object.flags & SEEN)) - prio_queue_put(queue, p); + lazy_queue_put(queue, p); p->object.flags |= c->object.flags; parents = parents->next; } @@ -319,7 +355,7 @@ static void append_suffix(int depth, const struct object_id *oid, struct strbuf static void describe_commit(struct object_id *oid, struct strbuf *dst) { struct commit *cmit, *gave_up_on = NULL; - struct prio_queue queue = { compare_commits_by_commit_date }; + struct lazy_queue queue = LAZY_QUEUE_INIT; struct commit_name *n; struct possible_tag all_matches[MAX_TAGS]; unsigned int match_cnt = 0, annotated_cnt = 0, cur_match; @@ -363,9 +399,9 @@ static void describe_commit(struct object_id *oid, struct strbuf *dst) } cmit->object.flags = SEEN; - prio_queue_put(&queue, cmit); - while (queue.nr) { - struct commit *c = prio_queue_get(&queue); + lazy_queue_put(&queue, cmit); + while (!lazy_queue_empty(&queue)) { + struct commit *c = lazy_queue_get(&queue); struct commit_list *parents = c->parents; struct commit_name **slot; @@ -399,7 +435,7 @@ static void describe_commit(struct object_id *oid, struct strbuf *dst) t->depth++; } /* Stop if last remaining path already covered by best candidate(s) */ - if (annotated_cnt && !queue.nr) { + if (annotated_cnt && lazy_queue_empty(&queue)) { int best_depth = INT_MAX; unsigned best_within = 0; for (cur_match = 0; cur_match < match_cnt; cur_match++) { @@ -422,7 +458,7 @@ static void describe_commit(struct object_id *oid, struct strbuf *dst) struct commit *p = parents->item; repo_parse_commit(the_repository, p); if (!(p->object.flags & SEEN)) - prio_queue_put(&queue, p); + lazy_queue_put(&queue, p); p->object.flags |= c->object.flags; parents = parents->next; @@ -437,7 +473,7 @@ static void describe_commit(struct object_id *oid, struct strbuf *dst) strbuf_add_unique_abbrev(dst, cmit_oid, abbrev); if (suffix) strbuf_addstr(dst, suffix); - clear_prio_queue(&queue); + lazy_queue_clear(&queue); return; } if (unannotated_cnt) @@ -453,11 +489,11 @@ static void describe_commit(struct object_id *oid, struct strbuf *dst) QSORT(all_matches, match_cnt, compare_pt); if (gave_up_on) { - prio_queue_put(&queue, gave_up_on); + lazy_queue_put(&queue, gave_up_on); seen_commits--; } seen_commits += finish_depth_computation(&queue, &all_matches[0]); - clear_prio_queue(&queue); + lazy_queue_clear(&queue); if (debug) { static int label_width = -1; -- GitLab From 19623eb97e4edf76d585100c605037f9e51f6987 Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:53 +0530 Subject: [PATCH 023/371] doc: factor out common option In preparation for adding documentation for `git refs list`, factor out the common options from the `git-for-each-ref` man page into a shareable file `for-each-ref-options.adoc` and update `git-for-each-ref.adoc` to use an `include::` macro. This change is a pure refactoring and results in no change to the final rendered documentation for `for-each-ref`. Mentored-by: Patrick Steinhardt Mentored-by: shejialuo Mentored-by: Karthik Nayak Signed-off-by: Meet Soni Signed-off-by: Junio C Hamano --- Documentation/for-each-ref-options.adoc | 88 ++++++++++++++++++++++++ Documentation/git-for-each-ref.adoc | 89 +------------------------ 2 files changed, 89 insertions(+), 88 deletions(-) create mode 100644 Documentation/for-each-ref-options.adoc diff --git a/Documentation/for-each-ref-options.adoc b/Documentation/for-each-ref-options.adoc new file mode 100644 index 00000000000..4a033d3e161 --- /dev/null +++ b/Documentation/for-each-ref-options.adoc @@ -0,0 +1,88 @@ +...:: + If one or more patterns are given, only refs are shown that + match against at least one pattern, either using fnmatch(3) or + literally, in the latter case matching completely or from the + beginning up to a slash. + +--stdin:: + If `--stdin` is supplied, then the list of patterns is read from + standard input instead of from the argument list. + +--count=:: + By default the command shows all refs that match + ``. This option makes it stop after showing + that many refs. + +--sort=:: + A field name to sort on. Prefix `-` to sort in + descending order of the value. When unspecified, + `refname` is used. You may use the --sort= option + multiple times, in which case the last key becomes the primary + key. + +--format=:: + A string that interpolates `%(fieldname)` from a ref being shown and + the object it points at. In addition, the string literal `%%` + renders as `%` and `%xx` - where `xx` are hex digits - renders as + the character with hex code `xx`. For example, `%00` interpolates to + `\0` (NUL), `%09` to `\t` (TAB), and `%0a` to `\n` (LF). ++ +When unspecified, `` defaults to `%(objectname) SPC %(objecttype) +TAB %(refname)`. + +--color[=]:: + Respect any colors specified in the `--format` option. The + `` field must be one of `always`, `never`, or `auto` (if + `` is absent, behave as if `always` was given). + +--shell:: +--perl:: +--python:: +--tcl:: + If given, strings that substitute `%(fieldname)` + placeholders are quoted as string literals suitable for + the specified host language. This is meant to produce + a scriptlet that can directly be `eval`ed. + +--points-at=:: + Only list refs which points at the given object. + +--merged[=]:: + Only list refs whose tips are reachable from the + specified commit (HEAD if not specified). + +--no-merged[=]:: + Only list refs whose tips are not reachable from the + specified commit (HEAD if not specified). + +--contains[=]:: + Only list refs which contain the specified commit (HEAD if not + specified). + +--no-contains[=]:: + Only list refs which don't contain the specified commit (HEAD + if not specified). + +--ignore-case:: + Sorting and filtering refs are case insensitive. + +--omit-empty:: + Do not print a newline after formatted refs where the format expands + to the empty string. + +--exclude=:: + If one or more patterns are given, only refs which do not match + any excluded pattern(s) are shown. Matching is done using the + same rules as `` above. + +--include-root-refs:: + List root refs (HEAD and pseudorefs) apart from regular refs. + +--start-after=:: + Allows paginating the output by skipping references up to and including the + specified marker. When paging, it should be noted that references may be + deleted, modified or added between invocations. Output will only yield those + references which follow the marker lexicographically. Output begins from the + first reference that would come after the marker alphabetically. Cannot be + used with `--sort=` or `--stdin` options, or the __ argument(s) + to limit the refs. diff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc index 060940904da..130d452de0e 100644 --- a/Documentation/git-for-each-ref.adoc +++ b/Documentation/git-for-each-ref.adoc @@ -28,94 +28,7 @@ host language allowing their direct evaluation in that language. OPTIONS ------- -...:: - If one or more patterns are given, only refs are shown that - match against at least one pattern, either using fnmatch(3) or - literally, in the latter case matching completely or from the - beginning up to a slash. - ---stdin:: - If `--stdin` is supplied, then the list of patterns is read from - standard input instead of from the argument list. - ---count=:: - By default the command shows all refs that match - ``. This option makes it stop after showing - that many refs. - ---sort=:: - A field name to sort on. Prefix `-` to sort in - descending order of the value. When unspecified, - `refname` is used. You may use the --sort= option - multiple times, in which case the last key becomes the primary - key. - ---format=:: - A string that interpolates `%(fieldname)` from a ref being shown and - the object it points at. In addition, the string literal `%%` - renders as `%` and `%xx` - where `xx` are hex digits - renders as - the character with hex code `xx`. For example, `%00` interpolates to - `\0` (NUL), `%09` to `\t` (TAB), and `%0a` to `\n` (LF). -+ -When unspecified, `` defaults to `%(objectname) SPC %(objecttype) -TAB %(refname)`. - ---color[=]:: - Respect any colors specified in the `--format` option. The - `` field must be one of `always`, `never`, or `auto` (if - `` is absent, behave as if `always` was given). - ---shell:: ---perl:: ---python:: ---tcl:: - If given, strings that substitute `%(fieldname)` - placeholders are quoted as string literals suitable for - the specified host language. This is meant to produce - a scriptlet that can directly be `eval`ed. - ---points-at=:: - Only list refs which points at the given object. - ---merged[=]:: - Only list refs whose tips are reachable from the - specified commit (HEAD if not specified). - ---no-merged[=]:: - Only list refs whose tips are not reachable from the - specified commit (HEAD if not specified). - ---contains[=]:: - Only list refs which contain the specified commit (HEAD if not - specified). - ---no-contains[=]:: - Only list refs which don't contain the specified commit (HEAD - if not specified). - ---ignore-case:: - Sorting and filtering refs are case insensitive. - ---omit-empty:: - Do not print a newline after formatted refs where the format expands - to the empty string. - ---exclude=:: - If one or more patterns are given, only refs which do not match - any excluded pattern(s) are shown. Matching is done using the - same rules as `` above. - ---include-root-refs:: - List root refs (HEAD and pseudorefs) apart from regular refs. - ---start-after=:: - Allows paginating the output by skipping references up to and including the - specified marker. When paging, it should be noted that references may be - deleted, modified or added between invocations. Output will only yield those - references which follow the marker lexicographically. Output begins from the - first reference that would come after the marker alphabetically. Cannot be - used with `--sort=` or `--stdin` options, or the __ argument(s) - to limit the refs. +include::for-each-ref-options.adoc[] FIELD NAMES ----------- -- GitLab From 69c207dc45c0e95bff2bdcaf1c7aca41e9679fb8 Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:54 +0530 Subject: [PATCH 024/371] builtin/for-each-ref: align usage string with the man page Usage string for `git for-each-ref` was out of sync with its official documentation. The test `t0450-txt-doc-vs-help.sh` was marked as broken due to this. Update the usage string to match the documentation. This allows the test to pass, so remove the corresponding 'known breakage' marker from the test file. Mentored-by: Patrick Steinhardt Mentored-by: shejialuo Mentored-by: Karthik Nayak Signed-off-by: Meet Soni Signed-off-by: Junio C Hamano --- builtin/for-each-ref.c | 15 ++++++++++----- t/t0450/adoc-help-mismatches | 1 - 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/builtin/for-each-ref.c b/builtin/for-each-ref.c index 8b5fe7b65e0..fe62f078617 100644 --- a/builtin/for-each-ref.c +++ b/builtin/for-each-ref.c @@ -9,12 +9,17 @@ #include "strbuf.h" #include "strvec.h" +#define COMMON_USAGE_FOR_EACH_REF \ + "[--count=] [--shell|--perl|--python|--tcl]\n" \ + " [(--sort=)...] [--format=]\n" \ + " [--include-root-refs] [--points-at=]\n" \ + " [--merged[=]] [--no-merged[=]]\n" \ + " [--contains[=]] [--no-contains[=]]\n" \ + " [(--exclude=)...] [--start-after=]\n" \ + " [ --stdin | ... ]" + static char const * const for_each_ref_usage[] = { - N_("git for-each-ref [] []"), - N_("git for-each-ref [--points-at ]"), - N_("git for-each-ref [--merged []] [--no-merged []]"), - N_("git for-each-ref [--contains []] [--no-contains []]"), - N_("git for-each-ref [--start-after ]"), + "git for-each-ref " COMMON_USAGE_FOR_EACH_REF, NULL }; diff --git a/t/t0450/adoc-help-mismatches b/t/t0450/adoc-help-mismatches index 06b469bdee2..2c6ecd5fc8e 100644 --- a/t/t0450/adoc-help-mismatches +++ b/t/t0450/adoc-help-mismatches @@ -17,7 +17,6 @@ fast-export fast-import fetch-pack fmt-merge-msg -for-each-ref format-patch fsck-objects fsmonitor--daemon -- GitLab From 6eeb1c070a8746734d74064905a8edeae08bd2a8 Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:55 +0530 Subject: [PATCH 025/371] builtin/for-each-ref: factor out core logic into a helper The implementation of `git for-each-ref` is monolithic within `cmd_for_each_ref()`, making it impossible to share its logic with other commands. To enable code reuse for the upcoming `git refs list` subcommand, refactor the core logic into a shared helper function. Introduce a new `for-each-ref.h` header to define the public interface for this shared logic. It contains the declaration for a new helper function, `for_each_ref_core()`, and a macro for the common usage options. Move the option parsing, filtering, and formatting logic from `cmd_for_each_ref()` into a new helper function named `for_each_ref_core()`. This helper is made generic by accepting the command's usage string as a parameter. The original `cmd_for_each_ref()` is simplified to a thin wrapper that is only responsible for defining its specific usage array and calling the shared helper. Mentored-by: Patrick Steinhardt Mentored-by: shejialuo Mentored-by: Karthik Nayak Signed-off-by: Meet Soni Signed-off-by: Junio C Hamano --- builtin/for-each-ref.c | 41 +++++++++++++++++++---------------------- for-each-ref.h | 26 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 22 deletions(-) create mode 100644 for-each-ref.h diff --git a/builtin/for-each-ref.c b/builtin/for-each-ref.c index fe62f078617..4af33de5767 100644 --- a/builtin/for-each-ref.c +++ b/builtin/for-each-ref.c @@ -2,6 +2,7 @@ #include "commit.h" #include "config.h" #include "environment.h" +#include "for-each-ref.h" #include "gettext.h" #include "object.h" #include "parse-options.h" @@ -9,24 +10,7 @@ #include "strbuf.h" #include "strvec.h" -#define COMMON_USAGE_FOR_EACH_REF \ - "[--count=] [--shell|--perl|--python|--tcl]\n" \ - " [(--sort=)...] [--format=]\n" \ - " [--include-root-refs] [--points-at=]\n" \ - " [--merged[=]] [--no-merged[=]]\n" \ - " [--contains[=]] [--no-contains[=]]\n" \ - " [(--exclude=)...] [--start-after=]\n" \ - " [ --stdin | ... ]" - -static char const * const for_each_ref_usage[] = { - "git for-each-ref " COMMON_USAGE_FOR_EACH_REF, - NULL -}; - -int cmd_for_each_ref(int argc, - const char **argv, - const char *prefix, - struct repository *repo) +int for_each_ref_core(int argc, const char **argv, const char *prefix, struct repository *repo, const char *const *usage) { struct ref_sorting *sorting; struct string_list sorting_options = STRING_LIST_INIT_DUP; @@ -75,17 +59,17 @@ int cmd_for_each_ref(int argc, /* Set default (refname) sorting */ string_list_append(&sorting_options, "refname"); - parse_options(argc, argv, prefix, opts, for_each_ref_usage, 0); + parse_options(argc, argv, prefix, opts, usage, 0); if (format.array_opts.max_count < 0) { error("invalid --count argument: `%d'", format.array_opts.max_count); - usage_with_options(for_each_ref_usage, opts); + usage_with_options(usage, opts); } if (HAS_MULTI_BITS(format.quote_style)) { error("more than one quoting style?"); - usage_with_options(for_each_ref_usage, opts); + usage_with_options(usage, opts); } if (verify_ref_format(&format)) - usage_with_options(for_each_ref_usage, opts); + usage_with_options(usage, opts); if (filter.start_after && sorting_options.nr > 1) die(_("cannot use --start-after with custom sort options")); @@ -125,3 +109,16 @@ int cmd_for_each_ref(int argc, strvec_clear(&vec); return 0; } + +int cmd_for_each_ref(int argc, + const char **argv, + const char *prefix, + struct repository *repo) +{ + static char const * const for_each_ref_usage[] = { + N_("git for-each-ref " COMMON_USAGE_FOR_EACH_REF), + NULL + }; + + return for_each_ref_core(argc, argv, prefix, repo, for_each_ref_usage); +} diff --git a/for-each-ref.h b/for-each-ref.h new file mode 100644 index 00000000000..a5e0b6d17aa --- /dev/null +++ b/for-each-ref.h @@ -0,0 +1,26 @@ +#ifndef FOR_EACH_REF_H +#define FOR_EACH_REF_H + +struct repository; + +/* + * Shared usage string for options common to git-for-each-ref(1) + * and git-refs-list(1). The command-specific part (e.g., "git refs list ") + * must be prepended by the caller. + */ +#define COMMON_USAGE_FOR_EACH_REF \ + "[--count=] [--shell|--perl|--python|--tcl]\n" \ + " [(--sort=)...] [--format=]\n" \ + " [--include-root-refs] [--points-at=]\n" \ + " [--merged[=]] [--no-merged[=]]\n" \ + " [--contains[=]] [--no-contains[=]]\n" \ + " [(--exclude=)...] [--start-after=]\n" \ + " [ --stdin | ... ]" + +/* + * The core logic for for-each-ref and its clones. + */ +int for_each_ref_core(int argc, const char **argv, const char *prefix, + struct repository *repo, const char *const *usage); + +#endif /* FOR_EACH_REF_H */ -- GitLab From eecccfe98bb023a79f3c2b8bc415b6d656d0d381 Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:56 +0530 Subject: [PATCH 026/371] builtin/refs: add list subcommand Git's reference management is distributed across multiple commands. As part of an ongoing effort to consolidate and modernize reference handling, introduce a `list` subcommand under the `git refs` umbrella as a replacement for `git for-each-ref`. Implement `cmd_refs_list` by having it call the `for_each_ref_core()` helper function. This helper was factored out of the original `cmd_for_each_ref` in a preceding commit, allowing both commands to share the same core logic as independent peers. Add documentation for the new command. The man page leverages the shared options file, created in a previous commit, by using the AsciiDoc `include::` macro to ensure consistency with git-for-each-ref(1). Mentored-by: Patrick Steinhardt Mentored-by: shejialuo Mentored-by: Karthik Nayak Signed-off-by: Meet Soni Signed-off-by: Junio C Hamano --- Documentation/git-refs.adoc | 16 ++++++++++++++++ builtin/refs.c | 14 ++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc index 4d6dc994f92..e6089807118 100644 --- a/Documentation/git-refs.adoc +++ b/Documentation/git-refs.adoc @@ -11,6 +11,13 @@ SYNOPSIS [synopsis] git refs migrate --ref-format= [--no-reflog] [--dry-run] git refs verify [--strict] [--verbose] +git refs list [--count=] [--shell|--perl|--python|--tcl] + [(--sort=)...] [--format=] + [--include-root-refs] [--points-at=] + [--merged[=]] [--no-merged[=]] + [--contains[=]] [--no-contains[=]] + [(--exclude=)...] [--start-after=] + [ --stdin | ... ] DESCRIPTION ----------- @@ -26,6 +33,11 @@ migrate:: verify:: Verify reference database consistency. +list:: + List references in the repository with support for filtering, + formatting, and sorting. This subcommand is an alias for + linkgit:git-for-each-ref[1] and offers identical functionality. + OPTIONS ------- @@ -57,6 +69,10 @@ The following options are specific to 'git refs verify': --verbose:: When verifying the reference database consistency, be chatty. +The following options are specific to 'git refs list': + +include::for-each-ref-options.adoc[] + KNOWN LIMITATIONS ----------------- diff --git a/builtin/refs.c b/builtin/refs.c index c7ad0a2963a..76224feba4d 100644 --- a/builtin/refs.c +++ b/builtin/refs.c @@ -6,6 +6,7 @@ #include "refs.h" #include "strbuf.h" #include "worktree.h" +#include "for-each-ref.h" #define REFS_MIGRATE_USAGE \ N_("git refs migrate --ref-format= [--no-reflog] [--dry-run]") @@ -101,6 +102,17 @@ static int cmd_refs_verify(int argc, const char **argv, const char *prefix, return ret; } +static int cmd_refs_list(int argc, const char **argv, const char *prefix, + struct repository *repo) +{ + static char const * const refs_list_usage[] = { + N_("git refs list " COMMON_USAGE_FOR_EACH_REF), + NULL + }; + + return for_each_ref_core(argc, argv, prefix, repo, refs_list_usage); +} + int cmd_refs(int argc, const char **argv, const char *prefix, @@ -109,12 +121,14 @@ int cmd_refs(int argc, const char * const refs_usage[] = { REFS_MIGRATE_USAGE, REFS_VERIFY_USAGE, + "git refs list " COMMON_USAGE_FOR_EACH_REF, NULL, }; parse_opt_subcommand_fn *fn = NULL; struct option opts[] = { OPT_SUBCOMMAND("migrate", &fn, cmd_refs_migrate), OPT_SUBCOMMAND("verify", &fn, cmd_refs_verify), + OPT_SUBCOMMAND("list", &fn, cmd_refs_list), OPT_END(), }; -- GitLab From aa91c5c57013bdeca7b58ee5044bf667a4757978 Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:57 +0530 Subject: [PATCH 027/371] t6300: refactor tests to be shareable In preparation for adding tests for the new `git refs list` command, refactor the existing t6300 test suite to make its logic shareable. Move the core test logic from `t6300-for-each-ref.sh` into a new `for-each-ref-tests.sh` file. Inside this new script, replace hardcoded calls to "git for-each-ref" with the `$git_for_each_ref` variable. The original `t6300-for-each-ref.sh` script now becomes a simple "driver". It is responsible for setting the default value of the variable and then sourcing the test library. This new structure follows the established pattern used for sharing tests between `git-blame` and `git-annotate` and prepares the test suite for the `refs list` tests to be added in a subsequent commit. Mentored-by: Patrick Steinhardt Mentored-by: shejialuo Mentored-by: Karthik Nayak Signed-off-by: Meet Soni Signed-off-by: Junio C Hamano --- t/for-each-ref-tests.sh | 2141 +++++++++++++++++++++++++++++++++++++++ t/t6300-for-each-ref.sh | 2140 +------------------------------------- 2 files changed, 2143 insertions(+), 2138 deletions(-) create mode 100644 t/for-each-ref-tests.sh diff --git a/t/for-each-ref-tests.sh b/t/for-each-ref-tests.sh new file mode 100644 index 00000000000..e3ad19298ac --- /dev/null +++ b/t/for-each-ref-tests.sh @@ -0,0 +1,2141 @@ +git_for_each_ref=${git_for_each_ref:-git for-each-ref} +GNUPGHOME_NOT_USED=$GNUPGHOME +. "$TEST_DIRECTORY"/lib-gpg.sh +. "$TEST_DIRECTORY"/lib-terminal.sh + +# Mon Jul 3 23:18:43 2006 +0000 +datestamp=1151968723 +setdate_and_increment () { + GIT_COMMITTER_DATE="$datestamp +0200" + datestamp=$(expr "$datestamp" + 1) + GIT_AUTHOR_DATE="$datestamp +0200" + datestamp=$(expr "$datestamp" + 1) + export GIT_COMMITTER_DATE GIT_AUTHOR_DATE +} + +test_object_file_size () { + oid=$(git rev-parse "$1") + path=".git/objects/$(test_oid_to_path $oid)" + test_file_size "$path" +} + +test_expect_success setup ' + # setup .mailmap + cat >.mailmap <<-EOF && + A Thor A U Thor + C Mitter C O Mitter + EOF + + setdate_and_increment && + echo "Using $datestamp" > one && + git add one && + git commit -m "Initial" && + git branch -M main && + setdate_and_increment && + git tag -a -m "Tagging at $datestamp" testtag && + git update-ref refs/remotes/origin/main main && + git remote add origin nowhere && + git config branch.main.remote origin && + git config branch.main.merge refs/heads/main && + git remote add myfork elsewhere && + git config remote.pushdefault myfork && + git config push.default current +' + +test_atom () { + case "$1" in + head) ref=refs/heads/main ;; + tag) ref=refs/tags/testtag ;; + sym) ref=refs/heads/sym ;; + *) ref=$1 ;; + esac + format=$2 + test_do=test_expect_${4:-success} + + printf '%s\n' "$3" >expected + $test_do $PREREQ "basic atom: $ref $format" ' + ${git_for_each_ref} --format="%($format)" "$ref" >actual && + sanitize_pgp actual.clean && + test_cmp expected actual.clean + ' + + # Automatically test "contents:size" atom after testing "contents" + if test "$format" = "contents" + then + # for commit leg, $3 is changed there + expect=$(printf '%s' "$3" | wc -c) + $test_do $PREREQ "basic atom: $ref contents:size" ' + type=$(git cat-file -t "$ref") && + case $type in + tag) + # We cannot use $3 as it expects sanitize_pgp to run + git cat-file tag $ref >out && + expect=$(tail -n +6 out | wc -c) && + rm -f out ;; + tree | blob) + expect="" ;; + commit) + : "use the calculated expect" ;; + *) + BUG "unknown object type" ;; + esac && + # Leave $expect unquoted to lose possible leading whitespaces + echo $expect >expected && + ${git_for_each_ref} --format="%(contents:size)" "$ref" >actual && + test_cmp expected actual + ' + fi +} + +hexlen=$(test_oid hexsz) + +test_atom head refname refs/heads/main +test_atom head refname: refs/heads/main +test_atom head refname:short main +test_atom head refname:lstrip=1 heads/main +test_atom head refname:lstrip=2 main +test_atom head refname:lstrip=-1 main +test_atom head refname:lstrip=-2 heads/main +test_atom head refname:rstrip=1 refs/heads +test_atom head refname:rstrip=2 refs +test_atom head refname:rstrip=-1 refs +test_atom head refname:rstrip=-2 refs/heads +test_atom head refname:strip=1 heads/main +test_atom head refname:strip=2 main +test_atom head refname:strip=-1 main +test_atom head refname:strip=-2 heads/main +test_atom head upstream refs/remotes/origin/main +test_atom head upstream:short origin/main +test_atom head upstream:lstrip=2 origin/main +test_atom head upstream:lstrip=-2 origin/main +test_atom head upstream:rstrip=2 refs/remotes +test_atom head upstream:rstrip=-2 refs/remotes +test_atom head upstream:strip=2 origin/main +test_atom head upstream:strip=-2 origin/main +test_atom head push refs/remotes/myfork/main +test_atom head push:short myfork/main +test_atom head push:lstrip=1 remotes/myfork/main +test_atom head push:lstrip=-1 main +test_atom head push:rstrip=1 refs/remotes/myfork +test_atom head push:rstrip=-1 refs +test_atom head push:strip=1 remotes/myfork/main +test_atom head push:strip=-1 main +test_atom head objecttype commit +test_atom head objectsize $((131 + hexlen)) +test_atom head objectsize:disk $(test_object_file_size refs/heads/main) +test_atom head deltabase $ZERO_OID +test_atom head objectname $(git rev-parse refs/heads/main) +test_atom head objectname:short $(git rev-parse --short refs/heads/main) +test_atom head objectname:short=1 $(git rev-parse --short=1 refs/heads/main) +test_atom head objectname:short=10 $(git rev-parse --short=10 refs/heads/main) +test_atom head tree $(git rev-parse refs/heads/main^{tree}) +test_atom head tree:short $(git rev-parse --short refs/heads/main^{tree}) +test_atom head tree:short=1 $(git rev-parse --short=1 refs/heads/main^{tree}) +test_atom head tree:short=10 $(git rev-parse --short=10 refs/heads/main^{tree}) +test_atom head parent '' +test_atom head parent:short '' +test_atom head parent:short=1 '' +test_atom head parent:short=10 '' +test_atom head numparent 0 +test_atom head object '' +test_atom head type '' +test_atom head raw "$(git cat-file commit refs/heads/main) +" +test_atom head '*objectname' '' +test_atom head '*objecttype' '' +test_atom head author 'A U Thor 1151968724 +0200' +test_atom head authorname 'A U Thor' +test_atom head authorname:mailmap 'A Thor' +test_atom head authoremail '' +test_atom head authoremail:trim 'author@example.com' +test_atom head authoremail:localpart 'author' +test_atom head authoremail:trim,localpart 'author' +test_atom head authoremail:mailmap '' +test_atom head authoremail:mailmap,trim 'athor@example.com' +test_atom head authoremail:trim,mailmap 'athor@example.com' +test_atom head authoremail:mailmap,localpart 'athor' +test_atom head authoremail:localpart,mailmap 'athor' +test_atom head authoremail:mailmap,trim,localpart,mailmap,trim 'athor' +test_atom head authordate 'Tue Jul 4 01:18:44 2006 +0200' +test_atom head committer 'C O Mitter 1151968723 +0200' +test_atom head committername 'C O Mitter' +test_atom head committername:mailmap 'C Mitter' +test_atom head committeremail '' +test_atom head committeremail:trim 'committer@example.com' +test_atom head committeremail:localpart 'committer' +test_atom head committeremail:localpart,trim 'committer' +test_atom head committeremail:mailmap '' +test_atom head committeremail:mailmap,trim 'cmitter@example.com' +test_atom head committeremail:trim,mailmap 'cmitter@example.com' +test_atom head committeremail:mailmap,localpart 'cmitter' +test_atom head committeremail:localpart,mailmap 'cmitter' +test_atom head committeremail:trim,mailmap,trim,trim,localpart 'cmitter' +test_atom head committerdate 'Tue Jul 4 01:18:43 2006 +0200' +test_atom head tag '' +test_atom head tagger '' +test_atom head taggername '' +test_atom head taggeremail '' +test_atom head taggeremail:trim '' +test_atom head taggeremail:localpart '' +test_atom head taggerdate '' +test_atom head creator 'C O Mitter 1151968723 +0200' +test_atom head creatordate 'Tue Jul 4 01:18:43 2006 +0200' +test_atom head subject 'Initial' +test_atom head subject:sanitize 'Initial' +test_atom head contents:subject 'Initial' +test_atom head body '' +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 refname:short testtag +test_atom tag upstream '' +test_atom tag push '' +test_atom tag objecttype tag +test_atom tag objectsize $((114 + hexlen)) +test_atom tag objectsize:disk $(test_object_file_size refs/tags/testtag) +test_atom tag '*objectsize:disk' $(test_object_file_size refs/heads/main) +test_atom tag deltabase $ZERO_OID +test_atom tag '*deltabase' $ZERO_OID +test_atom tag objectname $(git rev-parse refs/tags/testtag) +test_atom tag objectname:short $(git rev-parse --short refs/tags/testtag) +test_atom head objectname:short=1 $(git rev-parse --short=1 refs/heads/main) +test_atom head objectname:short=10 $(git rev-parse --short=10 refs/heads/main) +test_atom tag tree '' +test_atom tag tree:short '' +test_atom tag tree:short=1 '' +test_atom tag tree:short=10 '' +test_atom tag parent '' +test_atom tag parent:short '' +test_atom tag parent:short=1 '' +test_atom tag parent:short=10 '' +test_atom tag numparent '' +test_atom tag object $(git rev-parse refs/tags/testtag^0) +test_atom tag type 'commit' +test_atom tag '*objectname' $(git rev-parse refs/tags/testtag^{}) +test_atom tag '*objecttype' 'commit' +test_atom tag author '' +test_atom tag authorname '' +test_atom tag authorname:mailmap '' +test_atom tag authoremail '' +test_atom tag authoremail:trim '' +test_atom tag authoremail:localpart '' +test_atom tag authoremail:trim,localpart '' +test_atom tag authoremail:mailmap '' +test_atom tag authoremail:mailmap,trim '' +test_atom tag authoremail:trim,mailmap '' +test_atom tag authoremail:mailmap,localpart '' +test_atom tag authoremail:localpart,mailmap '' +test_atom tag authoremail:mailmap,trim,localpart,mailmap,trim '' +test_atom tag authordate '' +test_atom tag committer '' +test_atom tag committername '' +test_atom tag committername:mailmap '' +test_atom tag committeremail '' +test_atom tag committeremail:trim '' +test_atom tag committeremail:localpart '' +test_atom tag committeremail:localpart,trim '' +test_atom tag committeremail:mailmap '' +test_atom tag committeremail:mailmap,trim '' +test_atom tag committeremail:trim,mailmap '' +test_atom tag committeremail:mailmap,localpart '' +test_atom tag committeremail:localpart,mailmap '' +test_atom tag committeremail:trim,mailmap,trim,trim,localpart '' +test_atom tag committerdate '' +test_atom tag tag 'testtag' +test_atom tag tagger 'C O Mitter 1151968725 +0200' +test_atom tag taggername 'C O Mitter' +test_atom tag taggername:mailmap 'C Mitter' +test_atom tag taggeremail '' +test_atom tag taggeremail:trim 'committer@example.com' +test_atom tag taggeremail:localpart 'committer' +test_atom tag taggeremail:trim,localpart 'committer' +test_atom tag taggeremail:mailmap '' +test_atom tag taggeremail:mailmap,trim 'cmitter@example.com' +test_atom tag taggeremail:trim,mailmap 'cmitter@example.com' +test_atom tag taggeremail:mailmap,localpart 'cmitter' +test_atom tag taggeremail:localpart,mailmap 'cmitter' +test_atom tag taggeremail:trim,mailmap,trim,localpart,localpart 'cmitter' +test_atom tag taggerdate 'Tue Jul 4 01:18:45 2006 +0200' +test_atom tag creator 'C O Mitter 1151968725 +0200' +test_atom tag creatordate 'Tue Jul 4 01:18:45 2006 +0200' +test_atom tag subject 'Tagging at 1151968727' +test_atom tag subject:sanitize 'Tagging-at-1151968727' +test_atom tag contents:subject 'Tagging at 1151968727' +test_atom tag body '' +test_atom tag contents:body '' +test_atom tag contents:signature '' +test_atom tag contents 'Tagging at 1151968727 +' +test_atom tag HEAD ' ' + +test_expect_success 'basic atom: refs/tags/testtag *raw' ' + git cat-file commit refs/tags/testtag^{} >expected && + ${git_for_each_ref} --format="%(*raw)" refs/tags/testtag >actual && + sanitize_pgp expected.clean && + echo >>expected.clean && + sanitize_pgp actual.clean && + test_cmp expected.clean actual.clean +' + +test_expect_success 'Check invalid atoms names are errors' ' + test_must_fail ${git_for_each_ref} --format="%(INVALID)" refs/heads +' + +test_expect_success 'Check format specifiers are ignored in naming date atoms' ' + ${git_for_each_ref} --format="%(authordate)" refs/heads && + ${git_for_each_ref} --format="%(authordate:default) %(authordate)" refs/heads && + ${git_for_each_ref} --format="%(authordate) %(authordate:default)" refs/heads && + ${git_for_each_ref} --format="%(authordate:default) %(authordate:default)" refs/heads +' + +test_expect_success 'Check valid format specifiers for date fields' ' + ${git_for_each_ref} --format="%(authordate:default)" refs/heads && + ${git_for_each_ref} --format="%(authordate:relative)" refs/heads && + ${git_for_each_ref} --format="%(authordate:short)" refs/heads && + ${git_for_each_ref} --format="%(authordate:local)" refs/heads && + ${git_for_each_ref} --format="%(authordate:iso8601)" refs/heads && + ${git_for_each_ref} --format="%(authordate:rfc2822)" refs/heads +' + +test_expect_success 'Check invalid format specifiers are errors' ' + test_must_fail ${git_for_each_ref} --format="%(authordate:INVALID)" refs/heads +' + +test_expect_success 'arguments to %(objectname:short=) must be positive integers' ' + test_must_fail ${git_for_each_ref} --format="%(objectname:short=0)" && + test_must_fail ${git_for_each_ref} --format="%(objectname:short=-1)" && + test_must_fail ${git_for_each_ref} --format="%(objectname:short=foo)" +' + +test_bad_atom () { + case "$1" in + head) ref=refs/heads/main ;; + tag) ref=refs/tags/testtag ;; + sym) ref=refs/heads/sym ;; + *) ref=$1 ;; + esac + format=$2 + test_do=test_expect_${4:-success} + + printf '%s\n' "$3" >expect + $test_do $PREREQ "err basic atom: $ref $format" ' + test_must_fail ${git_for_each_ref} \ + --format="%($format)" "$ref" 2>error && + test_cmp expect error + ' +} + +test_bad_atom head 'authoremail:foo' \ + 'fatal: unrecognized %(authoremail) argument: foo' + +test_bad_atom head 'authoremail:mailmap,trim,bar' \ + 'fatal: unrecognized %(authoremail) argument: bar' + +test_bad_atom head 'authoremail:trim,' \ + 'fatal: unrecognized %(authoremail) argument: ' + +test_bad_atom head 'authoremail:mailmaptrim' \ + 'fatal: unrecognized %(authoremail) argument: trim' + +test_bad_atom head 'committeremail: ' \ + 'fatal: unrecognized %(committeremail) argument: ' + +test_bad_atom head 'committeremail: trim,foo' \ + 'fatal: unrecognized %(committeremail) argument: trim,foo' + +test_bad_atom head 'committeremail:mailmap,localpart ' \ + 'fatal: unrecognized %(committeremail) argument: ' + +test_bad_atom head 'committeremail:trim_localpart' \ + 'fatal: unrecognized %(committeremail) argument: _localpart' + +test_bad_atom head 'committeremail:localpart,,,trim' \ + 'fatal: unrecognized %(committeremail) argument: ,,trim' + +test_bad_atom tag 'taggeremail:mailmap,trim, foo ' \ + 'fatal: unrecognized %(taggeremail) argument: foo ' + +test_bad_atom tag 'taggeremail:trim,localpart,' \ + 'fatal: unrecognized %(taggeremail) argument: ' + +test_bad_atom tag 'taggeremail:mailmap;localpart trim' \ + 'fatal: unrecognized %(taggeremail) argument: ;localpart trim' + +test_bad_atom tag 'taggeremail:localpart trim' \ + 'fatal: unrecognized %(taggeremail) argument: trim' + +test_bad_atom tag 'taggeremail:mailmap,mailmap,trim,qux,localpart,trim' \ + 'fatal: unrecognized %(taggeremail) argument: qux,localpart,trim' + +test_date () { + f=$1 && + committer_date=$2 && + author_date=$3 && + tagger_date=$4 && + cat >expected <<-EOF && + 'refs/heads/main' '$committer_date' '$author_date' + 'refs/tags/testtag' '$tagger_date' + EOF + ( + ${git_for_each_ref} --shell \ + --format="%(refname) %(committerdate${f:+:$f}) %(authordate${f:+:$f})" \ + refs/heads && + ${git_for_each_ref} --shell \ + --format="%(refname) %(taggerdate${f:+:$f})" \ + refs/tags + ) >actual && + test_cmp expected actual +} + +test_expect_success 'Check unformatted date fields output' ' + test_date "" \ + "Tue Jul 4 01:18:43 2006 +0200" \ + "Tue Jul 4 01:18:44 2006 +0200" \ + "Tue Jul 4 01:18:45 2006 +0200" +' + +test_expect_success 'Check format "default" formatted date fields output' ' + test_date default \ + "Tue Jul 4 01:18:43 2006 +0200" \ + "Tue Jul 4 01:18:44 2006 +0200" \ + "Tue Jul 4 01:18:45 2006 +0200" +' + +test_expect_success 'Check format "default-local" date fields output' ' + test_date default-local "Mon Jul 3 23:18:43 2006" "Mon Jul 3 23:18:44 2006" "Mon Jul 3 23:18:45 2006" +' + +# Don't know how to do relative check because I can't know when this script +# is going to be run and can't fake the current time to git, and hence can't +# provide expected output. Instead, I'll just make sure that "relative" +# doesn't exit in error +test_expect_success 'Check format "relative" date fields output' ' + f=relative && + (${git_for_each_ref} --shell --format="%(refname) %(committerdate:$f) %(authordate:$f)" refs/heads && + ${git_for_each_ref} --shell --format="%(refname) %(taggerdate:$f)" refs/tags) >actual +' + +# We just check that this is the same as "relative" for now. +test_expect_success 'Check format "relative-local" date fields output' ' + test_date relative-local \ + "$(${git_for_each_ref} --format="%(committerdate:relative)" refs/heads)" \ + "$(${git_for_each_ref} --format="%(authordate:relative)" refs/heads)" \ + "$(${git_for_each_ref} --format="%(taggerdate:relative)" refs/tags)" +' + +test_expect_success 'Check format "short" date fields output' ' + test_date short 2006-07-04 2006-07-04 2006-07-04 +' + +test_expect_success 'Check format "short-local" date fields output' ' + test_date short-local 2006-07-03 2006-07-03 2006-07-03 +' + +test_expect_success 'Check format "local" date fields output' ' + test_date local \ + "Mon Jul 3 23:18:43 2006" \ + "Mon Jul 3 23:18:44 2006" \ + "Mon Jul 3 23:18:45 2006" +' + +test_expect_success 'Check format "iso8601" date fields output' ' + test_date iso8601 \ + "2006-07-04 01:18:43 +0200" \ + "2006-07-04 01:18:44 +0200" \ + "2006-07-04 01:18:45 +0200" +' + +test_expect_success 'Check format "iso8601-local" date fields output' ' + test_date iso8601-local "2006-07-03 23:18:43 +0000" "2006-07-03 23:18:44 +0000" "2006-07-03 23:18:45 +0000" +' + +test_expect_success 'Check format "rfc2822" date fields output' ' + test_date rfc2822 \ + "Tue, 4 Jul 2006 01:18:43 +0200" \ + "Tue, 4 Jul 2006 01:18:44 +0200" \ + "Tue, 4 Jul 2006 01:18:45 +0200" +' + +test_expect_success 'Check format "rfc2822-local" date fields output' ' + test_date rfc2822-local "Mon, 3 Jul 2006 23:18:43 +0000" "Mon, 3 Jul 2006 23:18:44 +0000" "Mon, 3 Jul 2006 23:18:45 +0000" +' + +test_expect_success 'Check format "raw" date fields output' ' + test_date raw "1151968723 +0200" "1151968724 +0200" "1151968725 +0200" +' + +test_expect_success 'Check format "raw-local" date fields output' ' + test_date raw-local "1151968723 +0000" "1151968724 +0000" "1151968725 +0000" +' + +test_expect_success 'Check format of strftime date fields' ' + echo "my date is 2006-07-04" >expected && + ${git_for_each_ref} \ + --format="%(authordate:format:my date is %Y-%m-%d)" \ + refs/heads >actual && + test_cmp expected actual +' + +test_expect_success 'Check format of strftime-local date fields' ' + echo "my date is 2006-07-03" >expected && + ${git_for_each_ref} \ + --format="%(authordate:format-local:my date is %Y-%m-%d)" \ + refs/heads >actual && + test_cmp expected actual +' + +test_expect_success 'exercise strftime with odd fields' ' + echo >expected && + ${git_for_each_ref} --format="%(authordate:format:)" refs/heads >actual && + test_cmp expected actual && + long="long format -- $ZERO_OID$ZERO_OID$ZERO_OID$ZERO_OID$ZERO_OID$ZERO_OID$ZERO_OID" && + echo $long >expected && + ${git_for_each_ref} --format="%(authordate:format:$long)" refs/heads >actual && + test_cmp expected actual +' + +cat >expected <<\EOF +refs/heads/main +refs/remotes/origin/main +refs/tags/testtag +EOF + +test_expect_success 'Verify ascending sort' ' + ${git_for_each_ref} --format="%(refname)" --sort=refname >actual && + test_cmp expected actual +' + + +cat >expected <<\EOF +refs/tags/testtag +refs/remotes/origin/main +refs/heads/main +EOF + +test_expect_success 'Verify descending sort' ' + ${git_for_each_ref} --format="%(refname)" --sort=-refname >actual && + test_cmp expected actual +' + +test_expect_success 'Give help even with invalid sort atoms' ' + test_expect_code 129 ${git_for_each_ref} --sort=bogus -h >actual 2>&1 && + grep "^usage: ${git_for_each_ref}" actual +' + +cat >expected <<\EOF +refs/tags/testtag +refs/tags/testtag-2 +EOF + +test_expect_success 'exercise patterns with prefixes' ' + git tag testtag-2 && + test_when_finished "git tag -d testtag-2" && + ${git_for_each_ref} --format="%(refname)" \ + refs/tags/testtag refs/tags/testtag-2 >actual && + test_cmp expected actual +' + +cat >expected <<\EOF +refs/tags/testtag +refs/tags/testtag-2 +EOF + +test_expect_success 'exercise glob patterns with prefixes' ' + git tag testtag-2 && + test_when_finished "git tag -d testtag-2" && + ${git_for_each_ref} --format="%(refname)" \ + refs/tags/testtag "refs/tags/testtag-*" >actual && + test_cmp expected actual +' + +cat >expected <<\EOF +refs/tags/bar +refs/tags/baz +refs/tags/testtag +EOF + +test_expect_success 'exercise patterns with prefix exclusions' ' + for tag in foo/one foo/two foo/three bar baz + do + git tag "$tag" || return 1 + done && + test_when_finished "git tag -d foo/one foo/two foo/three bar baz" && + ${git_for_each_ref} --format="%(refname)" \ + refs/tags/ --exclude=refs/tags/foo >actual && + test_cmp expected actual +' + +cat >expected <<\EOF +refs/tags/bar +refs/tags/baz +refs/tags/foo/one +refs/tags/testtag +EOF + +test_expect_success 'exercise patterns with pattern exclusions' ' + for tag in foo/one foo/two foo/three bar baz + do + git tag "$tag" || return 1 + done && + test_when_finished "git tag -d foo/one foo/two foo/three bar baz" && + ${git_for_each_ref} --format="%(refname)" \ + refs/tags/ --exclude="refs/tags/foo/t*" >actual && + test_cmp expected actual +' + +cat >expected <<\EOF +'refs/heads/main' +'refs/remotes/origin/main' +'refs/tags/testtag' +EOF + +test_expect_success 'Quoting style: shell' ' + ${git_for_each_ref} --shell --format="%(refname)" >actual && + test_cmp expected actual +' + +test_expect_success 'Quoting style: perl' ' + ${git_for_each_ref} --perl --format="%(refname)" >actual && + test_cmp expected actual +' + +test_expect_success 'Quoting style: python' ' + ${git_for_each_ref} --python --format="%(refname)" >actual && + test_cmp expected actual +' + +cat >expected <<\EOF +"refs/heads/main" +"refs/remotes/origin/main" +"refs/tags/testtag" +EOF + +test_expect_success 'Quoting style: tcl' ' + ${git_for_each_ref} --tcl --format="%(refname)" >actual && + test_cmp expected actual +' + +for i in "--perl --shell" "-s --python" "--python --tcl" "--tcl --perl"; do + test_expect_success "more than one quoting style: $i" " + test_must_fail ${git_for_each_ref} $i 2>err && + grep '^error: more than one quoting style' err + " +done + +test_expect_success 'setup for upstream:track[short]' ' + test_commit two +' + +test_atom head upstream:track '[ahead 1]' +test_atom head upstream:trackshort '>' +test_atom head upstream:track,nobracket 'ahead 1' +test_atom head upstream:nobracket,track 'ahead 1' + +test_expect_success 'setup for push:track[short]' ' + test_commit third && + git update-ref refs/remotes/myfork/main main && + git reset main~1 +' + +test_atom head push:track '[behind 1]' +test_atom head push:trackshort '<' + +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 +' + +test_expect_success 'Check that :track[short] works when upstream is invalid' ' + cat >expected <<-\EOF && + [gone] + + EOF + test_when_finished "git config branch.main.merge refs/heads/main" && + git config branch.main.merge refs/heads/does-not-exist && + ${git_for_each_ref} \ + --format="%(upstream:track)$LF%(upstream:trackshort)" \ + refs/heads >actual && + test_cmp expected actual +' + +test_expect_success 'Check for invalid refname format' ' + test_must_fail ${git_for_each_ref} --format="%(refname:INVALID)" +' + +test_expect_success 'set up color tests' ' + cat >expected.color <<-EOF && + $(git rev-parse --short refs/heads/main) main + $(git rev-parse --short refs/remotes/myfork/main) myfork/main + $(git rev-parse --short refs/remotes/origin/main) origin/main + $(git rev-parse --short refs/tags/testtag) testtag + $(git rev-parse --short refs/tags/third) third + $(git rev-parse --short refs/tags/two) two + EOF + sed "s/<[^>]*>//g" expected.bare && + color_format="%(objectname:short) %(color:green)%(refname:short)" +' + +test_expect_success TTY '%(color) shows color with a tty' ' + test_terminal ${git_for_each_ref} --format="$color_format" >actual.raw && + test_decode_color actual && + test_cmp expected.color actual +' + +test_expect_success '%(color) does not show color without tty' ' + TERM=vt100 ${git_for_each_ref} --format="$color_format" >actual && + test_cmp expected.bare actual +' + +test_expect_success '--color can override tty check' ' + ${git_for_each_ref} --color --format="$color_format" >actual.raw && + test_decode_color actual && + test_cmp expected.color actual +' + +test_expect_success 'color.ui=always does not override tty check' ' + git -c color.ui=always ${git_for_each_ref#git} --format="$color_format" >actual && + test_cmp expected.bare actual +' + +test_expect_success 'setup for describe atom tests' ' + git init -b master describe-repo && + ( + cd describe-repo && + + test_commit --no-tag one && + git tag tagone && + + test_commit --no-tag two && + git tag -a -m "tag two" tagtwo + ) +' + +test_expect_success 'describe atom vs git describe' ' + ( + cd describe-repo && + + ${git_for_each_ref} --format="%(objectname)" \ + refs/tags/ >obj && + while read hash + do + if desc=$(git describe $hash) + then + : >expect-contains-good + else + : >expect-contains-bad + fi && + echo "$hash $desc" || return 1 + done expect && + test_path_exists expect-contains-good && + test_path_exists expect-contains-bad && + + ${git_for_each_ref} --format="%(objectname) %(describe)" \ + refs/tags/ >actual 2>err && + test_cmp expect actual && + test_must_be_empty err + ) +' + +test_expect_success 'describe:tags vs describe --tags' ' + ( + cd describe-repo && + git describe --tags >expect && + ${git_for_each_ref} --format="%(describe:tags)" \ + refs/heads/master >actual && + test_cmp expect actual + ) +' + +test_expect_success 'describe:abbrev=... vs describe --abbrev=...' ' + ( + cd describe-repo && + + # Case 1: We have commits between HEAD and the most + # recent tag reachable from it + test_commit --no-tag file && + git describe --abbrev=14 >expect && + ${git_for_each_ref} --format="%(describe:abbrev=14)" \ + refs/heads/master >actual && + test_cmp expect actual && + + # Make sure the hash used is at least 14 digits long + sed -e "s/^.*-g\([0-9a-f]*\)$/\1/" hexpart && + test 15 -le $(wc -c expect && + ${git_for_each_ref} --format="%(describe:abbrev=14)" \ + refs/heads/master >actual && + test_cmp expect actual && + test tagname = $(cat actual) + ) +' + +test_expect_success 'describe:match=... vs describe --match ...' ' + ( + cd describe-repo && + git tag -a -m "tag foo" tag-foo && + git describe --match "*-foo" >expect && + ${git_for_each_ref} --format="%(describe:match="*-foo")" \ + refs/heads/master >actual && + test_cmp expect actual + ) +' + +test_expect_success 'describe:exclude:... vs describe --exclude ...' ' + ( + cd describe-repo && + git tag -a -m "tag bar" tag-bar && + git describe --exclude "*-bar" >expect && + ${git_for_each_ref} --format="%(describe:exclude="*-bar")" \ + refs/heads/master >actual && + test_cmp expect actual + ) +' + +test_expect_success 'deref with describe atom' ' + ( + cd describe-repo && + cat >expect <<-\EOF && + + tagname + tagname + tagname + + tagtwo + EOF + ${git_for_each_ref} --format="%(*describe)" >actual && + test_cmp expect actual + ) +' + +test_expect_success 'err on bad describe atom arg' ' + ( + cd describe-repo && + + # The bad arg is the only arg passed to describe atom + cat >expect <<-\EOF && + fatal: unrecognized %(describe) argument: baz + EOF + test_must_fail ${git_for_each_ref} --format="%(describe:baz)" \ + refs/heads/master 2>actual && + test_cmp expect actual && + + # The bad arg is in the middle of the option string + # passed to the describe atom + cat >expect <<-\EOF && + fatal: unrecognized %(describe) argument: qux=1,abbrev=14 + EOF + test_must_fail ${git_for_each_ref} \ + --format="%(describe:tags,qux=1,abbrev=14)" \ + ref/heads/master 2>actual && + test_cmp expect actual + ) +' + +cat >expected <<\EOF +heads/main +tags/main +EOF + +test_expect_success 'Check ambiguous head and tag refs (strict)' ' + git config --bool core.warnambiguousrefs true && + git checkout -b newtag && + echo "Using $datestamp" > one && + git add one && + git commit -m "Branch" && + setdate_and_increment && + git tag -m "Tagging at $datestamp" main && + ${git_for_each_ref} --format "%(refname:short)" refs/heads/main refs/tags/main >actual && + test_cmp expected actual +' + +cat >expected <<\EOF +heads/main +main +EOF + +test_expect_success 'Check ambiguous head and tag refs (loose)' ' + git config --bool core.warnambiguousrefs false && + ${git_for_each_ref} --format "%(refname:short)" refs/heads/main refs/tags/main >actual && + test_cmp expected actual +' + +cat >expected <<\EOF +heads/ambiguous +ambiguous +EOF + +test_expect_success 'Check ambiguous head and tag refs II (loose)' ' + git checkout main && + git tag ambiguous testtag^0 && + git branch ambiguous testtag^0 && + ${git_for_each_ref} --format "%(refname:short)" refs/heads/ambiguous refs/tags/ambiguous >actual && + test_cmp expected actual +' + +test_expect_success 'create tag without tagger' ' + git tag -a -m "Broken tag" taggerless && + git tag -f taggerless $(git cat-file tag taggerless | + sed -e "/^tagger /d" | + git hash-object --literally --stdin -w -t tag) +' + +test_atom refs/tags/taggerless type 'commit' +test_atom refs/tags/taggerless tag 'taggerless' +test_atom refs/tags/taggerless tagger '' +test_atom refs/tags/taggerless taggername '' +test_atom refs/tags/taggerless taggeremail '' +test_atom refs/tags/taggerless taggeremail:trim '' +test_atom refs/tags/taggerless taggeremail:localpart '' +test_atom refs/tags/taggerless taggerdate '' +test_atom refs/tags/taggerless committer '' +test_atom refs/tags/taggerless committername '' +test_atom refs/tags/taggerless committeremail '' +test_atom refs/tags/taggerless committeremail:trim '' +test_atom refs/tags/taggerless committeremail:localpart '' +test_atom refs/tags/taggerless committerdate '' +test_atom refs/tags/taggerless subject 'Broken tag' + +test_expect_success 'an unusual tag with an incomplete line' ' + + git tag -m "bogo" bogo && + bogo=$(git cat-file tag bogo) && + bogo=$(printf "%s" "$bogo" | git mktag) && + git tag -f bogo "$bogo" && + ${git_for_each_ref} --format "%(body)" refs/tags/bogo + +' + +test_expect_success 'create tag with subject and body content' ' + cat >>msg <<-\EOF && + the subject line + + first body line + second body line + EOF + git tag -F msg subject-body +' +test_atom refs/tags/subject-body subject 'the subject line' +test_atom refs/tags/subject-body subject:sanitize 'the-subject-line' +test_atom refs/tags/subject-body body 'first body line +second body line +' +test_atom refs/tags/subject-body contents 'the subject line + +first body line +second body line +' + +test_expect_success 'create tag with multiline subject' ' + cat >msg <<-\EOF && + first subject line + second subject line + + first body line + second body line + EOF + git tag -F msg multiline +' +test_atom refs/tags/multiline subject 'first subject line second subject line' +test_atom refs/tags/multiline subject:sanitize 'first-subject-line-second-subject-line' +test_atom refs/tags/multiline contents:subject 'first subject line second subject line' +test_atom refs/tags/multiline body 'first body line +second body line +' +test_atom refs/tags/multiline contents:body 'first body line +second body line +' +test_atom refs/tags/multiline contents:signature '' +test_atom refs/tags/multiline contents 'first subject line +second subject line + +first body line +second body line +' + +test_expect_success GPG 'create signed tags' ' + git tag -s -m "" signed-empty && + git tag -s -m "subject line" signed-short && + cat >msg <<-\EOF && + subject line + + body contents + EOF + git tag -s -F msg signed-long +' + +sig='-----BEGIN PGP SIGNATURE----- +-----END PGP SIGNATURE----- +' + +PREREQ=GPG +test_atom refs/tags/signed-empty subject '' +test_atom refs/tags/signed-empty subject:sanitize '' +test_atom refs/tags/signed-empty contents:subject '' +test_atom refs/tags/signed-empty body "$sig" +test_atom refs/tags/signed-empty contents:body '' +test_atom refs/tags/signed-empty contents:signature "$sig" +test_atom refs/tags/signed-empty contents "$sig" + +test_expect_success GPG 'basic atom: refs/tags/signed-empty raw' ' + git cat-file tag refs/tags/signed-empty >expected && + ${git_for_each_ref} --format="%(raw)" refs/tags/signed-empty >actual && + sanitize_pgp expected.clean && + echo >>expected.clean && + sanitize_pgp actual.clean && + test_cmp expected.clean actual.clean +' + +test_atom refs/tags/signed-short subject 'subject line' +test_atom refs/tags/signed-short subject:sanitize 'subject-line' +test_atom refs/tags/signed-short contents:subject 'subject line' +test_atom refs/tags/signed-short body "$sig" +test_atom refs/tags/signed-short contents:body '' +test_atom refs/tags/signed-short contents:signature "$sig" +test_atom refs/tags/signed-short contents "subject line +$sig" + +test_expect_success GPG 'basic atom: refs/tags/signed-short raw' ' + git cat-file tag refs/tags/signed-short >expected && + ${git_for_each_ref} --format="%(raw)" refs/tags/signed-short >actual && + sanitize_pgp expected.clean && + echo >>expected.clean && + sanitize_pgp actual.clean && + test_cmp expected.clean actual.clean +' + +test_atom refs/tags/signed-long subject 'subject line' +test_atom refs/tags/signed-long subject:sanitize 'subject-line' +test_atom refs/tags/signed-long contents:subject 'subject line' +test_atom refs/tags/signed-long body "body contents +$sig" +test_atom refs/tags/signed-long contents:body 'body contents +' +test_atom refs/tags/signed-long contents:signature "$sig" +test_atom refs/tags/signed-long contents "subject line + +body contents +$sig" + +test_expect_success GPG 'basic atom: refs/tags/signed-long raw' ' + git cat-file tag refs/tags/signed-long >expected && + ${git_for_each_ref} --format="%(raw)" refs/tags/signed-long >actual && + sanitize_pgp expected.clean && + echo >>expected.clean && + sanitize_pgp actual.clean && + test_cmp expected.clean actual.clean +' + +test_expect_success 'set up refs pointing to tree and blob' ' + git update-ref refs/mytrees/first refs/heads/main^{tree} && + git update-ref refs/myblobs/first refs/heads/main:one +' + +test_atom refs/mytrees/first subject "" +test_atom refs/mytrees/first contents:subject "" +test_atom refs/mytrees/first body "" +test_atom refs/mytrees/first contents:body "" +test_atom refs/mytrees/first contents:signature "" +test_atom refs/mytrees/first contents "" + +test_expect_success 'basic atom: refs/mytrees/first raw' ' + git cat-file tree refs/mytrees/first >expected && + echo >>expected && + ${git_for_each_ref} --format="%(raw)" refs/mytrees/first >actual && + test_cmp expected actual && + git cat-file -s refs/mytrees/first >expected && + ${git_for_each_ref} --format="%(raw:size)" refs/mytrees/first >actual && + test_cmp expected actual +' + +test_atom refs/myblobs/first subject "" +test_atom refs/myblobs/first contents:subject "" +test_atom refs/myblobs/first body "" +test_atom refs/myblobs/first contents:body "" +test_atom refs/myblobs/first contents:signature "" +test_atom refs/myblobs/first contents "" + +test_expect_success 'basic atom: refs/myblobs/first raw' ' + git cat-file blob refs/myblobs/first >expected && + echo >>expected && + ${git_for_each_ref} --format="%(raw)" refs/myblobs/first >actual && + test_cmp expected actual && + git cat-file -s refs/myblobs/first >expected && + ${git_for_each_ref} --format="%(raw:size)" refs/myblobs/first >actual && + test_cmp expected actual +' + +test_expect_success 'set up refs pointing to binary blob' ' + printf "a\0b\0c" >blob1 && + printf "a\0c\0b" >blob2 && + printf "\0a\0b\0c" >blob3 && + printf "abc" >blob4 && + printf "\0 \0 \0 " >blob5 && + printf "\0 \0a\0 " >blob6 && + printf " " >blob7 && + >blob8 && + obj=$(git hash-object -w blob1) && + git update-ref refs/myblobs/blob1 "$obj" && + obj=$(git hash-object -w blob2) && + git update-ref refs/myblobs/blob2 "$obj" && + obj=$(git hash-object -w blob3) && + git update-ref refs/myblobs/blob3 "$obj" && + obj=$(git hash-object -w blob4) && + git update-ref refs/myblobs/blob4 "$obj" && + obj=$(git hash-object -w blob5) && + git update-ref refs/myblobs/blob5 "$obj" && + obj=$(git hash-object -w blob6) && + git update-ref refs/myblobs/blob6 "$obj" && + obj=$(git hash-object -w blob7) && + git update-ref refs/myblobs/blob7 "$obj" && + obj=$(git hash-object -w blob8) && + git update-ref refs/myblobs/blob8 "$obj" +' + +test_expect_success 'Verify sorts with raw' ' + cat >expected <<-EOF && + refs/myblobs/blob8 + refs/myblobs/blob5 + refs/myblobs/blob6 + refs/myblobs/blob3 + refs/myblobs/blob7 + refs/mytrees/first + refs/myblobs/first + refs/myblobs/blob1 + refs/myblobs/blob2 + refs/myblobs/blob4 + refs/heads/main + EOF + ${git_for_each_ref} --format="%(refname)" --sort=raw \ + refs/heads/main refs/myblobs/ refs/mytrees/first >actual && + test_cmp expected actual +' + +test_expect_success 'Verify sorts with raw:size' ' + cat >expected <<-EOF && + refs/myblobs/blob8 + refs/myblobs/blob7 + refs/myblobs/blob4 + refs/myblobs/blob1 + refs/myblobs/blob2 + refs/myblobs/blob3 + refs/myblobs/blob5 + refs/myblobs/blob6 + refs/myblobs/first + refs/mytrees/first + refs/heads/main + EOF + ${git_for_each_ref} --format="%(refname)" --sort=raw:size \ + refs/heads/main refs/myblobs/ refs/mytrees/first >actual && + test_cmp expected actual +' + +test_expect_success 'validate raw atom with %(if:equals)' ' + cat >expected <<-EOF && + not equals + not equals + not equals + not equals + not equals + not equals + refs/myblobs/blob4 + not equals + not equals + not equals + not equals + not equals + EOF + ${git_for_each_ref} --format="%(if:equals=abc)%(raw)%(then)%(refname)%(else)not equals%(end)" \ + refs/myblobs/ refs/heads/ >actual && + test_cmp expected actual +' + +test_expect_success 'validate raw atom with %(if:notequals)' ' + cat >expected <<-EOF && + refs/heads/ambiguous + refs/heads/main + refs/heads/newtag + refs/myblobs/blob1 + refs/myblobs/blob2 + refs/myblobs/blob3 + equals + refs/myblobs/blob5 + refs/myblobs/blob6 + refs/myblobs/blob7 + refs/myblobs/blob8 + refs/myblobs/first + EOF + ${git_for_each_ref} --format="%(if:notequals=abc)%(raw)%(then)%(refname)%(else)equals%(end)" \ + refs/myblobs/ refs/heads/ >actual && + test_cmp expected actual +' + +test_expect_success 'empty raw refs with %(if)' ' + cat >expected <<-EOF && + refs/myblobs/blob1 not empty + refs/myblobs/blob2 not empty + refs/myblobs/blob3 not empty + refs/myblobs/blob4 not empty + refs/myblobs/blob5 not empty + refs/myblobs/blob6 not empty + refs/myblobs/blob7 empty + refs/myblobs/blob8 empty + refs/myblobs/first not empty + EOF + ${git_for_each_ref} --format="%(refname) %(if)%(raw)%(then)not empty%(else)empty%(end)" \ + refs/myblobs/ >actual && + test_cmp expected actual +' + +test_expect_success '%(raw) with --python must fail' ' + test_must_fail ${git_for_each_ref} --format="%(raw)" --python +' + +test_expect_success '%(raw) with --tcl must fail' ' + test_must_fail ${git_for_each_ref} --format="%(raw)" --tcl +' + +test_expect_success PERL_TEST_HELPERS '%(raw) with --perl' ' + ${git_for_each_ref} --format="\$name= %(raw); +print \"\$name\"" refs/myblobs/blob1 --perl | perl >actual && + cmp blob1 actual && + ${git_for_each_ref} --format="\$name= %(raw); +print \"\$name\"" refs/myblobs/blob3 --perl | perl >actual && + cmp blob3 actual && + ${git_for_each_ref} --format="\$name= %(raw); +print \"\$name\"" refs/myblobs/blob8 --perl | perl >actual && + cmp blob8 actual && + ${git_for_each_ref} --format="\$name= %(raw); +print \"\$name\"" refs/myblobs/first --perl | perl >actual && + cmp one actual && + git cat-file tree refs/mytrees/first > expected && + ${git_for_each_ref} --format="\$name= %(raw); +print \"\$name\"" refs/mytrees/first --perl | perl >actual && + cmp expected actual +' + +test_expect_success '%(raw) with --shell must fail' ' + test_must_fail ${git_for_each_ref} --format="%(raw)" --shell +' + +test_expect_success '%(raw) with --shell and --sort=raw must fail' ' + test_must_fail ${git_for_each_ref} --format="%(raw)" --sort=raw --shell +' + +test_expect_success '%(raw:size) with --shell' ' + ${git_for_each_ref} --format="%(raw:size)" | sed "s/^/$SQ/;s/$/$SQ/" >expect && + ${git_for_each_ref} --format="%(raw:size)" --shell >actual && + test_cmp expect actual +' + +test_expect_success "${git_for_each_ref} --format compare with cat-file --batch" ' + git rev-parse refs/mytrees/first | git cat-file --batch >expected && + ${git_for_each_ref} --format="%(objectname) %(objecttype) %(objectsize) +%(raw)" refs/mytrees/first >actual && + test_cmp expected actual +' + +test_expect_success 'verify sorts with contents:size' ' + cat >expect <<-\EOF && + refs/heads/main + refs/heads/newtag + refs/heads/ambiguous + EOF + ${git_for_each_ref} --format="%(refname)" \ + --sort=contents:size refs/heads/ >actual && + test_cmp expect actual +' + +test_expect_success 'set up multiple-sort tags' ' + for when in 100000 200000 + do + for email in user1 user2 + do + for ref in ref1 ref2 + do + GIT_COMMITTER_DATE="@$when +0000" \ + GIT_COMMITTER_EMAIL="$email@example.com" \ + git tag -m "tag $ref-$when-$email" \ + multi-$ref-$when-$email || return 1 + done + done + done +' + +test_expect_success 'Verify sort with multiple keys' ' + cat >expected <<-\EOF && + 100000 refs/tags/multi-ref2-100000-user1 + 100000 refs/tags/multi-ref1-100000-user1 + 100000 refs/tags/multi-ref2-100000-user2 + 100000 refs/tags/multi-ref1-100000-user2 + 200000 refs/tags/multi-ref2-200000-user1 + 200000 refs/tags/multi-ref1-200000-user1 + 200000 refs/tags/multi-ref2-200000-user2 + 200000 refs/tags/multi-ref1-200000-user2 + EOF + ${git_for_each_ref} \ + --format="%(taggerdate:unix) %(taggeremail) %(refname)" \ + --sort=-refname \ + --sort=taggeremail \ + --sort=taggerdate \ + "refs/tags/multi-*" >actual && + test_cmp expected actual +' + +test_expect_success 'equivalent sorts fall back on refname' ' + cat >expected <<-\EOF && + 100000 refs/tags/multi-ref1-100000-user1 + 100000 refs/tags/multi-ref1-100000-user2 + 100000 refs/tags/multi-ref2-100000-user1 + 100000 refs/tags/multi-ref2-100000-user2 + 200000 refs/tags/multi-ref1-200000-user1 + 200000 refs/tags/multi-ref1-200000-user2 + 200000 refs/tags/multi-ref2-200000-user1 + 200000 refs/tags/multi-ref2-200000-user2 + EOF + ${git_for_each_ref} \ + --format="%(taggerdate:unix) %(taggeremail) %(refname)" \ + --sort=taggerdate \ + "refs/tags/multi-*" >actual && + test_cmp expected actual +' + +test_expect_success '--no-sort cancels the previous sort keys' ' + cat >expected <<-\EOF && + 100000 refs/tags/multi-ref1-100000-user1 + 100000 refs/tags/multi-ref1-100000-user2 + 100000 refs/tags/multi-ref2-100000-user1 + 100000 refs/tags/multi-ref2-100000-user2 + 200000 refs/tags/multi-ref1-200000-user1 + 200000 refs/tags/multi-ref1-200000-user2 + 200000 refs/tags/multi-ref2-200000-user1 + 200000 refs/tags/multi-ref2-200000-user2 + EOF + ${git_for_each_ref} \ + --format="%(taggerdate:unix) %(taggeremail) %(refname)" \ + --sort=-refname \ + --sort=taggeremail \ + --no-sort \ + --sort=taggerdate \ + "refs/tags/multi-*" >actual && + test_cmp expected actual +' + +test_expect_success '--no-sort without subsequent --sort prints expected refs' ' + cat >expected <<-\EOF && + refs/tags/multi-ref1-100000-user1 + refs/tags/multi-ref1-100000-user2 + refs/tags/multi-ref1-200000-user1 + refs/tags/multi-ref1-200000-user2 + refs/tags/multi-ref2-100000-user1 + refs/tags/multi-ref2-100000-user2 + refs/tags/multi-ref2-200000-user1 + refs/tags/multi-ref2-200000-user2 + EOF + + # Sort the results with `sort` for a consistent comparison against + # expected + ${git_for_each_ref} \ + --format="%(refname)" \ + --no-sort \ + "refs/tags/multi-*" | sort >actual && + test_cmp expected actual +' + +test_expect_success 'set up custom date sorting' ' + # Dates: + # - Wed Feb 07 2024 21:34:20 +0000 + # - Tue Dec 14 1999 00:05:22 +0000 + # - Fri Jun 04 2021 11:26:51 +0000 + # - Mon Jan 22 2007 16:44:01 GMT+0000 + i=1 && + for when in 1707341660 945129922 1622806011 1169484241 + do + GIT_COMMITTER_DATE="@$when +0000" \ + GIT_COMMITTER_EMAIL="user@example.com" \ + git tag -m "tag $when" custom-dates-$i && + i=$(($i+1)) || return 1 + done +' + +test_expect_success 'sort by date defaults to full timestamp' ' + cat >expected <<-\EOF && + 945129922 refs/tags/custom-dates-2 + 1169484241 refs/tags/custom-dates-4 + 1622806011 refs/tags/custom-dates-3 + 1707341660 refs/tags/custom-dates-1 + EOF + + ${git_for_each_ref} \ + --format="%(creatordate:unix) %(refname)" \ + --sort=creatordate \ + "refs/tags/custom-dates-*" >actual && + test_cmp expected actual +' + +test_expect_success 'sort by custom date format' ' + cat >expected <<-\EOF && + 00:05:22 refs/tags/custom-dates-2 + 11:26:51 refs/tags/custom-dates-3 + 16:44:01 refs/tags/custom-dates-4 + 21:34:20 refs/tags/custom-dates-1 + EOF + + ${git_for_each_ref} \ + --format="%(creatordate:format:%H:%M:%S) %(refname)" \ + --sort="creatordate:format:%H:%M:%S" \ + "refs/tags/custom-dates-*" >actual && + test_cmp expected actual +' + +test_expect_success 'do not dereference NULL upon %(HEAD) on unborn branch' ' + test_when_finished "git checkout main" && + ${git_for_each_ref} --format="%(HEAD) %(refname:short)" refs/heads/ >actual && + sed -e "s/^\* / /" actual >expect && + git checkout --orphan orphaned-branch && + ${git_for_each_ref} --format="%(HEAD) %(refname:short)" refs/heads/ >actual && + test_cmp expect actual +' + +cat >trailers < +Signed-off-by: A U Thor +[ v2 updated patch description ] +Acked-by: A U Thor + +EOF + +unfold () { + perl -0pe 's/\n\s+/ /g' +} + +test_expect_success 'set up trailers for next test' ' + echo "Some contents" > two && + git add two && + git commit -F - <<-EOF + trailers: this commit message has trailers + + Some message contents + + $(cat trailers) + EOF +' + +test_trailer_option () { + if test "$#" -eq 3 + then + prereq="$1" + shift + fi && + title=$1 option=$2 + cat >expect + test_expect_success $prereq "$title" ' + ${git_for_each_ref} --format="%($option)" refs/heads/main >actual && + test_cmp expect actual && + ${git_for_each_ref} --format="%(contents:$option)" refs/heads/main >actual && + test_cmp expect actual + ' +} + +test_trailer_option PERL_TEST_HELPERS '%(trailers:unfold) unfolds trailers' \ + 'trailers:unfold' <<-EOF + $(unfold + + EOF + +test_trailer_option '%(trailers:key=foo) is case insensitive' \ + 'trailers:key=SiGned-oFf-bY' <<-EOF + Signed-off-by: A U Thor + + EOF + +test_trailer_option '%(trailers:key=foo:) trailing colon also works' \ + 'trailers:key=Signed-off-by:' <<-EOF + Signed-off-by: A U Thor + + EOF + +test_trailer_option '%(trailers:key=foo) multiple keys' \ + 'trailers:key=Reviewed-by:,key=Signed-off-by' <<-EOF + Reviewed-by: A U Thor + Signed-off-by: A U Thor + + EOF + +test_trailer_option '%(trailers:key=nonexistent) becomes empty' \ + 'trailers:key=Shined-off-by:' <<-EOF + + EOF + +test_trailer_option '%(trailers:key=foo) handles multiple lines even if folded' \ + 'trailers:key=Acked-by' <<-EOF + $(grep -v patch.description + $(grep patch.description + + EOF + +test_trailer_option '%(trailers:separator) changes separator' \ + 'trailers:separator=%x2C,key=Reviewed-by,key=Signed-off-by:' <<-EOF + Reviewed-by: A U Thor ,Signed-off-by: A U Thor + EOF + +test_trailer_option '%(trailers:key_value_separator) changes key-value separator' \ + 'trailers:key_value_separator=%x2C,key=Reviewed-by,key=Signed-off-by:' <<-EOF + Reviewed-by,A U Thor + Signed-off-by,A U Thor + + EOF + +test_trailer_option '%(trailers:separator,key_value_separator) changes both separators' \ + 'trailers:separator=%x2C,key_value_separator=%x2C,key=Reviewed-by,key=Signed-off-by:' <<-EOF + Reviewed-by,A U Thor ,Signed-off-by,A U Thor + EOF + +test_expect_success 'multiple %(trailers) use their own options' ' + git tag -F - tag-with-trailers <<-\EOF && + body + + one: foo + one: bar + two: baz + two: qux + EOF + t1="%(trailers:key=one,key_value_separator=W,separator=X)" && + t2="%(trailers:key=two,key_value_separator=Y,separator=Z)" && + ${git_for_each_ref} --format="$t1%0a$t2" refs/tags/tag-with-trailers >actual && + cat >expect <<-\EOF && + oneWfooXoneWbar + twoYbazZtwoYqux + EOF + test_cmp expect actual +' + +test_failing_trailer_option () { + title=$1 option=$2 + cat >expect + test_expect_success "$title" ' + # error message cannot be checked under i18n + test_must_fail ${git_for_each_ref} --format="%($option)" refs/heads/main 2>actual && + test_cmp expect actual && + test_must_fail ${git_for_each_ref} --format="%(contents:$option)" refs/heads/main 2>actual && + test_cmp expect actual + ' +} + +test_failing_trailer_option '%(trailers) rejects unknown trailers arguments' \ + 'trailers:unsupported' <<-\EOF + fatal: unknown %(trailers) argument: unsupported + EOF + +test_failing_trailer_option '%(trailers:key) without value is error' \ + 'trailers:key' <<-\EOF + fatal: expected %(trailers:key=) + EOF + +test_expect_success 'if arguments, %(contents:trailers) shows error if colon is missing' ' + cat >expect <<-EOF && + fatal: unrecognized %(contents) argument: trailersonly + EOF + test_must_fail ${git_for_each_ref} --format="%(contents:trailersonly)" 2>actual && + test_cmp expect actual +' + +test_expect_success 'basic atom: head contents:trailers' ' + ${git_for_each_ref} --format="%(contents:trailers)" refs/heads/main >actual && + sanitize_pgp actual.clean && + # ${git_for_each_ref} ends with a blank line + cat >expect <<-EOF && + $(cat trailers) + + EOF + test_cmp expect actual.clean +' + +test_expect_success 'basic atom: rest must fail' ' + test_must_fail ${git_for_each_ref} --format="%(rest)" refs/heads/main +' + +test_expect_success 'HEAD atom does not take arguments' ' + test_must_fail ${git_for_each_ref} --format="%(HEAD:foo)" 2>err && + echo "fatal: %(HEAD) does not take arguments" >expect && + test_cmp expect err +' + +test_expect_success 'subject atom rejects unknown arguments' ' + test_must_fail ${git_for_each_ref} --format="%(subject:foo)" 2>err && + echo "fatal: unrecognized %(subject) argument: foo" >expect && + test_cmp expect err +' + +test_expect_success 'refname atom rejects unknown arguments' ' + test_must_fail ${git_for_each_ref} --format="%(refname:foo)" 2>err && + echo "fatal: unrecognized %(refname) argument: foo" >expect && + test_cmp expect err +' + +test_expect_success 'trailer parsing not fooled by --- line' ' + git commit --allow-empty -F - <<-\EOF && + this is the subject + + This is the body. The message has a "---" line which would confuse a + message+patch parser. But here we know we have only a commit message, + so we get it right. + + trailer: wrong + --- + This is more body. + + trailer: right + EOF + + { + echo "trailer: right" && + echo + } >expect && + ${git_for_each_ref} --format="%(trailers)" refs/heads/main >actual && + test_cmp expect actual +' + +test_expect_success 'Add symbolic ref for the following tests' ' + git symbolic-ref refs/heads/sym refs/heads/main +' + +cat >expected <actual && + test_cmp expected actual +' + +cat >expected <actual && + test_cmp expected actual +' + +cat >expected < actual && + ${git_for_each_ref} --format="%(symref:lstrip=-2)" refs/heads/sym >> actual && + test_cmp expected actual && + + ${git_for_each_ref} --format="%(symref:strip=2)" refs/heads/sym > actual && + ${git_for_each_ref} --format="%(symref:strip=-2)" refs/heads/sym >> actual && + test_cmp expected actual +' + +cat >expected < actual && + ${git_for_each_ref} --format="%(symref:rstrip=-2)" refs/heads/sym >> actual && + test_cmp expected actual +' + +test_expect_success ':remotename and :remoteref' ' + git init remote-tests && + ( + cd remote-tests && + test_commit initial && + git branch -M main && + git remote add from fifth.coffee:blub && + git config branch.main.remote from && + git config branch.main.merge refs/heads/stable && + git remote add to southridge.audio:repo && + git config remote.to.push "refs/heads/*:refs/heads/pushed/*" && + git config branch.main.pushRemote to && + for pair in "%(upstream)=refs/remotes/from/stable" \ + "%(upstream:remotename)=from" \ + "%(upstream:remoteref)=refs/heads/stable" \ + "%(push)=refs/remotes/to/pushed/main" \ + "%(push:remotename)=to" \ + "%(push:remoteref)=refs/heads/pushed/main" + do + echo "${pair#*=}" >expect && + ${git_for_each_ref} --format="${pair%=*}" \ + refs/heads/main >actual && + test_cmp expect actual || exit 1 + done && + git branch push-simple && + git config branch.push-simple.pushRemote from && + actual="$(${git_for_each_ref} \ + --format="%(push:remotename),%(push:remoteref)" \ + refs/heads/push-simple)" && + test from, = "$actual" + ) +' + +test_expect_success "${git_for_each_ref} --ignore-case ignores case" ' + ${git_for_each_ref} --format="%(refname)" refs/heads/MAIN >actual && + test_must_be_empty actual && + + echo refs/heads/main >expect && + ${git_for_each_ref} --format="%(refname)" --ignore-case \ + refs/heads/MAIN >actual && + test_cmp expect actual +' + +test_expect_success "${git_for_each_ref} --omit-empty works" ' + ${git_for_each_ref} --format="%(refname)" >actual && + test_line_count -gt 1 actual && + ${git_for_each_ref} --format="%(if:equals=refs/heads/main)%(refname)%(then)%(refname)%(end)" --omit-empty >actual && + echo refs/heads/main >expect && + test_cmp expect actual +' + +test_expect_success "${git_for_each_ref} --ignore-case works on multiple sort keys" ' + # name refs numerically to avoid case-insensitive filesystem conflicts + nr=0 && + for email in a A b B + do + for subject in a A b B + do + GIT_COMMITTER_EMAIL="$email@example.com" \ + git tag -m "tag $subject" icase-$(printf %02d $nr) && + nr=$((nr+1))|| + return 1 + done + done && + ${git_for_each_ref} --ignore-case \ + --format="%(taggeremail) %(subject) %(refname)" \ + --sort=refname \ + --sort=subject \ + --sort=taggeremail \ + refs/tags/icase-* >actual && + cat >expect <<-\EOF && + tag a refs/tags/icase-00 + tag A refs/tags/icase-01 + tag a refs/tags/icase-04 + tag A refs/tags/icase-05 + tag b refs/tags/icase-02 + tag B refs/tags/icase-03 + tag b refs/tags/icase-06 + tag B refs/tags/icase-07 + tag a refs/tags/icase-08 + tag A refs/tags/icase-09 + tag a refs/tags/icase-12 + tag A refs/tags/icase-13 + tag b refs/tags/icase-10 + tag B refs/tags/icase-11 + tag b refs/tags/icase-14 + tag B refs/tags/icase-15 + EOF + test_cmp expect actual +' + +test_expect_success "${git_for_each_ref} reports broken tags" ' + git tag -m "good tag" broken-tag-good HEAD && + git cat-file tag broken-tag-good >good && + sed s/commit/blob/ bad && + bad=$(git hash-object -w -t tag bad) && + git update-ref refs/tags/broken-tag-bad $bad && + test_must_fail ${git_for_each_ref} --format="%(*objectname)" \ + refs/tags/broken-tag-* +' + +test_expect_success 'set up tag with signature and no blank lines' ' + git tag -F - fake-sig-no-blanks <<-\EOF + this is the subject + -----BEGIN PGP SIGNATURE----- + not a real signature, but we just care about the + subject/body parsing. It is important here that + there are no blank lines in the signature. + -----END PGP SIGNATURE----- + EOF +' + +test_atom refs/tags/fake-sig-no-blanks contents:subject 'this is the subject' +test_atom refs/tags/fake-sig-no-blanks contents:body '' +test_atom refs/tags/fake-sig-no-blanks contents:signature "$sig" + +test_expect_success 'set up tag with CRLF signature' ' + append_cr <<-\EOF | + this is the subject + -----BEGIN PGP SIGNATURE----- + + not a real signature, but we just care about + the subject/body parsing. It is important here + that there is a blank line separating this + from the signature header. + -----END PGP SIGNATURE----- + EOF + git tag -F - --cleanup=verbatim fake-sig-crlf +' + +test_atom refs/tags/fake-sig-crlf contents:subject 'this is the subject' +test_atom refs/tags/fake-sig-crlf contents:body '' + +# CRLF is retained in the signature, so we have to pass our expected value +# through append_cr. But test_atom requires a shell string, which means command +# substitution, and the shell will strip trailing newlines from the output of +# the substitution. Hack around it by adding and then removing a dummy line. +sig_crlf="$(printf "%s" "$sig" | append_cr; echo dummy)" +sig_crlf=${sig_crlf%dummy} +test_atom refs/tags/fake-sig-crlf contents:signature "$sig_crlf" + +test_expect_success 'set up tag with signature and trailers' ' + git tag -F - fake-sig-trailer <<-\EOF + this is the subject + + this is the body + + My-Trailer: foo + -----BEGIN PGP SIGNATURE----- + + not a real signature, but we just care about the + subject/body/trailer parsing. + -----END PGP SIGNATURE----- + EOF +' + +# use "separator=" here to suppress the terminating newline +test_atom refs/tags/fake-sig-trailer trailers:separator= 'My-Trailer: foo' + +test_expect_success "${git_for_each_ref} --stdin: empty" ' + >in && + ${git_for_each_ref} --format="%(refname)" --stdin actual && + ${git_for_each_ref} --format="%(refname)" >expect && + test_cmp expect actual +' + +test_expect_success "${git_for_each_ref} --stdin: fails if extra args" ' + >in && + test_must_fail ${git_for_each_ref} --format="%(refname)" \ + --stdin refs/heads/extra err && + grep "unknown arguments supplied with --stdin" err +' + +test_expect_success "${git_for_each_ref} --stdin: matches" ' + cat >in <<-EOF && + refs/tags/multi* + refs/heads/amb* + EOF + + cat >expect <<-EOF && + refs/heads/ambiguous + refs/tags/multi-ref1-100000-user1 + refs/tags/multi-ref1-100000-user2 + refs/tags/multi-ref1-200000-user1 + refs/tags/multi-ref1-200000-user2 + refs/tags/multi-ref2-100000-user1 + refs/tags/multi-ref2-100000-user2 + refs/tags/multi-ref2-200000-user1 + refs/tags/multi-ref2-200000-user2 + refs/tags/multiline + EOF + + ${git_for_each_ref} --format="%(refname)" --stdin actual && + test_cmp expect actual +' + +test_expect_success "${git_for_each_ref} with non-existing refs" ' + cat >in <<-EOF && + refs/heads/this-ref-does-not-exist + refs/tags/bogus + EOF + + ${git_for_each_ref} --format="%(refname)" --stdin actual && + test_must_be_empty actual && + + xargs ${git_for_each_ref} --format="%(refname)" actual && + test_must_be_empty actual +' + +test_expect_success "${git_for_each_ref} with nested tags" ' + git tag -am "Normal tag" nested/base HEAD && + git tag -am "Nested tag" nested/nest1 refs/tags/nested/base && + git tag -am "Double nested tag" nested/nest2 refs/tags/nested/nest1 && + + head_oid="$(git rev-parse HEAD)" && + base_tag_oid="$(git rev-parse refs/tags/nested/base)" && + nest1_tag_oid="$(git rev-parse refs/tags/nested/nest1)" && + nest2_tag_oid="$(git rev-parse refs/tags/nested/nest2)" && + + cat >expect <<-EOF && + refs/tags/nested/base $base_tag_oid tag $head_oid commit + refs/tags/nested/nest1 $nest1_tag_oid tag $head_oid commit + refs/tags/nested/nest2 $nest2_tag_oid tag $head_oid commit + EOF + + ${git_for_each_ref} \ + --format="%(refname) %(objectname) %(objecttype) %(*objectname) %(*objecttype)" \ + refs/tags/nested/ >actual && + test_cmp expect actual +' + +test_expect_success 'is-base atom with non-commits' ' + ${git_for_each_ref} --format="%(is-base:HEAD) %(refname)" >out 2>err && + grep "(HEAD) refs/heads/main" out && + + test_line_count = 2 err && + grep "error: object .* is a commit, not a blob" err && + grep "error: bad tag pointer to" err +' + +GRADE_FORMAT="%(signature:grade)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)" +TRUSTLEVEL_FORMAT="%(signature:trustlevel)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)" + +test_expect_success GPG 'setup for signature atom using gpg' ' + git checkout -b signed && + + test_when_finished "test_unconfig commit.gpgSign" && + + echo "1" >file && + git add file && + test_tick && + git commit -S -m "file: 1" && + git tag first-signed && + + echo "2" >file && + test_tick && + git commit -a -m "file: 2" && + git tag second-unsigned && + + git config commit.gpgSign 1 && + echo "3" >file && + test_tick && + git commit -a --no-gpg-sign -m "file: 3" && + git tag third-unsigned && + + test_tick && + git rebase -f HEAD^^ && git tag second-signed HEAD^ && + git tag third-signed && + + echo "4" >file && + test_tick && + git commit -a -SB7227189 -m "file: 4" && + git tag fourth-signed && + + echo "5" >file && + test_tick && + git commit -a --no-gpg-sign -m "file: 5" && + git tag fifth-unsigned && + + echo "6" >file && + test_tick && + git commit -a --no-gpg-sign -m "file: 6" && + + test_tick && + git rebase -f HEAD^^ && + git tag fifth-signed HEAD^ && + git tag sixth-signed && + + echo "7" >file && + test_tick && + git commit -a --no-gpg-sign -m "file: 7" && + git tag seventh-unsigned +' + +test_expect_success GPGSSH 'setup for signature atom using ssh' ' + test_when_finished "test_unconfig gpg.format user.signingkey" && + + test_config gpg.format ssh && + test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" && + echo "8" >file && + test_tick && + git add file && + git commit -S -m "file: 8" && + git tag eighth-signed-ssh +' + +test_expect_success GPG2 'bare signature atom' ' + git verify-commit first-signed 2>expect && + echo >>expect && + ${git_for_each_ref} refs/tags/first-signed \ + --format="%(signature)" >actual && + test_cmp expect actual +' + +test_expect_success GPG 'show good signature with custom format' ' + git verify-commit first-signed && + cat >expect <<-\EOF && + G + 13B6F51ECDDE430D + C O Mitter + 73D758744BE721698EC54E8713B6F51ECDDE430D + 73D758744BE721698EC54E8713B6F51ECDDE430D + EOF + ${git_for_each_ref} refs/tags/first-signed \ + --format="$GRADE_FORMAT" >actual && + test_cmp expect actual +' +test_expect_success GPGSSH 'show good signature with custom format with ssh' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + FINGERPRINT=$(ssh-keygen -lf "${GPGSSH_KEY_PRIMARY}" | awk "{print \$2;}") && + cat >expect.tmpl <<-\EOF && + G + FINGERPRINT + principal with number 1 + FINGERPRINT + + EOF + sed "s|FINGERPRINT|$FINGERPRINT|g" expect.tmpl >expect && + ${git_for_each_ref} refs/tags/eighth-signed-ssh \ + --format="$GRADE_FORMAT" >actual && + test_cmp expect actual +' + +test_expect_success GPG 'signature atom with grade option and bad signature' ' + git cat-file commit third-signed >raw && + sed -e "s/^file: 3/file: 3 forged/" raw >forged1 && + FORGED1=$(git hash-object -w -t commit forged1) && + git update-ref refs/tags/third-signed "$FORGED1" && + test_must_fail git verify-commit "$FORGED1" && + + cat >expect <<-\EOF && + B + 13B6F51ECDDE430D + C O Mitter + + + EOF + ${git_for_each_ref} refs/tags/third-signed \ + --format="$GRADE_FORMAT" >actual && + test_cmp expect actual +' + +test_expect_success GPG 'show untrusted signature with custom format' ' + cat >expect <<-\EOF && + U + 65A0EEA02E30CAD7 + Eris Discordia + F8364A59E07FFE9F4D63005A65A0EEA02E30CAD7 + D4BE22311AD3131E5EDA29A461092E85B7227189 + EOF + ${git_for_each_ref} refs/tags/fourth-signed \ + --format="$GRADE_FORMAT" >actual && + test_cmp expect actual +' + +test_expect_success GPG 'show untrusted signature with undefined trust level' ' + cat >expect <<-\EOF && + undefined + 65A0EEA02E30CAD7 + Eris Discordia + F8364A59E07FFE9F4D63005A65A0EEA02E30CAD7 + D4BE22311AD3131E5EDA29A461092E85B7227189 + EOF + ${git_for_each_ref} refs/tags/fourth-signed \ + --format="$TRUSTLEVEL_FORMAT" >actual && + test_cmp expect actual +' + +test_expect_success GPG 'show untrusted signature with ultimate trust level' ' + cat >expect <<-\EOF && + ultimate + 13B6F51ECDDE430D + C O Mitter + 73D758744BE721698EC54E8713B6F51ECDDE430D + 73D758744BE721698EC54E8713B6F51ECDDE430D + EOF + ${git_for_each_ref} refs/tags/sixth-signed \ + --format="$TRUSTLEVEL_FORMAT" >actual && + test_cmp expect actual +' + +test_expect_success GPG 'show unknown signature with custom format' ' + cat >expect <<-\EOF && + E + 13B6F51ECDDE430D + + + + EOF + GNUPGHOME="$GNUPGHOME_NOT_USED" ${git_for_each_ref} \ + refs/tags/sixth-signed --format="$GRADE_FORMAT" >actual && + test_cmp expect actual +' + +test_expect_success GPG 'show lack of signature with custom format' ' + cat >expect <<-\EOF && + N + + + + + EOF + ${git_for_each_ref} refs/tags/seventh-unsigned \ + --format="$GRADE_FORMAT" >actual && + test_cmp expect actual +' + +test_done diff --git a/t/t6300-for-each-ref.sh b/t/t6300-for-each-ref.sh index ce9af79ab12..1d9809114d2 100755 --- a/t/t6300-for-each-ref.sh +++ b/t/t6300-for-each-ref.sh @@ -6,2150 +6,14 @@ test_description='for-each-ref test' . ./test-lib.sh -GNUPGHOME_NOT_USED=$GNUPGHOME -. "$TEST_DIRECTORY"/lib-gpg.sh -. "$TEST_DIRECTORY"/lib-terminal.sh -# Mon Jul 3 23:18:43 2006 +0000 -datestamp=1151968723 -setdate_and_increment () { - GIT_COMMITTER_DATE="$datestamp +0200" - datestamp=$(expr "$datestamp" + 1) - GIT_AUTHOR_DATE="$datestamp +0200" - datestamp=$(expr "$datestamp" + 1) - export GIT_COMMITTER_DATE GIT_AUTHOR_DATE -} - -test_object_file_size () { - oid=$(git rev-parse "$1") - path=".git/objects/$(test_oid_to_path $oid)" - test_file_size "$path" -} - -test_expect_success setup ' - # setup .mailmap - cat >.mailmap <<-EOF && - A Thor A U Thor - C Mitter C O Mitter - EOF - - setdate_and_increment && - echo "Using $datestamp" > one && - git add one && - git commit -m "Initial" && - git branch -M main && - setdate_and_increment && - git tag -a -m "Tagging at $datestamp" testtag && - git update-ref refs/remotes/origin/main main && - git remote add origin nowhere && - git config branch.main.remote origin && - git config branch.main.merge refs/heads/main && - git remote add myfork elsewhere && - git config remote.pushdefault myfork && - git config push.default current -' - -test_atom () { - case "$1" in - head) ref=refs/heads/main ;; - tag) ref=refs/tags/testtag ;; - sym) ref=refs/heads/sym ;; - *) ref=$1 ;; - esac - format=$2 - test_do=test_expect_${4:-success} - - printf '%s\n' "$3" >expected - $test_do $PREREQ "basic atom: $ref $format" ' - git for-each-ref --format="%($format)" "$ref" >actual && - sanitize_pgp actual.clean && - test_cmp expected actual.clean - ' - - # Automatically test "contents:size" atom after testing "contents" - if test "$format" = "contents" - then - # for commit leg, $3 is changed there - expect=$(printf '%s' "$3" | wc -c) - $test_do $PREREQ "basic atom: $ref contents:size" ' - type=$(git cat-file -t "$ref") && - case $type in - tag) - # We cannot use $3 as it expects sanitize_pgp to run - git cat-file tag $ref >out && - expect=$(tail -n +6 out | wc -c) && - rm -f out ;; - tree | blob) - expect="" ;; - commit) - : "use the calculated expect" ;; - *) - BUG "unknown object type" ;; - esac && - # Leave $expect unquoted to lose possible leading whitespaces - echo $expect >expected && - git for-each-ref --format="%(contents:size)" "$ref" >actual && - test_cmp expected actual - ' - fi -} - -hexlen=$(test_oid hexsz) - -test_atom head refname refs/heads/main -test_atom head refname: refs/heads/main -test_atom head refname:short main -test_atom head refname:lstrip=1 heads/main -test_atom head refname:lstrip=2 main -test_atom head refname:lstrip=-1 main -test_atom head refname:lstrip=-2 heads/main -test_atom head refname:rstrip=1 refs/heads -test_atom head refname:rstrip=2 refs -test_atom head refname:rstrip=-1 refs -test_atom head refname:rstrip=-2 refs/heads -test_atom head refname:strip=1 heads/main -test_atom head refname:strip=2 main -test_atom head refname:strip=-1 main -test_atom head refname:strip=-2 heads/main -test_atom head upstream refs/remotes/origin/main -test_atom head upstream:short origin/main -test_atom head upstream:lstrip=2 origin/main -test_atom head upstream:lstrip=-2 origin/main -test_atom head upstream:rstrip=2 refs/remotes -test_atom head upstream:rstrip=-2 refs/remotes -test_atom head upstream:strip=2 origin/main -test_atom head upstream:strip=-2 origin/main -test_atom head push refs/remotes/myfork/main -test_atom head push:short myfork/main -test_atom head push:lstrip=1 remotes/myfork/main -test_atom head push:lstrip=-1 main -test_atom head push:rstrip=1 refs/remotes/myfork -test_atom head push:rstrip=-1 refs -test_atom head push:strip=1 remotes/myfork/main -test_atom head push:strip=-1 main -test_atom head objecttype commit -test_atom head objectsize $((131 + hexlen)) -test_atom head objectsize:disk $(test_object_file_size refs/heads/main) -test_atom head deltabase $ZERO_OID -test_atom head objectname $(git rev-parse refs/heads/main) -test_atom head objectname:short $(git rev-parse --short refs/heads/main) -test_atom head objectname:short=1 $(git rev-parse --short=1 refs/heads/main) -test_atom head objectname:short=10 $(git rev-parse --short=10 refs/heads/main) -test_atom head tree $(git rev-parse refs/heads/main^{tree}) -test_atom head tree:short $(git rev-parse --short refs/heads/main^{tree}) -test_atom head tree:short=1 $(git rev-parse --short=1 refs/heads/main^{tree}) -test_atom head tree:short=10 $(git rev-parse --short=10 refs/heads/main^{tree}) -test_atom head parent '' -test_atom head parent:short '' -test_atom head parent:short=1 '' -test_atom head parent:short=10 '' -test_atom head numparent 0 -test_atom head object '' -test_atom head type '' -test_atom head raw "$(git cat-file commit refs/heads/main) -" -test_atom head '*objectname' '' -test_atom head '*objecttype' '' -test_atom head author 'A U Thor 1151968724 +0200' -test_atom head authorname 'A U Thor' -test_atom head authorname:mailmap 'A Thor' -test_atom head authoremail '' -test_atom head authoremail:trim 'author@example.com' -test_atom head authoremail:localpart 'author' -test_atom head authoremail:trim,localpart 'author' -test_atom head authoremail:mailmap '' -test_atom head authoremail:mailmap,trim 'athor@example.com' -test_atom head authoremail:trim,mailmap 'athor@example.com' -test_atom head authoremail:mailmap,localpart 'athor' -test_atom head authoremail:localpart,mailmap 'athor' -test_atom head authoremail:mailmap,trim,localpart,mailmap,trim 'athor' -test_atom head authordate 'Tue Jul 4 01:18:44 2006 +0200' -test_atom head committer 'C O Mitter 1151968723 +0200' -test_atom head committername 'C O Mitter' -test_atom head committername:mailmap 'C Mitter' -test_atom head committeremail '' -test_atom head committeremail:trim 'committer@example.com' -test_atom head committeremail:localpart 'committer' -test_atom head committeremail:localpart,trim 'committer' -test_atom head committeremail:mailmap '' -test_atom head committeremail:mailmap,trim 'cmitter@example.com' -test_atom head committeremail:trim,mailmap 'cmitter@example.com' -test_atom head committeremail:mailmap,localpart 'cmitter' -test_atom head committeremail:localpart,mailmap 'cmitter' -test_atom head committeremail:trim,mailmap,trim,trim,localpart 'cmitter' -test_atom head committerdate 'Tue Jul 4 01:18:43 2006 +0200' -test_atom head tag '' -test_atom head tagger '' -test_atom head taggername '' -test_atom head taggeremail '' -test_atom head taggeremail:trim '' -test_atom head taggeremail:localpart '' -test_atom head taggerdate '' -test_atom head creator 'C O Mitter 1151968723 +0200' -test_atom head creatordate 'Tue Jul 4 01:18:43 2006 +0200' -test_atom head subject 'Initial' -test_atom head subject:sanitize 'Initial' -test_atom head contents:subject 'Initial' -test_atom head body '' -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 refname:short testtag -test_atom tag upstream '' -test_atom tag push '' -test_atom tag objecttype tag -test_atom tag objectsize $((114 + hexlen)) -test_atom tag objectsize:disk $(test_object_file_size refs/tags/testtag) -test_atom tag '*objectsize:disk' $(test_object_file_size refs/heads/main) -test_atom tag deltabase $ZERO_OID -test_atom tag '*deltabase' $ZERO_OID -test_atom tag objectname $(git rev-parse refs/tags/testtag) -test_atom tag objectname:short $(git rev-parse --short refs/tags/testtag) -test_atom head objectname:short=1 $(git rev-parse --short=1 refs/heads/main) -test_atom head objectname:short=10 $(git rev-parse --short=10 refs/heads/main) -test_atom tag tree '' -test_atom tag tree:short '' -test_atom tag tree:short=1 '' -test_atom tag tree:short=10 '' -test_atom tag parent '' -test_atom tag parent:short '' -test_atom tag parent:short=1 '' -test_atom tag parent:short=10 '' -test_atom tag numparent '' -test_atom tag object $(git rev-parse refs/tags/testtag^0) -test_atom tag type 'commit' -test_atom tag '*objectname' $(git rev-parse refs/tags/testtag^{}) -test_atom tag '*objecttype' 'commit' -test_atom tag author '' -test_atom tag authorname '' -test_atom tag authorname:mailmap '' -test_atom tag authoremail '' -test_atom tag authoremail:trim '' -test_atom tag authoremail:localpart '' -test_atom tag authoremail:trim,localpart '' -test_atom tag authoremail:mailmap '' -test_atom tag authoremail:mailmap,trim '' -test_atom tag authoremail:trim,mailmap '' -test_atom tag authoremail:mailmap,localpart '' -test_atom tag authoremail:localpart,mailmap '' -test_atom tag authoremail:mailmap,trim,localpart,mailmap,trim '' -test_atom tag authordate '' -test_atom tag committer '' -test_atom tag committername '' -test_atom tag committername:mailmap '' -test_atom tag committeremail '' -test_atom tag committeremail:trim '' -test_atom tag committeremail:localpart '' -test_atom tag committeremail:localpart,trim '' -test_atom tag committeremail:mailmap '' -test_atom tag committeremail:mailmap,trim '' -test_atom tag committeremail:trim,mailmap '' -test_atom tag committeremail:mailmap,localpart '' -test_atom tag committeremail:localpart,mailmap '' -test_atom tag committeremail:trim,mailmap,trim,trim,localpart '' -test_atom tag committerdate '' -test_atom tag tag 'testtag' -test_atom tag tagger 'C O Mitter 1151968725 +0200' -test_atom tag taggername 'C O Mitter' -test_atom tag taggername:mailmap 'C Mitter' -test_atom tag taggeremail '' -test_atom tag taggeremail:trim 'committer@example.com' -test_atom tag taggeremail:localpart 'committer' -test_atom tag taggeremail:trim,localpart 'committer' -test_atom tag taggeremail:mailmap '' -test_atom tag taggeremail:mailmap,trim 'cmitter@example.com' -test_atom tag taggeremail:trim,mailmap 'cmitter@example.com' -test_atom tag taggeremail:mailmap,localpart 'cmitter' -test_atom tag taggeremail:localpart,mailmap 'cmitter' -test_atom tag taggeremail:trim,mailmap,trim,localpart,localpart 'cmitter' -test_atom tag taggerdate 'Tue Jul 4 01:18:45 2006 +0200' -test_atom tag creator 'C O Mitter 1151968725 +0200' -test_atom tag creatordate 'Tue Jul 4 01:18:45 2006 +0200' -test_atom tag subject 'Tagging at 1151968727' -test_atom tag subject:sanitize 'Tagging-at-1151968727' -test_atom tag contents:subject 'Tagging at 1151968727' -test_atom tag body '' -test_atom tag contents:body '' -test_atom tag contents:signature '' -test_atom tag contents 'Tagging at 1151968727 -' -test_atom tag HEAD ' ' - -test_expect_success 'basic atom: refs/tags/testtag *raw' ' - git cat-file commit refs/tags/testtag^{} >expected && - git for-each-ref --format="%(*raw)" refs/tags/testtag >actual && - sanitize_pgp expected.clean && - echo >>expected.clean && - sanitize_pgp actual.clean && - test_cmp expected.clean actual.clean -' - -test_expect_success 'Check invalid atoms names are errors' ' - test_must_fail git for-each-ref --format="%(INVALID)" refs/heads -' - -test_expect_success 'for-each-ref does not crash with -h' ' +test_expect_success "for-each-ref does not crash with -h" ' test_expect_code 129 git for-each-ref -h >usage && test_grep "[Uu]sage: git for-each-ref " usage && test_expect_code 129 nongit git for-each-ref -h >usage && test_grep "[Uu]sage: git for-each-ref " usage ' -test_expect_success 'Check format specifiers are ignored in naming date atoms' ' - git for-each-ref --format="%(authordate)" refs/heads && - git for-each-ref --format="%(authordate:default) %(authordate)" refs/heads && - git for-each-ref --format="%(authordate) %(authordate:default)" refs/heads && - git for-each-ref --format="%(authordate:default) %(authordate:default)" refs/heads -' - -test_expect_success 'Check valid format specifiers for date fields' ' - git for-each-ref --format="%(authordate:default)" refs/heads && - git for-each-ref --format="%(authordate:relative)" refs/heads && - git for-each-ref --format="%(authordate:short)" refs/heads && - git for-each-ref --format="%(authordate:local)" refs/heads && - git for-each-ref --format="%(authordate:iso8601)" refs/heads && - git for-each-ref --format="%(authordate:rfc2822)" refs/heads -' - -test_expect_success 'Check invalid format specifiers are errors' ' - test_must_fail git for-each-ref --format="%(authordate:INVALID)" refs/heads -' - -test_expect_success 'arguments to %(objectname:short=) must be positive integers' ' - test_must_fail git for-each-ref --format="%(objectname:short=0)" && - test_must_fail git for-each-ref --format="%(objectname:short=-1)" && - test_must_fail git for-each-ref --format="%(objectname:short=foo)" -' - -test_bad_atom () { - case "$1" in - head) ref=refs/heads/main ;; - tag) ref=refs/tags/testtag ;; - sym) ref=refs/heads/sym ;; - *) ref=$1 ;; - esac - format=$2 - test_do=test_expect_${4:-success} - - printf '%s\n' "$3" >expect - $test_do $PREREQ "err basic atom: $ref $format" ' - test_must_fail git for-each-ref \ - --format="%($format)" "$ref" 2>error && - test_cmp expect error - ' -} - -test_bad_atom head 'authoremail:foo' \ - 'fatal: unrecognized %(authoremail) argument: foo' - -test_bad_atom head 'authoremail:mailmap,trim,bar' \ - 'fatal: unrecognized %(authoremail) argument: bar' - -test_bad_atom head 'authoremail:trim,' \ - 'fatal: unrecognized %(authoremail) argument: ' - -test_bad_atom head 'authoremail:mailmaptrim' \ - 'fatal: unrecognized %(authoremail) argument: trim' - -test_bad_atom head 'committeremail: ' \ - 'fatal: unrecognized %(committeremail) argument: ' - -test_bad_atom head 'committeremail: trim,foo' \ - 'fatal: unrecognized %(committeremail) argument: trim,foo' - -test_bad_atom head 'committeremail:mailmap,localpart ' \ - 'fatal: unrecognized %(committeremail) argument: ' - -test_bad_atom head 'committeremail:trim_localpart' \ - 'fatal: unrecognized %(committeremail) argument: _localpart' - -test_bad_atom head 'committeremail:localpart,,,trim' \ - 'fatal: unrecognized %(committeremail) argument: ,,trim' - -test_bad_atom tag 'taggeremail:mailmap,trim, foo ' \ - 'fatal: unrecognized %(taggeremail) argument: foo ' - -test_bad_atom tag 'taggeremail:trim,localpart,' \ - 'fatal: unrecognized %(taggeremail) argument: ' - -test_bad_atom tag 'taggeremail:mailmap;localpart trim' \ - 'fatal: unrecognized %(taggeremail) argument: ;localpart trim' - -test_bad_atom tag 'taggeremail:localpart trim' \ - 'fatal: unrecognized %(taggeremail) argument: trim' - -test_bad_atom tag 'taggeremail:mailmap,mailmap,trim,qux,localpart,trim' \ - 'fatal: unrecognized %(taggeremail) argument: qux,localpart,trim' - -test_date () { - f=$1 && - committer_date=$2 && - author_date=$3 && - tagger_date=$4 && - cat >expected <<-EOF && - 'refs/heads/main' '$committer_date' '$author_date' - 'refs/tags/testtag' '$tagger_date' - EOF - ( - git for-each-ref --shell \ - --format="%(refname) %(committerdate${f:+:$f}) %(authordate${f:+:$f})" \ - refs/heads && - git for-each-ref --shell \ - --format="%(refname) %(taggerdate${f:+:$f})" \ - refs/tags - ) >actual && - test_cmp expected actual -} - -test_expect_success 'Check unformatted date fields output' ' - test_date "" \ - "Tue Jul 4 01:18:43 2006 +0200" \ - "Tue Jul 4 01:18:44 2006 +0200" \ - "Tue Jul 4 01:18:45 2006 +0200" -' - -test_expect_success 'Check format "default" formatted date fields output' ' - test_date default \ - "Tue Jul 4 01:18:43 2006 +0200" \ - "Tue Jul 4 01:18:44 2006 +0200" \ - "Tue Jul 4 01:18:45 2006 +0200" -' - -test_expect_success 'Check format "default-local" date fields output' ' - test_date default-local "Mon Jul 3 23:18:43 2006" "Mon Jul 3 23:18:44 2006" "Mon Jul 3 23:18:45 2006" -' - -# Don't know how to do relative check because I can't know when this script -# is going to be run and can't fake the current time to git, and hence can't -# provide expected output. Instead, I'll just make sure that "relative" -# doesn't exit in error -test_expect_success 'Check format "relative" date fields output' ' - f=relative && - (git for-each-ref --shell --format="%(refname) %(committerdate:$f) %(authordate:$f)" refs/heads && - git for-each-ref --shell --format="%(refname) %(taggerdate:$f)" refs/tags) >actual -' - -# We just check that this is the same as "relative" for now. -test_expect_success 'Check format "relative-local" date fields output' ' - test_date relative-local \ - "$(git for-each-ref --format="%(committerdate:relative)" refs/heads)" \ - "$(git for-each-ref --format="%(authordate:relative)" refs/heads)" \ - "$(git for-each-ref --format="%(taggerdate:relative)" refs/tags)" -' - -test_expect_success 'Check format "short" date fields output' ' - test_date short 2006-07-04 2006-07-04 2006-07-04 -' - -test_expect_success 'Check format "short-local" date fields output' ' - test_date short-local 2006-07-03 2006-07-03 2006-07-03 -' - -test_expect_success 'Check format "local" date fields output' ' - test_date local \ - "Mon Jul 3 23:18:43 2006" \ - "Mon Jul 3 23:18:44 2006" \ - "Mon Jul 3 23:18:45 2006" -' - -test_expect_success 'Check format "iso8601" date fields output' ' - test_date iso8601 \ - "2006-07-04 01:18:43 +0200" \ - "2006-07-04 01:18:44 +0200" \ - "2006-07-04 01:18:45 +0200" -' - -test_expect_success 'Check format "iso8601-local" date fields output' ' - test_date iso8601-local "2006-07-03 23:18:43 +0000" "2006-07-03 23:18:44 +0000" "2006-07-03 23:18:45 +0000" -' - -test_expect_success 'Check format "rfc2822" date fields output' ' - test_date rfc2822 \ - "Tue, 4 Jul 2006 01:18:43 +0200" \ - "Tue, 4 Jul 2006 01:18:44 +0200" \ - "Tue, 4 Jul 2006 01:18:45 +0200" -' - -test_expect_success 'Check format "rfc2822-local" date fields output' ' - test_date rfc2822-local "Mon, 3 Jul 2006 23:18:43 +0000" "Mon, 3 Jul 2006 23:18:44 +0000" "Mon, 3 Jul 2006 23:18:45 +0000" -' - -test_expect_success 'Check format "raw" date fields output' ' - test_date raw "1151968723 +0200" "1151968724 +0200" "1151968725 +0200" -' - -test_expect_success 'Check format "raw-local" date fields output' ' - test_date raw-local "1151968723 +0000" "1151968724 +0000" "1151968725 +0000" -' - -test_expect_success 'Check format of strftime date fields' ' - echo "my date is 2006-07-04" >expected && - git for-each-ref \ - --format="%(authordate:format:my date is %Y-%m-%d)" \ - refs/heads >actual && - test_cmp expected actual -' - -test_expect_success 'Check format of strftime-local date fields' ' - echo "my date is 2006-07-03" >expected && - git for-each-ref \ - --format="%(authordate:format-local:my date is %Y-%m-%d)" \ - refs/heads >actual && - test_cmp expected actual -' - -test_expect_success 'exercise strftime with odd fields' ' - echo >expected && - git for-each-ref --format="%(authordate:format:)" refs/heads >actual && - test_cmp expected actual && - long="long format -- $ZERO_OID$ZERO_OID$ZERO_OID$ZERO_OID$ZERO_OID$ZERO_OID$ZERO_OID" && - echo $long >expected && - git for-each-ref --format="%(authordate:format:$long)" refs/heads >actual && - test_cmp expected actual -' - -cat >expected <<\EOF -refs/heads/main -refs/remotes/origin/main -refs/tags/testtag -EOF - -test_expect_success 'Verify ascending sort' ' - git for-each-ref --format="%(refname)" --sort=refname >actual && - test_cmp expected actual -' - - -cat >expected <<\EOF -refs/tags/testtag -refs/remotes/origin/main -refs/heads/main -EOF - -test_expect_success 'Verify descending sort' ' - git for-each-ref --format="%(refname)" --sort=-refname >actual && - test_cmp expected actual -' - -test_expect_success 'Give help even with invalid sort atoms' ' - test_expect_code 129 git for-each-ref --sort=bogus -h >actual 2>&1 && - grep "^usage: git for-each-ref" actual -' - -cat >expected <<\EOF -refs/tags/testtag -refs/tags/testtag-2 -EOF - -test_expect_success 'exercise patterns with prefixes' ' - git tag testtag-2 && - test_when_finished "git tag -d testtag-2" && - git for-each-ref --format="%(refname)" \ - refs/tags/testtag refs/tags/testtag-2 >actual && - test_cmp expected actual -' - -cat >expected <<\EOF -refs/tags/testtag -refs/tags/testtag-2 -EOF - -test_expect_success 'exercise glob patterns with prefixes' ' - git tag testtag-2 && - test_when_finished "git tag -d testtag-2" && - git for-each-ref --format="%(refname)" \ - refs/tags/testtag "refs/tags/testtag-*" >actual && - test_cmp expected actual -' - -cat >expected <<\EOF -refs/tags/bar -refs/tags/baz -refs/tags/testtag -EOF - -test_expect_success 'exercise patterns with prefix exclusions' ' - for tag in foo/one foo/two foo/three bar baz - do - git tag "$tag" || return 1 - done && - test_when_finished "git tag -d foo/one foo/two foo/three bar baz" && - git for-each-ref --format="%(refname)" \ - refs/tags/ --exclude=refs/tags/foo >actual && - test_cmp expected actual -' - -cat >expected <<\EOF -refs/tags/bar -refs/tags/baz -refs/tags/foo/one -refs/tags/testtag -EOF - -test_expect_success 'exercise patterns with pattern exclusions' ' - for tag in foo/one foo/two foo/three bar baz - do - git tag "$tag" || return 1 - done && - test_when_finished "git tag -d foo/one foo/two foo/three bar baz" && - git for-each-ref --format="%(refname)" \ - refs/tags/ --exclude="refs/tags/foo/t*" >actual && - test_cmp expected actual -' - -cat >expected <<\EOF -'refs/heads/main' -'refs/remotes/origin/main' -'refs/tags/testtag' -EOF - -test_expect_success 'Quoting style: shell' ' - git for-each-ref --shell --format="%(refname)" >actual && - test_cmp expected actual -' - -test_expect_success 'Quoting style: perl' ' - git for-each-ref --perl --format="%(refname)" >actual && - test_cmp expected actual -' - -test_expect_success 'Quoting style: python' ' - git for-each-ref --python --format="%(refname)" >actual && - test_cmp expected actual -' - -cat >expected <<\EOF -"refs/heads/main" -"refs/remotes/origin/main" -"refs/tags/testtag" -EOF - -test_expect_success 'Quoting style: tcl' ' - git for-each-ref --tcl --format="%(refname)" >actual && - test_cmp expected actual -' - -for i in "--perl --shell" "-s --python" "--python --tcl" "--tcl --perl"; do - test_expect_success "more than one quoting style: $i" " - test_must_fail git for-each-ref $i 2>err && - grep '^error: more than one quoting style' err - " -done - -test_expect_success 'setup for upstream:track[short]' ' - test_commit two -' - -test_atom head upstream:track '[ahead 1]' -test_atom head upstream:trackshort '>' -test_atom head upstream:track,nobracket 'ahead 1' -test_atom head upstream:nobracket,track 'ahead 1' - -test_expect_success 'setup for push:track[short]' ' - test_commit third && - git update-ref refs/remotes/myfork/main main && - git reset main~1 -' - -test_atom head push:track '[behind 1]' -test_atom head push:trackshort '<' - -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 -' - -test_expect_success 'Check that :track[short] works when upstream is invalid' ' - cat >expected <<-\EOF && - [gone] - - EOF - test_when_finished "git config branch.main.merge refs/heads/main" && - git config branch.main.merge refs/heads/does-not-exist && - git for-each-ref \ - --format="%(upstream:track)$LF%(upstream:trackshort)" \ - refs/heads >actual && - test_cmp expected actual -' - -test_expect_success 'Check for invalid refname format' ' - test_must_fail git for-each-ref --format="%(refname:INVALID)" -' - -test_expect_success 'set up color tests' ' - cat >expected.color <<-EOF && - $(git rev-parse --short refs/heads/main) main - $(git rev-parse --short refs/remotes/myfork/main) myfork/main - $(git rev-parse --short refs/remotes/origin/main) origin/main - $(git rev-parse --short refs/tags/testtag) testtag - $(git rev-parse --short refs/tags/third) third - $(git rev-parse --short refs/tags/two) two - EOF - sed "s/<[^>]*>//g" expected.bare && - color_format="%(objectname:short) %(color:green)%(refname:short)" -' - -test_expect_success TTY '%(color) shows color with a tty' ' - test_terminal git for-each-ref --format="$color_format" >actual.raw && - test_decode_color actual && - test_cmp expected.color actual -' - -test_expect_success '%(color) does not show color without tty' ' - TERM=vt100 git for-each-ref --format="$color_format" >actual && - test_cmp expected.bare actual -' - -test_expect_success '--color can override tty check' ' - git for-each-ref --color --format="$color_format" >actual.raw && - test_decode_color actual && - test_cmp expected.color actual -' - -test_expect_success 'color.ui=always does not override tty check' ' - git -c color.ui=always for-each-ref --format="$color_format" >actual && - test_cmp expected.bare actual -' - -test_expect_success 'setup for describe atom tests' ' - git init -b master describe-repo && - ( - cd describe-repo && - - test_commit --no-tag one && - git tag tagone && - - test_commit --no-tag two && - git tag -a -m "tag two" tagtwo - ) -' - -test_expect_success 'describe atom vs git describe' ' - ( - cd describe-repo && - - git for-each-ref --format="%(objectname)" \ - refs/tags/ >obj && - while read hash - do - if desc=$(git describe $hash) - then - : >expect-contains-good - else - : >expect-contains-bad - fi && - echo "$hash $desc" || return 1 - done expect && - test_path_exists expect-contains-good && - test_path_exists expect-contains-bad && - - git for-each-ref --format="%(objectname) %(describe)" \ - refs/tags/ >actual 2>err && - test_cmp expect actual && - test_must_be_empty err - ) -' - -test_expect_success 'describe:tags vs describe --tags' ' - ( - cd describe-repo && - git describe --tags >expect && - git for-each-ref --format="%(describe:tags)" \ - refs/heads/master >actual && - test_cmp expect actual - ) -' - -test_expect_success 'describe:abbrev=... vs describe --abbrev=...' ' - ( - cd describe-repo && - - # Case 1: We have commits between HEAD and the most - # recent tag reachable from it - test_commit --no-tag file && - git describe --abbrev=14 >expect && - git for-each-ref --format="%(describe:abbrev=14)" \ - refs/heads/master >actual && - test_cmp expect actual && - - # Make sure the hash used is at least 14 digits long - sed -e "s/^.*-g\([0-9a-f]*\)$/\1/" hexpart && - test 15 -le $(wc -c expect && - git for-each-ref --format="%(describe:abbrev=14)" \ - refs/heads/master >actual && - test_cmp expect actual && - test tagname = $(cat actual) - ) -' - -test_expect_success 'describe:match=... vs describe --match ...' ' - ( - cd describe-repo && - git tag -a -m "tag foo" tag-foo && - git describe --match "*-foo" >expect && - git for-each-ref --format="%(describe:match="*-foo")" \ - refs/heads/master >actual && - test_cmp expect actual - ) -' - -test_expect_success 'describe:exclude:... vs describe --exclude ...' ' - ( - cd describe-repo && - git tag -a -m "tag bar" tag-bar && - git describe --exclude "*-bar" >expect && - git for-each-ref --format="%(describe:exclude="*-bar")" \ - refs/heads/master >actual && - test_cmp expect actual - ) -' - -test_expect_success 'deref with describe atom' ' - ( - cd describe-repo && - cat >expect <<-\EOF && - - tagname - tagname - tagname - - tagtwo - EOF - git for-each-ref --format="%(*describe)" >actual && - test_cmp expect actual - ) -' - -test_expect_success 'err on bad describe atom arg' ' - ( - cd describe-repo && - - # The bad arg is the only arg passed to describe atom - cat >expect <<-\EOF && - fatal: unrecognized %(describe) argument: baz - EOF - test_must_fail git for-each-ref --format="%(describe:baz)" \ - refs/heads/master 2>actual && - test_cmp expect actual && - - # The bad arg is in the middle of the option string - # passed to the describe atom - cat >expect <<-\EOF && - fatal: unrecognized %(describe) argument: qux=1,abbrev=14 - EOF - test_must_fail git for-each-ref \ - --format="%(describe:tags,qux=1,abbrev=14)" \ - ref/heads/master 2>actual && - test_cmp expect actual - ) -' - -cat >expected <<\EOF -heads/main -tags/main -EOF - -test_expect_success 'Check ambiguous head and tag refs (strict)' ' - git config --bool core.warnambiguousrefs true && - git checkout -b newtag && - echo "Using $datestamp" > one && - git add one && - git commit -m "Branch" && - setdate_and_increment && - git tag -m "Tagging at $datestamp" main && - git for-each-ref --format "%(refname:short)" refs/heads/main refs/tags/main >actual && - test_cmp expected actual -' - -cat >expected <<\EOF -heads/main -main -EOF - -test_expect_success 'Check ambiguous head and tag refs (loose)' ' - git config --bool core.warnambiguousrefs false && - git for-each-ref --format "%(refname:short)" refs/heads/main refs/tags/main >actual && - test_cmp expected actual -' - -cat >expected <<\EOF -heads/ambiguous -ambiguous -EOF - -test_expect_success 'Check ambiguous head and tag refs II (loose)' ' - git checkout main && - git tag ambiguous testtag^0 && - git branch ambiguous testtag^0 && - git for-each-ref --format "%(refname:short)" refs/heads/ambiguous refs/tags/ambiguous >actual && - test_cmp expected actual -' - -test_expect_success 'create tag without tagger' ' - git tag -a -m "Broken tag" taggerless && - git tag -f taggerless $(git cat-file tag taggerless | - sed -e "/^tagger /d" | - git hash-object --literally --stdin -w -t tag) -' - -test_atom refs/tags/taggerless type 'commit' -test_atom refs/tags/taggerless tag 'taggerless' -test_atom refs/tags/taggerless tagger '' -test_atom refs/tags/taggerless taggername '' -test_atom refs/tags/taggerless taggeremail '' -test_atom refs/tags/taggerless taggeremail:trim '' -test_atom refs/tags/taggerless taggeremail:localpart '' -test_atom refs/tags/taggerless taggerdate '' -test_atom refs/tags/taggerless committer '' -test_atom refs/tags/taggerless committername '' -test_atom refs/tags/taggerless committeremail '' -test_atom refs/tags/taggerless committeremail:trim '' -test_atom refs/tags/taggerless committeremail:localpart '' -test_atom refs/tags/taggerless committerdate '' -test_atom refs/tags/taggerless subject 'Broken tag' - -test_expect_success 'an unusual tag with an incomplete line' ' - - git tag -m "bogo" bogo && - bogo=$(git cat-file tag bogo) && - bogo=$(printf "%s" "$bogo" | git mktag) && - git tag -f bogo "$bogo" && - git for-each-ref --format "%(body)" refs/tags/bogo - -' - -test_expect_success 'create tag with subject and body content' ' - cat >>msg <<-\EOF && - the subject line - - first body line - second body line - EOF - git tag -F msg subject-body -' -test_atom refs/tags/subject-body subject 'the subject line' -test_atom refs/tags/subject-body subject:sanitize 'the-subject-line' -test_atom refs/tags/subject-body body 'first body line -second body line -' -test_atom refs/tags/subject-body contents 'the subject line - -first body line -second body line -' - -test_expect_success 'create tag with multiline subject' ' - cat >msg <<-\EOF && - first subject line - second subject line - - first body line - second body line - EOF - git tag -F msg multiline -' -test_atom refs/tags/multiline subject 'first subject line second subject line' -test_atom refs/tags/multiline subject:sanitize 'first-subject-line-second-subject-line' -test_atom refs/tags/multiline contents:subject 'first subject line second subject line' -test_atom refs/tags/multiline body 'first body line -second body line -' -test_atom refs/tags/multiline contents:body 'first body line -second body line -' -test_atom refs/tags/multiline contents:signature '' -test_atom refs/tags/multiline contents 'first subject line -second subject line - -first body line -second body line -' - -test_expect_success GPG 'create signed tags' ' - git tag -s -m "" signed-empty && - git tag -s -m "subject line" signed-short && - cat >msg <<-\EOF && - subject line - - body contents - EOF - git tag -s -F msg signed-long -' - -sig='-----BEGIN PGP SIGNATURE----- ------END PGP SIGNATURE----- -' - -PREREQ=GPG -test_atom refs/tags/signed-empty subject '' -test_atom refs/tags/signed-empty subject:sanitize '' -test_atom refs/tags/signed-empty contents:subject '' -test_atom refs/tags/signed-empty body "$sig" -test_atom refs/tags/signed-empty contents:body '' -test_atom refs/tags/signed-empty contents:signature "$sig" -test_atom refs/tags/signed-empty contents "$sig" - -test_expect_success GPG 'basic atom: refs/tags/signed-empty raw' ' - git cat-file tag refs/tags/signed-empty >expected && - git for-each-ref --format="%(raw)" refs/tags/signed-empty >actual && - sanitize_pgp expected.clean && - echo >>expected.clean && - sanitize_pgp actual.clean && - test_cmp expected.clean actual.clean -' - -test_atom refs/tags/signed-short subject 'subject line' -test_atom refs/tags/signed-short subject:sanitize 'subject-line' -test_atom refs/tags/signed-short contents:subject 'subject line' -test_atom refs/tags/signed-short body "$sig" -test_atom refs/tags/signed-short contents:body '' -test_atom refs/tags/signed-short contents:signature "$sig" -test_atom refs/tags/signed-short contents "subject line -$sig" - -test_expect_success GPG 'basic atom: refs/tags/signed-short raw' ' - git cat-file tag refs/tags/signed-short >expected && - git for-each-ref --format="%(raw)" refs/tags/signed-short >actual && - sanitize_pgp expected.clean && - echo >>expected.clean && - sanitize_pgp actual.clean && - test_cmp expected.clean actual.clean -' - -test_atom refs/tags/signed-long subject 'subject line' -test_atom refs/tags/signed-long subject:sanitize 'subject-line' -test_atom refs/tags/signed-long contents:subject 'subject line' -test_atom refs/tags/signed-long body "body contents -$sig" -test_atom refs/tags/signed-long contents:body 'body contents -' -test_atom refs/tags/signed-long contents:signature "$sig" -test_atom refs/tags/signed-long contents "subject line - -body contents -$sig" - -test_expect_success GPG 'basic atom: refs/tags/signed-long raw' ' - git cat-file tag refs/tags/signed-long >expected && - git for-each-ref --format="%(raw)" refs/tags/signed-long >actual && - sanitize_pgp expected.clean && - echo >>expected.clean && - sanitize_pgp actual.clean && - test_cmp expected.clean actual.clean -' - -test_expect_success 'set up refs pointing to tree and blob' ' - git update-ref refs/mytrees/first refs/heads/main^{tree} && - git update-ref refs/myblobs/first refs/heads/main:one -' - -test_atom refs/mytrees/first subject "" -test_atom refs/mytrees/first contents:subject "" -test_atom refs/mytrees/first body "" -test_atom refs/mytrees/first contents:body "" -test_atom refs/mytrees/first contents:signature "" -test_atom refs/mytrees/first contents "" - -test_expect_success 'basic atom: refs/mytrees/first raw' ' - git cat-file tree refs/mytrees/first >expected && - echo >>expected && - git for-each-ref --format="%(raw)" refs/mytrees/first >actual && - test_cmp expected actual && - git cat-file -s refs/mytrees/first >expected && - git for-each-ref --format="%(raw:size)" refs/mytrees/first >actual && - test_cmp expected actual -' - -test_atom refs/myblobs/first subject "" -test_atom refs/myblobs/first contents:subject "" -test_atom refs/myblobs/first body "" -test_atom refs/myblobs/first contents:body "" -test_atom refs/myblobs/first contents:signature "" -test_atom refs/myblobs/first contents "" - -test_expect_success 'basic atom: refs/myblobs/first raw' ' - git cat-file blob refs/myblobs/first >expected && - echo >>expected && - git for-each-ref --format="%(raw)" refs/myblobs/first >actual && - test_cmp expected actual && - git cat-file -s refs/myblobs/first >expected && - git for-each-ref --format="%(raw:size)" refs/myblobs/first >actual && - test_cmp expected actual -' - -test_expect_success 'set up refs pointing to binary blob' ' - printf "a\0b\0c" >blob1 && - printf "a\0c\0b" >blob2 && - printf "\0a\0b\0c" >blob3 && - printf "abc" >blob4 && - printf "\0 \0 \0 " >blob5 && - printf "\0 \0a\0 " >blob6 && - printf " " >blob7 && - >blob8 && - obj=$(git hash-object -w blob1) && - git update-ref refs/myblobs/blob1 "$obj" && - obj=$(git hash-object -w blob2) && - git update-ref refs/myblobs/blob2 "$obj" && - obj=$(git hash-object -w blob3) && - git update-ref refs/myblobs/blob3 "$obj" && - obj=$(git hash-object -w blob4) && - git update-ref refs/myblobs/blob4 "$obj" && - obj=$(git hash-object -w blob5) && - git update-ref refs/myblobs/blob5 "$obj" && - obj=$(git hash-object -w blob6) && - git update-ref refs/myblobs/blob6 "$obj" && - obj=$(git hash-object -w blob7) && - git update-ref refs/myblobs/blob7 "$obj" && - obj=$(git hash-object -w blob8) && - git update-ref refs/myblobs/blob8 "$obj" -' - -test_expect_success 'Verify sorts with raw' ' - cat >expected <<-EOF && - refs/myblobs/blob8 - refs/myblobs/blob5 - refs/myblobs/blob6 - refs/myblobs/blob3 - refs/myblobs/blob7 - refs/mytrees/first - refs/myblobs/first - refs/myblobs/blob1 - refs/myblobs/blob2 - refs/myblobs/blob4 - refs/heads/main - EOF - git for-each-ref --format="%(refname)" --sort=raw \ - refs/heads/main refs/myblobs/ refs/mytrees/first >actual && - test_cmp expected actual -' - -test_expect_success 'Verify sorts with raw:size' ' - cat >expected <<-EOF && - refs/myblobs/blob8 - refs/myblobs/blob7 - refs/myblobs/blob4 - refs/myblobs/blob1 - refs/myblobs/blob2 - refs/myblobs/blob3 - refs/myblobs/blob5 - refs/myblobs/blob6 - refs/myblobs/first - refs/mytrees/first - refs/heads/main - EOF - git for-each-ref --format="%(refname)" --sort=raw:size \ - refs/heads/main refs/myblobs/ refs/mytrees/first >actual && - test_cmp expected actual -' - -test_expect_success 'validate raw atom with %(if:equals)' ' - cat >expected <<-EOF && - not equals - not equals - not equals - not equals - not equals - not equals - refs/myblobs/blob4 - not equals - not equals - not equals - not equals - not equals - EOF - git for-each-ref --format="%(if:equals=abc)%(raw)%(then)%(refname)%(else)not equals%(end)" \ - refs/myblobs/ refs/heads/ >actual && - test_cmp expected actual -' - -test_expect_success 'validate raw atom with %(if:notequals)' ' - cat >expected <<-EOF && - refs/heads/ambiguous - refs/heads/main - refs/heads/newtag - refs/myblobs/blob1 - refs/myblobs/blob2 - refs/myblobs/blob3 - equals - refs/myblobs/blob5 - refs/myblobs/blob6 - refs/myblobs/blob7 - refs/myblobs/blob8 - refs/myblobs/first - EOF - git for-each-ref --format="%(if:notequals=abc)%(raw)%(then)%(refname)%(else)equals%(end)" \ - refs/myblobs/ refs/heads/ >actual && - test_cmp expected actual -' - -test_expect_success 'empty raw refs with %(if)' ' - cat >expected <<-EOF && - refs/myblobs/blob1 not empty - refs/myblobs/blob2 not empty - refs/myblobs/blob3 not empty - refs/myblobs/blob4 not empty - refs/myblobs/blob5 not empty - refs/myblobs/blob6 not empty - refs/myblobs/blob7 empty - refs/myblobs/blob8 empty - refs/myblobs/first not empty - EOF - git for-each-ref --format="%(refname) %(if)%(raw)%(then)not empty%(else)empty%(end)" \ - refs/myblobs/ >actual && - test_cmp expected actual -' - -test_expect_success '%(raw) with --python must fail' ' - test_must_fail git for-each-ref --format="%(raw)" --python -' - -test_expect_success '%(raw) with --tcl must fail' ' - test_must_fail git for-each-ref --format="%(raw)" --tcl -' - -test_expect_success PERL_TEST_HELPERS '%(raw) with --perl' ' - git for-each-ref --format="\$name= %(raw); -print \"\$name\"" refs/myblobs/blob1 --perl | perl >actual && - cmp blob1 actual && - git for-each-ref --format="\$name= %(raw); -print \"\$name\"" refs/myblobs/blob3 --perl | perl >actual && - cmp blob3 actual && - git for-each-ref --format="\$name= %(raw); -print \"\$name\"" refs/myblobs/blob8 --perl | perl >actual && - cmp blob8 actual && - git for-each-ref --format="\$name= %(raw); -print \"\$name\"" refs/myblobs/first --perl | perl >actual && - cmp one actual && - git cat-file tree refs/mytrees/first > expected && - git for-each-ref --format="\$name= %(raw); -print \"\$name\"" refs/mytrees/first --perl | perl >actual && - cmp expected actual -' - -test_expect_success '%(raw) with --shell must fail' ' - test_must_fail git for-each-ref --format="%(raw)" --shell -' - -test_expect_success '%(raw) with --shell and --sort=raw must fail' ' - test_must_fail git for-each-ref --format="%(raw)" --sort=raw --shell -' - -test_expect_success '%(raw:size) with --shell' ' - git for-each-ref --format="%(raw:size)" | sed "s/^/$SQ/;s/$/$SQ/" >expect && - git for-each-ref --format="%(raw:size)" --shell >actual && - test_cmp expect actual -' - -test_expect_success 'for-each-ref --format compare with cat-file --batch' ' - git rev-parse refs/mytrees/first | git cat-file --batch >expected && - git for-each-ref --format="%(objectname) %(objecttype) %(objectsize) -%(raw)" refs/mytrees/first >actual && - test_cmp expected actual -' - -test_expect_success 'verify sorts with contents:size' ' - cat >expect <<-\EOF && - refs/heads/main - refs/heads/newtag - refs/heads/ambiguous - EOF - git for-each-ref --format="%(refname)" \ - --sort=contents:size refs/heads/ >actual && - test_cmp expect actual -' - -test_expect_success 'set up multiple-sort tags' ' - for when in 100000 200000 - do - for email in user1 user2 - do - for ref in ref1 ref2 - do - GIT_COMMITTER_DATE="@$when +0000" \ - GIT_COMMITTER_EMAIL="$email@example.com" \ - git tag -m "tag $ref-$when-$email" \ - multi-$ref-$when-$email || return 1 - done - done - done -' - -test_expect_success 'Verify sort with multiple keys' ' - cat >expected <<-\EOF && - 100000 refs/tags/multi-ref2-100000-user1 - 100000 refs/tags/multi-ref1-100000-user1 - 100000 refs/tags/multi-ref2-100000-user2 - 100000 refs/tags/multi-ref1-100000-user2 - 200000 refs/tags/multi-ref2-200000-user1 - 200000 refs/tags/multi-ref1-200000-user1 - 200000 refs/tags/multi-ref2-200000-user2 - 200000 refs/tags/multi-ref1-200000-user2 - EOF - git for-each-ref \ - --format="%(taggerdate:unix) %(taggeremail) %(refname)" \ - --sort=-refname \ - --sort=taggeremail \ - --sort=taggerdate \ - "refs/tags/multi-*" >actual && - test_cmp expected actual -' - -test_expect_success 'equivalent sorts fall back on refname' ' - cat >expected <<-\EOF && - 100000 refs/tags/multi-ref1-100000-user1 - 100000 refs/tags/multi-ref1-100000-user2 - 100000 refs/tags/multi-ref2-100000-user1 - 100000 refs/tags/multi-ref2-100000-user2 - 200000 refs/tags/multi-ref1-200000-user1 - 200000 refs/tags/multi-ref1-200000-user2 - 200000 refs/tags/multi-ref2-200000-user1 - 200000 refs/tags/multi-ref2-200000-user2 - EOF - git for-each-ref \ - --format="%(taggerdate:unix) %(taggeremail) %(refname)" \ - --sort=taggerdate \ - "refs/tags/multi-*" >actual && - test_cmp expected actual -' - -test_expect_success '--no-sort cancels the previous sort keys' ' - cat >expected <<-\EOF && - 100000 refs/tags/multi-ref1-100000-user1 - 100000 refs/tags/multi-ref1-100000-user2 - 100000 refs/tags/multi-ref2-100000-user1 - 100000 refs/tags/multi-ref2-100000-user2 - 200000 refs/tags/multi-ref1-200000-user1 - 200000 refs/tags/multi-ref1-200000-user2 - 200000 refs/tags/multi-ref2-200000-user1 - 200000 refs/tags/multi-ref2-200000-user2 - EOF - git for-each-ref \ - --format="%(taggerdate:unix) %(taggeremail) %(refname)" \ - --sort=-refname \ - --sort=taggeremail \ - --no-sort \ - --sort=taggerdate \ - "refs/tags/multi-*" >actual && - test_cmp expected actual -' - -test_expect_success '--no-sort without subsequent --sort prints expected refs' ' - cat >expected <<-\EOF && - refs/tags/multi-ref1-100000-user1 - refs/tags/multi-ref1-100000-user2 - refs/tags/multi-ref1-200000-user1 - refs/tags/multi-ref1-200000-user2 - refs/tags/multi-ref2-100000-user1 - refs/tags/multi-ref2-100000-user2 - refs/tags/multi-ref2-200000-user1 - refs/tags/multi-ref2-200000-user2 - EOF - - # Sort the results with `sort` for a consistent comparison against - # expected - git for-each-ref \ - --format="%(refname)" \ - --no-sort \ - "refs/tags/multi-*" | sort >actual && - test_cmp expected actual -' - -test_expect_success 'set up custom date sorting' ' - # Dates: - # - Wed Feb 07 2024 21:34:20 +0000 - # - Tue Dec 14 1999 00:05:22 +0000 - # - Fri Jun 04 2021 11:26:51 +0000 - # - Mon Jan 22 2007 16:44:01 GMT+0000 - i=1 && - for when in 1707341660 945129922 1622806011 1169484241 - do - GIT_COMMITTER_DATE="@$when +0000" \ - GIT_COMMITTER_EMAIL="user@example.com" \ - git tag -m "tag $when" custom-dates-$i && - i=$(($i+1)) || return 1 - done -' - -test_expect_success 'sort by date defaults to full timestamp' ' - cat >expected <<-\EOF && - 945129922 refs/tags/custom-dates-2 - 1169484241 refs/tags/custom-dates-4 - 1622806011 refs/tags/custom-dates-3 - 1707341660 refs/tags/custom-dates-1 - EOF - - git for-each-ref \ - --format="%(creatordate:unix) %(refname)" \ - --sort=creatordate \ - "refs/tags/custom-dates-*" >actual && - test_cmp expected actual -' - -test_expect_success 'sort by custom date format' ' - cat >expected <<-\EOF && - 00:05:22 refs/tags/custom-dates-2 - 11:26:51 refs/tags/custom-dates-3 - 16:44:01 refs/tags/custom-dates-4 - 21:34:20 refs/tags/custom-dates-1 - EOF - - git for-each-ref \ - --format="%(creatordate:format:%H:%M:%S) %(refname)" \ - --sort="creatordate:format:%H:%M:%S" \ - "refs/tags/custom-dates-*" >actual && - test_cmp expected actual -' - -test_expect_success 'do not dereference NULL upon %(HEAD) on unborn branch' ' - test_when_finished "git checkout main" && - git for-each-ref --format="%(HEAD) %(refname:short)" refs/heads/ >actual && - sed -e "s/^\* / /" actual >expect && - git checkout --orphan orphaned-branch && - git for-each-ref --format="%(HEAD) %(refname:short)" refs/heads/ >actual && - test_cmp expect actual -' - -cat >trailers < -Signed-off-by: A U Thor -[ v2 updated patch description ] -Acked-by: A U Thor - -EOF - -unfold () { - perl -0pe 's/\n\s+/ /g' -} - -test_expect_success 'set up trailers for next test' ' - echo "Some contents" > two && - git add two && - git commit -F - <<-EOF - trailers: this commit message has trailers - - Some message contents - - $(cat trailers) - EOF -' - -test_trailer_option () { - if test "$#" -eq 3 - then - prereq="$1" - shift - fi && - title=$1 option=$2 - cat >expect - test_expect_success $prereq "$title" ' - git for-each-ref --format="%($option)" refs/heads/main >actual && - test_cmp expect actual && - git for-each-ref --format="%(contents:$option)" refs/heads/main >actual && - test_cmp expect actual - ' -} - -test_trailer_option PERL_TEST_HELPERS '%(trailers:unfold) unfolds trailers' \ - 'trailers:unfold' <<-EOF - $(unfold - - EOF - -test_trailer_option '%(trailers:key=foo) is case insensitive' \ - 'trailers:key=SiGned-oFf-bY' <<-EOF - Signed-off-by: A U Thor - - EOF - -test_trailer_option '%(trailers:key=foo:) trailing colon also works' \ - 'trailers:key=Signed-off-by:' <<-EOF - Signed-off-by: A U Thor - - EOF - -test_trailer_option '%(trailers:key=foo) multiple keys' \ - 'trailers:key=Reviewed-by:,key=Signed-off-by' <<-EOF - Reviewed-by: A U Thor - Signed-off-by: A U Thor - - EOF - -test_trailer_option '%(trailers:key=nonexistent) becomes empty' \ - 'trailers:key=Shined-off-by:' <<-EOF - - EOF - -test_trailer_option '%(trailers:key=foo) handles multiple lines even if folded' \ - 'trailers:key=Acked-by' <<-EOF - $(grep -v patch.description - $(grep patch.description - - EOF - -test_trailer_option '%(trailers:separator) changes separator' \ - 'trailers:separator=%x2C,key=Reviewed-by,key=Signed-off-by:' <<-EOF - Reviewed-by: A U Thor ,Signed-off-by: A U Thor - EOF - -test_trailer_option '%(trailers:key_value_separator) changes key-value separator' \ - 'trailers:key_value_separator=%x2C,key=Reviewed-by,key=Signed-off-by:' <<-EOF - Reviewed-by,A U Thor - Signed-off-by,A U Thor - - EOF - -test_trailer_option '%(trailers:separator,key_value_separator) changes both separators' \ - 'trailers:separator=%x2C,key_value_separator=%x2C,key=Reviewed-by,key=Signed-off-by:' <<-EOF - Reviewed-by,A U Thor ,Signed-off-by,A U Thor - EOF - -test_expect_success 'multiple %(trailers) use their own options' ' - git tag -F - tag-with-trailers <<-\EOF && - body - - one: foo - one: bar - two: baz - two: qux - EOF - t1="%(trailers:key=one,key_value_separator=W,separator=X)" && - t2="%(trailers:key=two,key_value_separator=Y,separator=Z)" && - git for-each-ref --format="$t1%0a$t2" refs/tags/tag-with-trailers >actual && - cat >expect <<-\EOF && - oneWfooXoneWbar - twoYbazZtwoYqux - EOF - test_cmp expect actual -' - -test_failing_trailer_option () { - title=$1 option=$2 - cat >expect - test_expect_success "$title" ' - # error message cannot be checked under i18n - test_must_fail git for-each-ref --format="%($option)" refs/heads/main 2>actual && - test_cmp expect actual && - test_must_fail git for-each-ref --format="%(contents:$option)" refs/heads/main 2>actual && - test_cmp expect actual - ' -} - -test_failing_trailer_option '%(trailers) rejects unknown trailers arguments' \ - 'trailers:unsupported' <<-\EOF - fatal: unknown %(trailers) argument: unsupported - EOF - -test_failing_trailer_option '%(trailers:key) without value is error' \ - 'trailers:key' <<-\EOF - fatal: expected %(trailers:key=) - EOF - -test_expect_success 'if arguments, %(contents:trailers) shows error if colon is missing' ' - cat >expect <<-EOF && - fatal: unrecognized %(contents) argument: trailersonly - EOF - test_must_fail git for-each-ref --format="%(contents:trailersonly)" 2>actual && - test_cmp expect actual -' - -test_expect_success 'basic atom: head contents:trailers' ' - git for-each-ref --format="%(contents:trailers)" refs/heads/main >actual && - sanitize_pgp actual.clean && - # git for-each-ref ends with a blank line - cat >expect <<-EOF && - $(cat trailers) - - EOF - test_cmp expect actual.clean -' - -test_expect_success 'basic atom: rest must fail' ' - test_must_fail git for-each-ref --format="%(rest)" refs/heads/main -' - -test_expect_success 'HEAD atom does not take arguments' ' - test_must_fail git for-each-ref --format="%(HEAD:foo)" 2>err && - echo "fatal: %(HEAD) does not take arguments" >expect && - test_cmp expect err -' - -test_expect_success 'subject atom rejects unknown arguments' ' - test_must_fail git for-each-ref --format="%(subject:foo)" 2>err && - echo "fatal: unrecognized %(subject) argument: foo" >expect && - test_cmp expect err -' - -test_expect_success 'refname atom rejects unknown arguments' ' - test_must_fail git for-each-ref --format="%(refname:foo)" 2>err && - echo "fatal: unrecognized %(refname) argument: foo" >expect && - test_cmp expect err -' - -test_expect_success 'trailer parsing not fooled by --- line' ' - git commit --allow-empty -F - <<-\EOF && - this is the subject - - This is the body. The message has a "---" line which would confuse a - message+patch parser. But here we know we have only a commit message, - so we get it right. - - trailer: wrong - --- - This is more body. - - trailer: right - EOF - - { - echo "trailer: right" && - echo - } >expect && - git for-each-ref --format="%(trailers)" refs/heads/main >actual && - test_cmp expect actual -' - -test_expect_success 'Add symbolic ref for the following tests' ' - git symbolic-ref refs/heads/sym refs/heads/main -' - -cat >expected <actual && - test_cmp expected actual -' - -cat >expected <actual && - test_cmp expected actual -' - -cat >expected < actual && - git for-each-ref --format="%(symref:lstrip=-2)" refs/heads/sym >> actual && - test_cmp expected actual && - - git for-each-ref --format="%(symref:strip=2)" refs/heads/sym > actual && - git for-each-ref --format="%(symref:strip=-2)" refs/heads/sym >> actual && - test_cmp expected actual -' - -cat >expected < actual && - git for-each-ref --format="%(symref:rstrip=-2)" refs/heads/sym >> actual && - test_cmp expected actual -' - -test_expect_success ':remotename and :remoteref' ' - git init remote-tests && - ( - cd remote-tests && - test_commit initial && - git branch -M main && - git remote add from fifth.coffee:blub && - git config branch.main.remote from && - git config branch.main.merge refs/heads/stable && - git remote add to southridge.audio:repo && - git config remote.to.push "refs/heads/*:refs/heads/pushed/*" && - git config branch.main.pushRemote to && - for pair in "%(upstream)=refs/remotes/from/stable" \ - "%(upstream:remotename)=from" \ - "%(upstream:remoteref)=refs/heads/stable" \ - "%(push)=refs/remotes/to/pushed/main" \ - "%(push:remotename)=to" \ - "%(push:remoteref)=refs/heads/pushed/main" - do - echo "${pair#*=}" >expect && - git for-each-ref --format="${pair%=*}" \ - refs/heads/main >actual && - test_cmp expect actual || exit 1 - done && - git branch push-simple && - git config branch.push-simple.pushRemote from && - actual="$(git for-each-ref \ - --format="%(push:remotename),%(push:remoteref)" \ - refs/heads/push-simple)" && - test from, = "$actual" - ) -' - -test_expect_success 'for-each-ref --ignore-case ignores case' ' - git for-each-ref --format="%(refname)" refs/heads/MAIN >actual && - test_must_be_empty actual && - - echo refs/heads/main >expect && - git for-each-ref --format="%(refname)" --ignore-case \ - refs/heads/MAIN >actual && - test_cmp expect actual -' - -test_expect_success 'for-each-ref --omit-empty works' ' - git for-each-ref --format="%(refname)" >actual && - test_line_count -gt 1 actual && - git for-each-ref --format="%(if:equals=refs/heads/main)%(refname)%(then)%(refname)%(end)" --omit-empty >actual && - echo refs/heads/main >expect && - test_cmp expect actual -' - -test_expect_success 'for-each-ref --ignore-case works on multiple sort keys' ' - # name refs numerically to avoid case-insensitive filesystem conflicts - nr=0 && - for email in a A b B - do - for subject in a A b B - do - GIT_COMMITTER_EMAIL="$email@example.com" \ - git tag -m "tag $subject" icase-$(printf %02d $nr) && - nr=$((nr+1))|| - return 1 - done - done && - git for-each-ref --ignore-case \ - --format="%(taggeremail) %(subject) %(refname)" \ - --sort=refname \ - --sort=subject \ - --sort=taggeremail \ - refs/tags/icase-* >actual && - cat >expect <<-\EOF && - tag a refs/tags/icase-00 - tag A refs/tags/icase-01 - tag a refs/tags/icase-04 - tag A refs/tags/icase-05 - tag b refs/tags/icase-02 - tag B refs/tags/icase-03 - tag b refs/tags/icase-06 - tag B refs/tags/icase-07 - tag a refs/tags/icase-08 - tag A refs/tags/icase-09 - tag a refs/tags/icase-12 - tag A refs/tags/icase-13 - tag b refs/tags/icase-10 - tag B refs/tags/icase-11 - tag b refs/tags/icase-14 - tag B refs/tags/icase-15 - EOF - test_cmp expect actual -' - -test_expect_success 'for-each-ref reports broken tags' ' - git tag -m "good tag" broken-tag-good HEAD && - git cat-file tag broken-tag-good >good && - sed s/commit/blob/ bad && - bad=$(git hash-object -w -t tag bad) && - git update-ref refs/tags/broken-tag-bad $bad && - test_must_fail git for-each-ref --format="%(*objectname)" \ - refs/tags/broken-tag-* -' - -test_expect_success 'set up tag with signature and no blank lines' ' - git tag -F - fake-sig-no-blanks <<-\EOF - this is the subject - -----BEGIN PGP SIGNATURE----- - not a real signature, but we just care about the - subject/body parsing. It is important here that - there are no blank lines in the signature. - -----END PGP SIGNATURE----- - EOF -' - -test_atom refs/tags/fake-sig-no-blanks contents:subject 'this is the subject' -test_atom refs/tags/fake-sig-no-blanks contents:body '' -test_atom refs/tags/fake-sig-no-blanks contents:signature "$sig" - -test_expect_success 'set up tag with CRLF signature' ' - append_cr <<-\EOF | - this is the subject - -----BEGIN PGP SIGNATURE----- - - not a real signature, but we just care about - the subject/body parsing. It is important here - that there is a blank line separating this - from the signature header. - -----END PGP SIGNATURE----- - EOF - git tag -F - --cleanup=verbatim fake-sig-crlf -' - -test_atom refs/tags/fake-sig-crlf contents:subject 'this is the subject' -test_atom refs/tags/fake-sig-crlf contents:body '' - -# CRLF is retained in the signature, so we have to pass our expected value -# through append_cr. But test_atom requires a shell string, which means command -# substitution, and the shell will strip trailing newlines from the output of -# the substitution. Hack around it by adding and then removing a dummy line. -sig_crlf="$(printf "%s" "$sig" | append_cr; echo dummy)" -sig_crlf=${sig_crlf%dummy} -test_atom refs/tags/fake-sig-crlf contents:signature "$sig_crlf" - -test_expect_success 'set up tag with signature and trailers' ' - git tag -F - fake-sig-trailer <<-\EOF - this is the subject - - this is the body - - My-Trailer: foo - -----BEGIN PGP SIGNATURE----- - - not a real signature, but we just care about the - subject/body/trailer parsing. - -----END PGP SIGNATURE----- - EOF -' - -# use "separator=" here to suppress the terminating newline -test_atom refs/tags/fake-sig-trailer trailers:separator= 'My-Trailer: foo' - -test_expect_success 'git for-each-ref --stdin: empty' ' - >in && - git for-each-ref --format="%(refname)" --stdin actual && - git for-each-ref --format="%(refname)" >expect && - test_cmp expect actual -' - -test_expect_success 'git for-each-ref --stdin: fails if extra args' ' - >in && - test_must_fail git for-each-ref --format="%(refname)" \ - --stdin refs/heads/extra err && - grep "unknown arguments supplied with --stdin" err -' - -test_expect_success 'git for-each-ref --stdin: matches' ' - cat >in <<-EOF && - refs/tags/multi* - refs/heads/amb* - EOF - - cat >expect <<-EOF && - refs/heads/ambiguous - refs/tags/multi-ref1-100000-user1 - refs/tags/multi-ref1-100000-user2 - refs/tags/multi-ref1-200000-user1 - refs/tags/multi-ref1-200000-user2 - refs/tags/multi-ref2-100000-user1 - refs/tags/multi-ref2-100000-user2 - refs/tags/multi-ref2-200000-user1 - refs/tags/multi-ref2-200000-user2 - refs/tags/multiline - EOF - - git for-each-ref --format="%(refname)" --stdin actual && - test_cmp expect actual -' - -test_expect_success 'git for-each-ref with non-existing refs' ' - cat >in <<-EOF && - refs/heads/this-ref-does-not-exist - refs/tags/bogus - EOF - - git for-each-ref --format="%(refname)" --stdin actual && - test_must_be_empty actual && - - xargs git for-each-ref --format="%(refname)" actual && - test_must_be_empty actual -' - -test_expect_success 'git for-each-ref with nested tags' ' - git tag -am "Normal tag" nested/base HEAD && - git tag -am "Nested tag" nested/nest1 refs/tags/nested/base && - git tag -am "Double nested tag" nested/nest2 refs/tags/nested/nest1 && - - head_oid="$(git rev-parse HEAD)" && - base_tag_oid="$(git rev-parse refs/tags/nested/base)" && - nest1_tag_oid="$(git rev-parse refs/tags/nested/nest1)" && - nest2_tag_oid="$(git rev-parse refs/tags/nested/nest2)" && - - cat >expect <<-EOF && - refs/tags/nested/base $base_tag_oid tag $head_oid commit - refs/tags/nested/nest1 $nest1_tag_oid tag $head_oid commit - refs/tags/nested/nest2 $nest2_tag_oid tag $head_oid commit - EOF - - git for-each-ref \ - --format="%(refname) %(objectname) %(objecttype) %(*objectname) %(*objecttype)" \ - refs/tags/nested/ >actual && - test_cmp expect actual -' - -test_expect_success 'is-base atom with non-commits' ' - git for-each-ref --format="%(is-base:HEAD) %(refname)" >out 2>err && - grep "(HEAD) refs/heads/main" out && - - test_line_count = 2 err && - grep "error: object .* is a commit, not a blob" err && - grep "error: bad tag pointer to" err -' - -GRADE_FORMAT="%(signature:grade)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)" -TRUSTLEVEL_FORMAT="%(signature:trustlevel)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)" - -test_expect_success GPG 'setup for signature atom using gpg' ' - git checkout -b signed && - - test_when_finished "test_unconfig commit.gpgSign" && - - echo "1" >file && - git add file && - test_tick && - git commit -S -m "file: 1" && - git tag first-signed && - - echo "2" >file && - test_tick && - git commit -a -m "file: 2" && - git tag second-unsigned && - - git config commit.gpgSign 1 && - echo "3" >file && - test_tick && - git commit -a --no-gpg-sign -m "file: 3" && - git tag third-unsigned && - - test_tick && - git rebase -f HEAD^^ && git tag second-signed HEAD^ && - git tag third-signed && - - echo "4" >file && - test_tick && - git commit -a -SB7227189 -m "file: 4" && - git tag fourth-signed && - - echo "5" >file && - test_tick && - git commit -a --no-gpg-sign -m "file: 5" && - git tag fifth-unsigned && - - echo "6" >file && - test_tick && - git commit -a --no-gpg-sign -m "file: 6" && - - test_tick && - git rebase -f HEAD^^ && - git tag fifth-signed HEAD^ && - git tag sixth-signed && - - echo "7" >file && - test_tick && - git commit -a --no-gpg-sign -m "file: 7" && - git tag seventh-unsigned -' - -test_expect_success GPGSSH 'setup for signature atom using ssh' ' - test_when_finished "test_unconfig gpg.format user.signingkey" && - - test_config gpg.format ssh && - test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" && - echo "8" >file && - test_tick && - git add file && - git commit -S -m "file: 8" && - git tag eighth-signed-ssh -' - -test_expect_success GPG2 'bare signature atom' ' - git verify-commit first-signed 2>expect && - echo >>expect && - git for-each-ref refs/tags/first-signed \ - --format="%(signature)" >actual && - test_cmp expect actual -' - -test_expect_success GPG 'show good signature with custom format' ' - git verify-commit first-signed && - cat >expect <<-\EOF && - G - 13B6F51ECDDE430D - C O Mitter - 73D758744BE721698EC54E8713B6F51ECDDE430D - 73D758744BE721698EC54E8713B6F51ECDDE430D - EOF - git for-each-ref refs/tags/first-signed \ - --format="$GRADE_FORMAT" >actual && - test_cmp expect actual -' -test_expect_success GPGSSH 'show good signature with custom format with ssh' ' - test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && - FINGERPRINT=$(ssh-keygen -lf "${GPGSSH_KEY_PRIMARY}" | awk "{print \$2;}") && - cat >expect.tmpl <<-\EOF && - G - FINGERPRINT - principal with number 1 - FINGERPRINT - - EOF - sed "s|FINGERPRINT|$FINGERPRINT|g" expect.tmpl >expect && - git for-each-ref refs/tags/eighth-signed-ssh \ - --format="$GRADE_FORMAT" >actual && - test_cmp expect actual -' - -test_expect_success GPG 'signature atom with grade option and bad signature' ' - git cat-file commit third-signed >raw && - sed -e "s/^file: 3/file: 3 forged/" raw >forged1 && - FORGED1=$(git hash-object -w -t commit forged1) && - git update-ref refs/tags/third-signed "$FORGED1" && - test_must_fail git verify-commit "$FORGED1" && - - cat >expect <<-\EOF && - B - 13B6F51ECDDE430D - C O Mitter - - - EOF - git for-each-ref refs/tags/third-signed \ - --format="$GRADE_FORMAT" >actual && - test_cmp expect actual -' - -test_expect_success GPG 'show untrusted signature with custom format' ' - cat >expect <<-\EOF && - U - 65A0EEA02E30CAD7 - Eris Discordia - F8364A59E07FFE9F4D63005A65A0EEA02E30CAD7 - D4BE22311AD3131E5EDA29A461092E85B7227189 - EOF - git for-each-ref refs/tags/fourth-signed \ - --format="$GRADE_FORMAT" >actual && - test_cmp expect actual -' - -test_expect_success GPG 'show untrusted signature with undefined trust level' ' - cat >expect <<-\EOF && - undefined - 65A0EEA02E30CAD7 - Eris Discordia - F8364A59E07FFE9F4D63005A65A0EEA02E30CAD7 - D4BE22311AD3131E5EDA29A461092E85B7227189 - EOF - git for-each-ref refs/tags/fourth-signed \ - --format="$TRUSTLEVEL_FORMAT" >actual && - test_cmp expect actual -' - -test_expect_success GPG 'show untrusted signature with ultimate trust level' ' - cat >expect <<-\EOF && - ultimate - 13B6F51ECDDE430D - C O Mitter - 73D758744BE721698EC54E8713B6F51ECDDE430D - 73D758744BE721698EC54E8713B6F51ECDDE430D - EOF - git for-each-ref refs/tags/sixth-signed \ - --format="$TRUSTLEVEL_FORMAT" >actual && - test_cmp expect actual -' - -test_expect_success GPG 'show unknown signature with custom format' ' - cat >expect <<-\EOF && - E - 13B6F51ECDDE430D - - - - EOF - GNUPGHOME="$GNUPGHOME_NOT_USED" git for-each-ref \ - refs/tags/sixth-signed --format="$GRADE_FORMAT" >actual && - test_cmp expect actual -' - -test_expect_success GPG 'show lack of signature with custom format' ' - cat >expect <<-\EOF && - N - - - - - EOF - git for-each-ref refs/tags/seventh-unsigned \ - --format="$GRADE_FORMAT" >actual && - test_cmp expect actual -' +. "$TEST_DIRECTORY"/for-each-ref-tests.sh test_done -- GitLab From fed66d91c0fce8a1911b24ad79ed9f10d411874e Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:58 +0530 Subject: [PATCH 028/371] t: add test for git refs list subcommand Add a test script, `t/t1461-refs-list.sh`, for the new `git refs list` command. This script acts as a simple driver, leveraging the shared test library created in the preceding commit. It works by overriding the `$git_for_each_ref` variable to "git refs list" and then sourcing the shared library (`t/for-each-ref-tests.sh`). This approach ensures that `git refs list` is tested against the entire comprehensive test suite of `git for-each-ref`, verifying that it acts as a compatible drop-in replacement. Mentored-by: Patrick Steinhardt Mentored-by: shejialuo Mentored-by: Karthik Nayak Signed-off-by: Meet Soni Signed-off-by: Junio C Hamano --- t/meson.build | 1 + t/t1461-refs-list.sh | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100755 t/t1461-refs-list.sh diff --git a/t/meson.build b/t/meson.build index bbeba1a8d50..23a37029589 100644 --- a/t/meson.build +++ b/t/meson.build @@ -208,6 +208,7 @@ integration_tests = [ 't1450-fsck.sh', 't1451-fsck-buffer.sh', 't1460-refs-migrate.sh', + 't1461-refs-list.sh', 't1500-rev-parse.sh', 't1501-work-tree.sh', 't1502-rev-parse-parseopt.sh', diff --git a/t/t1461-refs-list.sh b/t/t1461-refs-list.sh new file mode 100755 index 00000000000..36e3d81e597 --- /dev/null +++ b/t/t1461-refs-list.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +test_description='git refs list tests' + +. ./test-lib.sh + +git_for_each_ref='git refs list' +. "$TEST_DIRECTORY"/for-each-ref-tests.sh -- GitLab From e9493c55af074fb7179922fafa61104332c05cc9 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 6 Aug 2025 07:54:12 +0200 Subject: [PATCH 029/371] Documentation/git-reflog: convert to use synopsis type With 974cdca345c (doc: introduce a synopsis typesetting, 2024-09-24) we have introduced a new synopsis type that simplifies the rules for typesetting a command's synopsis. Convert the git-reflog(1) documentation to use it. While at it, convert the list of options to use backticks. This is done to appease an upcoming new linter that mandates the use of backticks when using the synopsis type. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/git-reflog.adoc | 38 +++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Documentation/git-reflog.adoc b/Documentation/git-reflog.adoc index 412f06b8fec..0d6601fdead 100644 --- a/Documentation/git-reflog.adoc +++ b/Documentation/git-reflog.adoc @@ -8,16 +8,16 @@ git-reflog - Manage reflog information SYNOPSIS -------- -[verse] -'git reflog' [show] [] [] -'git reflog list' -'git reflog expire' [--expire=