From 41d97837ab1e5a35fdcfd7f6af9b5d56af62e92a Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Mon, 28 Jul 2025 22:05:19 +0300 Subject: [PATCH 001/695] 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 444a108f87c0b6..e070ed649ffcbc 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 fd0bba94e8b4d2..13f68310472a69 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, 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/695] 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 53faaa84207bf9..0cb920e9b0d520 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++; 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/695] 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 91586e6852b09e..70a64604018e99 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 18b22c0a26d633..893f8950bfc057 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 ada642a9fe5257..4ae7af0eff96f7 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 e77287f426cdfd..867b12bde5a412 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 dca87e164fb615..a81949a4220655 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 c1be9b76eb6373..98662706968dba 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 97b995f3f2da6f..6afcf088b97485 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 5224203ffe7bf8..9e7cb7519276c0 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 a3ddd701c740c9..de325f7ef99df6 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 bae7226ff4074f..54b9f49c01b599 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 f9f2c512a8c6e0..4edfb9c83b2393 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 6f52dab64cacb6..b9f5eb8b51e2de 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 0cb920e9b0d520..2284a009cba6a2 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 122b3186419880..6c8650efde0dfb 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 086238c826aadb..f5f33751da620d 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 8d9a271845c4b6..aa1cb9b4acfb2a 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 d4ba5f9fa52aa2..150a5f505f5bee 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 c123ac1e38b815..76487b54530098 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 4f26f6afc77106..91fcdcad9b5b48 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; } 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/695] 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 2284a009cba6a2..65b6ceb2591280 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); } 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/695] 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 65b6ceb2591280..86a309f8fbd25e 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 6c8650efde0dfb..40e148712dacca 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 150a5f505f5bee..daa9307e45ea41 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); 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/695] 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 a81949a4220655..70666ad2cd1ca6 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) && 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/695] 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 86a309f8fbd25e..343cf1ca90d2ac 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 40e148712dacca..2b438c7733d869 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 daa9307e45ea41..a2457d7b1ec8fa 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) 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/695] 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 6afcf088b97485..3603c4a42bdc44 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 de325f7ef99df6..5993c4afa0eb37 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 7782ae585e6471..e4dc02bd7a0ba3 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 a288069b04cb3b..f8316a7d29cdd5 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) 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/695] 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 454601afa15a95..a34dc144ee3616 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) 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/695] 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 053c94fc6bd12a..224551537e3b67 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) { 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/695] 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 224551537e3b67..708cd9344ca905 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]; 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/695] 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 708cd9344ca905..9bb920e7fdc61a 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) { 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/695] 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 9bb920e7fdc61a..38780edc395f0c 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); } 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/695] 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 cf8b06cadc7d50..70235856d7aae1 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); 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/695] 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 a9529b1696ae14..4fb36a743cd376 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) { 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/695] 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 8a2d0b7916442f..1769f15ee31862 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; } 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/695] 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 7c2480b22e5991..ab3ed08433d2c0 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, '/'); 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/695] 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 1daf5a975254b9..83bf0a0e82e56d 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; 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/695] 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 22a99a0682a498..2b7cfcd10c70ea 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); } 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/695] 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 2b7cfcd10c70ea..bbcfeda60af4de 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); } } 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/695] 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 fbf305d7624487..80722ae0c0421b 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; 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/695] 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 80722ae0c0421b..c18e4b3e4b714f 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; From 19623eb97e4edf76d585100c605037f9e51f6987 Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:53 +0530 Subject: [PATCH 023/695] 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 00000000000000..4a033d3e1618db --- /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 060940904da21c..130d452de0efe1 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 ----------- From 69c207dc45c0e95bff2bdcaf1c7aca41e9679fb8 Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:54 +0530 Subject: [PATCH 024/695] 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 8b5fe7b65e0105..fe62f078617a83 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 06b469bdee203d..2c6ecd5fc8e80e 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 From 6eeb1c070a8746734d74064905a8edeae08bd2a8 Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:55 +0530 Subject: [PATCH 025/695] 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 fe62f078617a83..4af33de57675d8 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 00000000000000..a5e0b6d17aa0d6 --- /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 */ From eecccfe98bb023a79f3c2b8bc415b6d656d0d381 Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:56 +0530 Subject: [PATCH 026/695] 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 4d6dc994f92eb2..e608980711814a 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 c7ad0a2963a3d1..76224feba4d55a 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(), }; From aa91c5c57013bdeca7b58ee5044bf667a4757978 Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:57 +0530 Subject: [PATCH 027/695] 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 00000000000000..e3ad19298accde --- /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 ce9af79ab12723..1d9809114d2c8f 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 From fed66d91c0fce8a1911b24ad79ed9f10d411874e Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:58 +0530 Subject: [PATCH 028/695] 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 bbeba1a8d50e1b..23a370295891cf 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 00000000000000..36e3d81e5970e7 --- /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 From b9fd73a234db1a272f6cbfb528bae0ead9e07bde Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 31 Jul 2025 16:56:49 +0200 Subject: [PATCH 029/695] refs: pass refname when invoking reflog entry callback With `refs_for_each_reflog_ent()` callers can iterate through all the reflog entries for a given reference. The callback that is being invoked for each such entry does not receive the name of the reference that we are currently iterating through. This isn't really a limiting factor, as callers can simply pass the name via the callback data. But this layout sometimes does make for a bit of an awkward calling pattern. One example: when iterating through all reflogs, and for each reflog we iterate through all refnames, we have to do some extra book keeping to track which reference name we are currently yielding reflog entries for. Change the signature of the callback function so that the reference name of the reflog gets passed through to it. Adapt callers accordingly and start using the new parameter in trivial cases. The next commit will refactor the reference migration logic to make use of this parameter so that we can simplify its logic a bit. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/fsck.c | 9 ++++----- builtin/gc.c | 3 ++- builtin/stash.c | 6 ++++-- commit.c | 3 ++- object-name.c | 3 ++- reflog-walk.c | 7 ++++--- reflog.c | 3 ++- reflog.h | 3 ++- refs.c | 20 +++++++++----------- refs.h | 11 +++++++---- refs/debug.c | 5 +++-- refs/files-backend.c | 15 +++++++++------ refs/reftable-backend.c | 2 +- remote.c | 6 ++++-- revision.c | 3 ++- t/helper/test-ref-store.c | 3 ++- wt-status.c | 6 ++++-- 17 files changed, 63 insertions(+), 45 deletions(-) diff --git a/builtin/fsck.c b/builtin/fsck.c index 0084cf7400bd46..67eb5e4fa0fe35 100644 --- a/builtin/fsck.c +++ b/builtin/fsck.c @@ -502,13 +502,12 @@ static void fsck_handle_reflog_oid(const char *refname, struct object_id *oid, } } -static int fsck_handle_reflog_ent(struct object_id *ooid, struct object_id *noid, +static int fsck_handle_reflog_ent(const char *refname, + struct object_id *ooid, struct object_id *noid, const char *email UNUSED, timestamp_t timestamp, int tz UNUSED, - const char *message UNUSED, void *cb_data) + const char *message UNUSED, void *cb_data UNUSED) { - const char *refname = cb_data; - if (verbose) fprintf_ln(stderr, _("Checking reflog %s->%s"), oid_to_hex(ooid), oid_to_hex(noid)); @@ -525,7 +524,7 @@ static int fsck_handle_reflog(const char *logname, void *cb_data) strbuf_worktree_ref(cb_data, &refname, logname); refs_for_each_reflog_ent(get_main_ref_store(the_repository), refname.buf, fsck_handle_reflog_ent, - refname.buf); + NULL); strbuf_release(&refname); return 0; } diff --git a/builtin/gc.c b/builtin/gc.c index fab8f4dd4f7b6c..9ae87065d35c8f 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -312,7 +312,8 @@ struct count_reflog_entries_data { size_t limit; }; -static int count_reflog_entries(struct object_id *old_oid, struct object_id *new_oid, +static int count_reflog_entries(const char *refname UNUSED, + struct object_id *old_oid, struct object_id *new_oid, const char *committer, timestamp_t timestamp, int tz, const char *msg, void *cb_data) { diff --git a/builtin/stash.c b/builtin/stash.c index e2f95cc2ebc219..a1ed67661e3e7f 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -738,7 +738,8 @@ static int apply_stash(int argc, const char **argv, const char *prefix, return ret; } -static int reject_reflog_ent(struct object_id *ooid UNUSED, +static int reject_reflog_ent(const char *refname UNUSED, + struct object_id *ooid UNUSED, struct object_id *noid UNUSED, const char *email UNUSED, timestamp_t timestamp UNUSED, @@ -2173,7 +2174,8 @@ struct stash_entry_data { size_t count; }; -static int collect_stash_entries(struct object_id *old_oid UNUSED, +static int collect_stash_entries(const char *refname UNUSED, + struct object_id *old_oid UNUSED, struct object_id *new_oid, const char *committer UNUSED, timestamp_t timestamp UNUSED, diff --git a/commit.c b/commit.c index 15115125c3612c..7ebd05f3527b13 100644 --- a/commit.c +++ b/commit.c @@ -1031,7 +1031,8 @@ static void add_one_commit(struct object_id *oid, struct rev_collect *revs) commit->object.flags |= TMP_MARK; } -static int collect_one_reflog_ent(struct object_id *ooid, struct object_id *noid, +static int collect_one_reflog_ent(const char *refname UNUSED, + struct object_id *ooid, struct object_id *noid, const char *ident UNUSED, timestamp_t timestamp UNUSED, int tz UNUSED, const char *message UNUSED, void *cbdata) diff --git a/object-name.c b/object-name.c index ddafe7f9b13a96..9ec192c37318a8 100644 --- a/object-name.c +++ b/object-name.c @@ -1516,7 +1516,8 @@ struct grab_nth_branch_switch_cbdata { struct strbuf *sb; }; -static int grab_nth_branch_switch(struct object_id *ooid UNUSED, +static int grab_nth_branch_switch(const char *refname UNUSED, + struct object_id *ooid UNUSED, struct object_id *noid UNUSED, const char *email UNUSED, timestamp_t timestamp UNUSED, diff --git a/reflog-walk.c b/reflog-walk.c index c7070b13b004b7..4f1ce047498116 100644 --- a/reflog-walk.c +++ b/reflog-walk.c @@ -22,9 +22,10 @@ struct complete_reflogs { int nr, alloc; }; -static int read_one_reflog(struct object_id *ooid, struct object_id *noid, - const char *email, timestamp_t timestamp, int tz, - const char *message, void *cb_data) +static int read_one_reflog(const char *refname UNUSED, + struct object_id *ooid, struct object_id *noid, + const char *email, timestamp_t timestamp, int tz, + const char *message, void *cb_data) { struct complete_reflogs *array = cb_data; struct reflog_info *item; diff --git a/reflog.c b/reflog.c index 39c205fd26e77b..2264b3bd605f3a 100644 --- a/reflog.c +++ b/reflog.c @@ -492,7 +492,8 @@ void reflog_expiry_cleanup(void *cb_data) free_commit_list(cb->mark_list); } -int count_reflog_ent(struct object_id *ooid UNUSED, +int count_reflog_ent(const char *refname UNUSED, + struct object_id *ooid UNUSED, struct object_id *noid UNUSED, const char *email UNUSED, timestamp_t timestamp, int tz UNUSED, diff --git a/reflog.h b/reflog.h index 63bb56280f4ed6..44b306c08ae391 100644 --- a/reflog.h +++ b/reflog.h @@ -63,7 +63,8 @@ void reflog_expiry_prepare(const char *refname, const struct object_id *oid, int should_expire_reflog_ent(struct object_id *ooid, struct object_id *noid, const char *email, timestamp_t timestamp, int tz, const char *message, void *cb_data); -int count_reflog_ent(struct object_id *ooid, struct object_id *noid, +int count_reflog_ent(const char *refname, + struct object_id *ooid, struct object_id *noid, const char *email, timestamp_t timestamp, int tz, const char *message, void *cb_data); int should_expire_reflog_ent_verbose(struct object_id *ooid, diff --git a/refs.c b/refs.c index 4bd80287054e9b..6ed0cd6ddca00d 100644 --- a/refs.c +++ b/refs.c @@ -1022,7 +1022,6 @@ int is_branch(const char *refname) } struct read_ref_at_cb { - const char *refname; timestamp_t at_time; int cnt; int reccnt; @@ -1052,7 +1051,8 @@ static void set_read_ref_cutoffs(struct read_ref_at_cb *cb, *cb->cutoff_cnt = cb->reccnt; } -static int read_ref_at_ent(struct object_id *ooid, struct object_id *noid, +static int read_ref_at_ent(const char *refname, + struct object_id *ooid, struct object_id *noid, const char *email UNUSED, timestamp_t timestamp, int tz, const char *message, void *cb_data) @@ -1072,14 +1072,13 @@ static int read_ref_at_ent(struct object_id *ooid, struct object_id *noid, oidcpy(cb->oid, noid); if (!oideq(&cb->ooid, noid)) warning(_("log for ref %s has gap after %s"), - cb->refname, show_date(cb->date, cb->tz, DATE_MODE(RFC2822))); + refname, show_date(cb->date, cb->tz, DATE_MODE(RFC2822))); } else if (cb->date == cb->at_time) oidcpy(cb->oid, noid); else if (!oideq(noid, cb->oid)) warning(_("log for ref %s unexpectedly ended on %s"), - cb->refname, show_date(cb->date, cb->tz, - DATE_MODE(RFC2822))); + refname, show_date(cb->date, cb->tz, DATE_MODE(RFC2822))); cb->reccnt++; oidcpy(&cb->ooid, ooid); oidcpy(&cb->noid, noid); @@ -1094,7 +1093,8 @@ static int read_ref_at_ent(struct object_id *ooid, struct object_id *noid, return 0; } -static int read_ref_at_ent_oldest(struct object_id *ooid, struct object_id *noid, +static int read_ref_at_ent_oldest(const char *refname UNUSED, + struct object_id *ooid, struct object_id *noid, const char *email UNUSED, timestamp_t timestamp, int tz, const char *message, void *cb_data) @@ -1117,7 +1117,6 @@ int read_ref_at(struct ref_store *refs, const char *refname, struct read_ref_at_cb cb; memset(&cb, 0, sizeof(cb)); - cb.refname = refname; cb.at_time = at_time; cb.cnt = cnt; cb.msg = msg; @@ -2976,14 +2975,14 @@ static int migrate_one_ref(const char *refname, const char *referent UNUSED, con struct reflog_migration_data { uint64_t index; - const char *refname; struct ref_store *old_refs; struct ref_transaction *transaction; struct strbuf *errbuf; struct strbuf *sb, *name, *mail; }; -static int migrate_one_reflog_entry(struct object_id *old_oid, +static int migrate_one_reflog_entry(const char *refname, + struct object_id *old_oid, struct object_id *new_oid, const char *committer, timestamp_t timestamp, int tz, @@ -3006,7 +3005,7 @@ static int migrate_one_reflog_entry(struct object_id *old_oid, strbuf_reset(data->sb); strbuf_addstr(data->sb, fmt_ident(data->name->buf, data->mail->buf, WANT_BLANK_IDENT, date, 0)); - ret = ref_transaction_update_reflog(data->transaction, data->refname, + ret = ref_transaction_update_reflog(data->transaction, refname, new_oid, old_oid, data->sb->buf, msg, data->index++, data->errbuf); return ret; @@ -3016,7 +3015,6 @@ static int migrate_one_reflog(const char *refname, void *cb_data) { struct migration_data *migration_data = cb_data; struct reflog_migration_data data = { - .refname = refname, .old_refs = migration_data->old_refs, .transaction = migration_data->transaction, .errbuf = migration_data->errbuf, diff --git a/refs.h b/refs.h index 99b58d0b73c9db..0bf50ce25cc067 100644 --- a/refs.h +++ b/refs.h @@ -558,10 +558,13 @@ int refs_delete_reflog(struct ref_store *refs, const char *refname); * The cb_data is a caller-supplied pointer given to the iterator * functions. */ -typedef int each_reflog_ent_fn( - struct object_id *old_oid, struct object_id *new_oid, - const char *committer, timestamp_t timestamp, - int tz, const char *msg, void *cb_data); +typedef int each_reflog_ent_fn(const char *refname, + struct object_id *old_oid, + struct object_id *new_oid, + const char *committer, + timestamp_t timestamp, + int tz, const char *msg, + void *cb_data); /* Iterate over reflog entries in the log for `refname`. */ diff --git a/refs/debug.c b/refs/debug.c index 485e3079d7a3a7..5e113db307a2e5 100644 --- a/refs/debug.c +++ b/refs/debug.c @@ -276,7 +276,8 @@ struct debug_reflog { void *cb_data; }; -static int debug_print_reflog_ent(struct object_id *old_oid, +static int debug_print_reflog_ent(const char *refname, + struct object_id *old_oid, struct object_id *new_oid, const char *committer, timestamp_t timestamp, int tz, const char *msg, void *cb_data) @@ -291,7 +292,7 @@ static int debug_print_reflog_ent(struct object_id *old_oid, if (new_oid) oid_to_hex_r(n, new_oid); - ret = dbg->fn(old_oid, new_oid, committer, timestamp, tz, msg, + ret = dbg->fn(refname, old_oid, new_oid, committer, timestamp, tz, msg, dbg->cb_data); trace_printf_key(&trace_refs, "reflog_ent %s (ret %d): %s -> %s, %s %ld \"%.*s\"\n", diff --git a/refs/files-backend.c b/refs/files-backend.c index f53895cf4bd9fa..dff52a583a9cc5 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -2115,7 +2115,9 @@ static int files_delete_reflog(struct ref_store *ref_store, return ret; } -static int show_one_reflog_ent(struct files_ref_store *refs, struct strbuf *sb, +static int show_one_reflog_ent(struct files_ref_store *refs, + const char *refname, + struct strbuf *sb, each_reflog_ent_fn fn, void *cb_data) { struct object_id ooid, noid; @@ -2142,7 +2144,7 @@ static int show_one_reflog_ent(struct files_ref_store *refs, struct strbuf *sb, message += 6; else message += 7; - return fn(&ooid, &noid, p, timestamp, tz, message, cb_data); + return fn(refname, &ooid, &noid, p, timestamp, tz, message, cb_data); } static char *find_beginning_of_line(char *bob, char *scan) @@ -2226,7 +2228,7 @@ static int files_for_each_reflog_ent_reverse(struct ref_store *ref_store, strbuf_splice(&sb, 0, 0, bp + 1, endp - (bp + 1)); scanp = bp; endp = bp + 1; - ret = show_one_reflog_ent(refs, &sb, fn, cb_data); + ret = show_one_reflog_ent(refs, refname, &sb, fn, cb_data); strbuf_reset(&sb); if (ret) break; @@ -2238,7 +2240,7 @@ static int files_for_each_reflog_ent_reverse(struct ref_store *ref_store, * Process it, and we can end the loop. */ strbuf_splice(&sb, 0, 0, buf, endp - buf); - ret = show_one_reflog_ent(refs, &sb, fn, cb_data); + ret = show_one_reflog_ent(refs, refname, &sb, fn, cb_data); strbuf_reset(&sb); break; } @@ -2288,7 +2290,7 @@ static int files_for_each_reflog_ent(struct ref_store *ref_store, return -1; while (!ret && !strbuf_getwholeline(&sb, logfp, '\n')) - ret = show_one_reflog_ent(refs, &sb, fn, cb_data); + ret = show_one_reflog_ent(refs, refname, &sb, fn, cb_data); fclose(logfp); strbuf_release(&sb); return ret; @@ -3359,7 +3361,8 @@ struct expire_reflog_cb { dry_run:1; }; -static int expire_reflog_ent(struct object_id *ooid, struct object_id *noid, +static int expire_reflog_ent(const char *refname UNUSED, + struct object_id *ooid, struct object_id *noid, const char *email, timestamp_t timestamp, int tz, const char *message, void *cb_data) { diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index 99fafd75ebe8ff..25a1d516184e43 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -2148,7 +2148,7 @@ static int yield_log_record(struct reftable_ref_store *refs, full_committer = fmt_ident(log->value.update.name, log->value.update.email, WANT_COMMITTER_IDENT, NULL, IDENT_NO_DATE); - return fn(&old_oid, &new_oid, full_committer, + return fn(log->refname, &old_oid, &new_oid, full_committer, log->value.update.time, log->value.update.tz_offset, log->value.update.message, cb_data); } diff --git a/remote.c b/remote.c index e965f022f12b78..db9eea4fa456d0 100644 --- a/remote.c +++ b/remote.c @@ -2578,7 +2578,8 @@ struct check_and_collect_until_cb_data { }; /* Get the timestamp of the latest entry. */ -static int peek_reflog(struct object_id *o_oid UNUSED, +static int peek_reflog(const char *refname UNUSED, + struct object_id *o_oid UNUSED, struct object_id *n_oid UNUSED, const char *ident UNUSED, timestamp_t timestamp, int tz UNUSED, @@ -2589,7 +2590,8 @@ static int peek_reflog(struct object_id *o_oid UNUSED, return 1; } -static int check_and_collect_until(struct object_id *o_oid UNUSED, +static int check_and_collect_until(const char *refname UNUSED, + struct object_id *o_oid UNUSED, struct object_id *n_oid, const char *ident UNUSED, timestamp_t timestamp, int tz UNUSED, diff --git a/revision.c b/revision.c index 212ca0de2768b8..0fc1a167a10896 100644 --- a/revision.c +++ b/revision.c @@ -1699,7 +1699,8 @@ static void handle_one_reflog_commit(struct object_id *oid, void *cb_data) } } -static int handle_one_reflog_ent(struct object_id *ooid, struct object_id *noid, +static int handle_one_reflog_ent(const char *refname UNUSED, + struct object_id *ooid, struct object_id *noid, const char *email UNUSED, timestamp_t timestamp UNUSED, int tz UNUSED, diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c index 8d9a271845c4b6..b2380d57ba3065 100644 --- a/t/helper/test-ref-store.c +++ b/t/helper/test-ref-store.c @@ -215,7 +215,8 @@ static int cmd_for_each_reflog(struct ref_store *refs, return refs_for_each_reflog(refs, each_reflog, NULL); } -static int each_reflog_ent(struct object_id *old_oid, struct object_id *new_oid, +static int each_reflog_ent(const char *refname UNUSED, + struct object_id *old_oid, struct object_id *new_oid, const char *committer, timestamp_t timestamp, int tz, const char *msg, void *cb_data UNUSED) { diff --git a/wt-status.c b/wt-status.c index 454601afa15a95..71bd17b610a89f 100644 --- a/wt-status.c +++ b/wt-status.c @@ -972,7 +972,8 @@ static void wt_longstatus_print_changed(struct wt_status *s) wt_longstatus_print_trailer(s); } -static int stash_count_refs(struct object_id *ooid UNUSED, +static int stash_count_refs(const char *refname UNUSED, + struct object_id *ooid UNUSED, struct object_id *noid UNUSED, const char *email UNUSED, timestamp_t timestamp UNUSED, int tz UNUSED, @@ -1664,7 +1665,8 @@ struct grab_1st_switch_cbdata { struct object_id noid; }; -static int grab_1st_switch(struct object_id *ooid UNUSED, +static int grab_1st_switch(const char *refname UNUSED, + struct object_id *ooid UNUSED, struct object_id *noid, const char *email UNUSED, timestamp_t timestamp UNUSED, int tz UNUSED, From 2f530e5d0ac9349ad5884a7d74a60762e4ee05f8 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 31 Jul 2025 16:56:50 +0200 Subject: [PATCH 030/695] refs: simplify logic when migrating reflog entries When migrating reflog entries between two storage formats we have to do so via two callback-driven functions: - `migrate_one_reflog()` gets invoked via `refs_for_each_reflog()` to first list all available reflogs. - `migrate_one_reflog_entry()` gets invoked via `refs_for_each_reflog_ent()` in `migrate_one_reflog()`. Before the preceding commit we didn't have the refname available in `migrate_one_reflog_entry()`, which made it necessary to have a separate structure that we pass to the second callback so that we can propagate the refname. Now that `refs_for_each_reflog_ent()` knows to pass the refname to the callback though that indirection isn't necessary anymore. There's one catch though: we do have an update index that is also stored in the entry-specific callback data. This update index is required so that we can tell the ref backend in which order it should persist the reflog entries to disk. But that purpose can be trivially achieved by just converting it into a global counter that is used for all reflog entries, regardless of which reference they are for. The ordering will remain the same as both the update index and the refname is considered when sorting the entries. Move the index into `struct migration_data` and drop the now-unused `struct reflog_migration_data` to simplify the code a bit. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- refs.c | 36 ++++++++++-------------------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/refs.c b/refs.c index 6ed0cd6ddca00d..04c9ace7932ace 100644 --- a/refs.c +++ b/refs.c @@ -2941,6 +2941,7 @@ struct migration_data { struct ref_transaction *transaction; struct strbuf *errbuf; struct strbuf sb, name, mail; + uint64_t index; }; static int migrate_one_ref(const char *refname, const char *referent UNUSED, const struct object_id *oid, @@ -2973,14 +2974,6 @@ static int migrate_one_ref(const char *refname, const char *referent UNUSED, con return ret; } -struct reflog_migration_data { - uint64_t index; - struct ref_store *old_refs; - struct ref_transaction *transaction; - struct strbuf *errbuf; - struct strbuf *sb, *name, *mail; -}; - static int migrate_one_reflog_entry(const char *refname, struct object_id *old_oid, struct object_id *new_oid, @@ -2988,7 +2981,7 @@ static int migrate_one_reflog_entry(const char *refname, timestamp_t timestamp, int tz, const char *msg, void *cb_data) { - struct reflog_migration_data *data = cb_data; + struct migration_data *data = cb_data; struct ident_split ident; const char *date; int ret; @@ -2996,17 +2989,17 @@ static int migrate_one_reflog_entry(const char *refname, if (split_ident_line(&ident, committer, strlen(committer)) < 0) return -1; - strbuf_reset(data->name); - strbuf_add(data->name, ident.name_begin, ident.name_end - ident.name_begin); - strbuf_reset(data->mail); - strbuf_add(data->mail, ident.mail_begin, ident.mail_end - ident.mail_begin); + strbuf_reset(&data->name); + strbuf_add(&data->name, ident.name_begin, ident.name_end - ident.name_begin); + strbuf_reset(&data->mail); + strbuf_add(&data->mail, ident.mail_begin, ident.mail_end - ident.mail_begin); date = show_date(timestamp, tz, DATE_MODE(NORMAL)); - strbuf_reset(data->sb); - strbuf_addstr(data->sb, fmt_ident(data->name->buf, data->mail->buf, WANT_BLANK_IDENT, date, 0)); + strbuf_reset(&data->sb); + strbuf_addstr(&data->sb, fmt_ident(data->name.buf, data->mail.buf, WANT_BLANK_IDENT, date, 0)); ret = ref_transaction_update_reflog(data->transaction, refname, - new_oid, old_oid, data->sb->buf, + new_oid, old_oid, data->sb.buf, msg, data->index++, data->errbuf); return ret; } @@ -3014,17 +3007,8 @@ static int migrate_one_reflog_entry(const char *refname, static int migrate_one_reflog(const char *refname, void *cb_data) { struct migration_data *migration_data = cb_data; - struct reflog_migration_data data = { - .old_refs = migration_data->old_refs, - .transaction = migration_data->transaction, - .errbuf = migration_data->errbuf, - .sb = &migration_data->sb, - .name = &migration_data->name, - .mail = &migration_data->mail, - }; - return refs_for_each_reflog_ent(migration_data->old_refs, refname, - migrate_one_reflog_entry, &data); + migrate_one_reflog_entry, migration_data); } static int move_files(const char *from_path, const char *to_path, struct strbuf *errbuf) From 376d7f1a11a52bc3f2f4ce74557536ac2195ce5f Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 31 Jul 2025 16:56:51 +0200 Subject: [PATCH 031/695] builtin/remote: fix sign comparison warnings Fix -Wsign-comparison warnings. All of the warnings we have are about mismatches in signedness for loop counters. These are trivially fixable by using the correct integer type. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/remote.c | 54 +++++++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/builtin/remote.c b/builtin/remote.c index 5dd6cbbaeedb25..f63c5eb8881a6c 100644 --- a/builtin/remote.c +++ b/builtin/remote.c @@ -1,5 +1,4 @@ #define USE_THE_REPOSITORY_VARIABLE -#define DISABLE_SIGN_COMPARE_WARNINGS #include "builtin.h" #include "config.h" @@ -182,7 +181,6 @@ static int add(int argc, const char **argv, const char *prefix, struct remote *remote; struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT; const char *name, *url; - int i; int result = 0; struct option options[] = { @@ -233,7 +231,7 @@ static int add(int argc, const char **argv, const char *prefix, strbuf_addf(&buf, "remote.%s.fetch", name); if (track.nr == 0) string_list_append(&track, "*"); - for (i = 0; i < track.nr; i++) { + for (size_t i = 0; i < track.nr; i++) { add_branch(buf.buf, track.items[i].string, name, mirror, &buf2); } @@ -647,18 +645,17 @@ static int read_remote_branches(const char *refname, const char *referent UNUSED static int migrate_file(struct remote *remote) { struct strbuf buf = STRBUF_INIT; - int i; strbuf_addf(&buf, "remote.%s.url", remote->name); - for (i = 0; i < remote->url.nr; i++) + for (size_t i = 0; i < remote->url.nr; i++) git_config_set_multivar(buf.buf, remote->url.v[i], "^$", 0); strbuf_reset(&buf); strbuf_addf(&buf, "remote.%s.push", remote->name); - for (i = 0; i < remote->push.nr; i++) + for (int i = 0; i < remote->push.nr; i++) git_config_set_multivar(buf.buf, remote->push.items[i].raw, "^$", 0); strbuf_reset(&buf); strbuf_addf(&buf, "remote.%s.fetch", remote->name); - for (i = 0; i < remote->fetch.nr; i++) + for (int i = 0; i < remote->fetch.nr; i++) git_config_set_multivar(buf.buf, remote->fetch.items[i].raw, "^$", 0); #ifndef WITH_BREAKING_CHANGES if (remote->origin == REMOTE_REMOTES) @@ -744,7 +741,7 @@ static int mv(int argc, const char **argv, const char *prefix, old_remote_context = STRBUF_INIT; struct string_list remote_branches = STRING_LIST_INIT_DUP; struct rename_info rename; - int i, refs_renamed_nr = 0, refspec_updated = 0; + int refs_renamed_nr = 0, refspec_updated = 0; struct progress *progress = NULL; int result = 0; @@ -790,7 +787,7 @@ static int mv(int argc, const char **argv, const char *prefix, strbuf_addf(&buf, "remote.%s.fetch", rename.new_name); git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE); strbuf_addf(&old_remote_context, ":refs/remotes/%s/", rename.old_name); - for (i = 0; i < oldremote->fetch.nr; i++) { + for (int i = 0; i < oldremote->fetch.nr; i++) { char *ptr; strbuf_reset(&buf2); @@ -813,7 +810,7 @@ static int mv(int argc, const char **argv, const char *prefix, } read_branches(); - for (i = 0; i < branch_list.nr; i++) { + for (size_t i = 0; i < branch_list.nr; i++) { struct string_list_item *item = branch_list.items + i; struct branch_info *info = item->util; if (info->remote_name && !strcmp(info->remote_name, rename.old_name)) { @@ -846,7 +843,7 @@ static int mv(int argc, const char **argv, const char *prefix, _("Renaming remote references"), rename.remote_branches->nr + rename.symrefs_nr); } - for (i = 0; i < remote_branches.nr; i++) { + for (size_t i = 0; i < remote_branches.nr; i++) { struct string_list_item *item = remote_branches.items + i; struct strbuf referent = STRBUF_INIT; @@ -859,7 +856,7 @@ static int mv(int argc, const char **argv, const char *prefix, strbuf_release(&referent); display_progress(progress, ++refs_renamed_nr); } - for (i = 0; i < remote_branches.nr; i++) { + for (size_t i = 0; i < remote_branches.nr; i++) { struct string_list_item *item = remote_branches.items + i; if (item->util) @@ -875,7 +872,7 @@ static int mv(int argc, const char **argv, const char *prefix, die(_("renaming '%s' failed"), item->string); display_progress(progress, ++refs_renamed_nr); } - for (i = 0; i < remote_branches.nr; i++) { + for (size_t i = 0; i < remote_branches.nr; i++) { struct string_list_item *item = remote_branches.items + i; if (!item->util) @@ -920,7 +917,7 @@ static int rm(int argc, const char **argv, const char *prefix, struct string_list branches = STRING_LIST_INIT_DUP; struct string_list skipped = STRING_LIST_INIT_DUP; struct branches_for_remote cb_data; - int i, result; + int result; memset(&cb_data, 0, sizeof(cb_data)); cb_data.branches = &branches; @@ -942,7 +939,7 @@ static int rm(int argc, const char **argv, const char *prefix, for_each_remote(add_known_remote, &known_remotes); read_branches(); - for (i = 0; i < branch_list.nr; i++) { + for (size_t i = 0; i < branch_list.nr; i++) { struct string_list_item *item = branch_list.items + i; struct branch_info *info = item->util; if (info->remote_name && !strcmp(info->remote_name, remote->name)) { @@ -988,7 +985,7 @@ static int rm(int argc, const char **argv, const char *prefix, "Note: Some branches outside the refs/remotes/ hierarchy were not removed;\n" "to delete them, use:", skipped.nr)); - for (i = 0; i < skipped.nr; i++) + for (size_t i = 0; i < skipped.nr; i++) fprintf(stderr, " git branch -d %s\n", skipped.items[i].string); } @@ -1166,7 +1163,6 @@ static int show_local_info_item(struct string_list_item *item, void *cb_data) struct branch_info *branch_info = item->util; struct string_list *merge = &branch_info->merge; int width = show_info->width + 4; - int i; if (branch_info->rebase >= REBASE_TRUE && branch_info->merge.nr > 1) { error(_("invalid branch.%s.merge; cannot rebase onto > 1 branch"), @@ -1192,7 +1188,7 @@ static int show_local_info_item(struct string_list_item *item, void *cb_data) } else { printf_ln(_("merges with remote %s"), merge->items[0].string); } - for (i = 1; i < merge->nr; i++) + for (size_t i = 1; i < merge->nr; i++) printf(_("%-*s and with remote %s\n"), width, "", merge->items[i].string); @@ -1277,7 +1273,6 @@ static int get_one_entry(struct remote *remote, void *priv) struct string_list *list = priv; struct strbuf remote_info_buf = STRBUF_INIT; struct strvec *url; - int i; if (remote->url.nr > 0) { struct strbuf promisor_config = STRBUF_INIT; @@ -1294,8 +1289,7 @@ static int get_one_entry(struct remote *remote, void *priv) } else string_list_append(list, remote->name)->util = NULL; url = push_url_of_remote(remote); - for (i = 0; i < url->nr; i++) - { + for (size_t i = 0; i < url->nr; i++) { strbuf_addf(&remote_info_buf, "%s (push)", url->v[i]); string_list_append(list, remote->name)->util = strbuf_detach(&remote_info_buf, NULL); @@ -1312,10 +1306,8 @@ static int show_all(void) result = for_each_remote(get_one_entry, &list); if (!result) { - int i; - string_list_sort(&list); - for (i = 0; i < list.nr; i++) { + for (size_t i = 0; i < list.nr; i++) { struct string_list_item *item = list.items + i; if (verbose) printf("%s\t%s\n", item->string, @@ -1352,7 +1344,7 @@ static int show(int argc, const char **argv, const char *prefix, query_flag = (GET_REF_STATES | GET_HEAD_NAMES | GET_PUSH_REF_STATES); for (; argc; argc--, argv++) { - int i; + size_t i; struct strvec *url; get_remote_ref_states(*argv, &info.states, query_flag); @@ -1458,7 +1450,7 @@ static void report_set_head_auto(const char *remote, const char *head_name, static int set_head(int argc, const char **argv, const char *prefix, struct repository *repo UNUSED) { - int i, opt_a = 0, opt_d = 0, result = 0, was_detached; + int opt_a = 0, opt_d = 0, result = 0, was_detached; struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT, b_local_head = STRBUF_INIT; char *head_name = NULL; @@ -1489,7 +1481,7 @@ static int set_head(int argc, const char **argv, const char *prefix, else if (states.heads.nr > 1) { result |= error(_("Multiple remote HEAD branches. " "Please choose one explicitly with:")); - for (i = 0; i < states.heads.nr; i++) + for (size_t i = 0; i < states.heads.nr; i++) fprintf(stderr, " git remote set-head %s %s\n", argv[0], states.heads.items[i].string); } else @@ -1714,7 +1706,7 @@ static int set_branches(int argc, const char **argv, const char *prefix, static int get_url(int argc, const char **argv, const char *prefix, struct repository *repo UNUSED) { - int i, push_mode = 0, all_mode = 0; + int push_mode = 0, all_mode = 0; const char *remotename = NULL; struct remote *remote; struct strvec *url; @@ -1742,7 +1734,7 @@ static int get_url(int argc, const char **argv, const char *prefix, url = push_mode ? push_url_of_remote(remote) : &remote->url; if (all_mode) { - for (i = 0; i < url->nr; i++) + for (size_t i = 0; i < url->nr; i++) printf_ln("%s", url->v[i]); } else { printf_ln("%s", url->v[0]); @@ -1754,7 +1746,7 @@ static int get_url(int argc, const char **argv, const char *prefix, static int set_url(int argc, const char **argv, const char *prefix, struct repository *repo UNUSED) { - int i, push_mode = 0, add_mode = 0, delete_mode = 0; + int push_mode = 0, add_mode = 0, delete_mode = 0; int matches = 0, negative_matches = 0; const char *remotename = NULL; const char *newurl = NULL; @@ -1818,7 +1810,7 @@ static int set_url(int argc, const char **argv, const char *prefix, if (regcomp(&old_regex, oldurl, REG_EXTENDED)) die(_("Invalid old URL pattern: %s"), oldurl); - for (i = 0; i < urlset->nr; i++) + for (size_t i = 0; i < urlset->nr; i++) if (!regexec(&old_regex, urlset->v[i], 0, NULL, 0)) matches++; else From 08e6a7add4678662d929718e8aa80d2505352cfd Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 31 Jul 2025 16:56:52 +0200 Subject: [PATCH 032/695] builtin/remote: determine whether refs need renaming early on When renaming a remote we may have to also rename remote refs in case the refspec changes. Pull out this computation into a separate loop. While that seems nonsensical right now, it'll help us in a subsequent commit where we will prepare the reference transaction before we rewrite the configuration. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/remote.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/builtin/remote.c b/builtin/remote.c index f63c5eb8881a6c..34ddcaf5f6fb8f 100644 --- a/builtin/remote.c +++ b/builtin/remote.c @@ -741,7 +741,7 @@ static int mv(int argc, const char **argv, const char *prefix, old_remote_context = STRBUF_INIT; struct string_list remote_branches = STRING_LIST_INIT_DUP; struct rename_info rename; - int refs_renamed_nr = 0, refspec_updated = 0; + int refs_renamed_nr = 0, refspecs_need_update = 0; struct progress *progress = NULL; int result = 0; @@ -782,11 +782,16 @@ static int mv(int argc, const char **argv, const char *prefix, goto out; } + strbuf_addf(&old_remote_context, ":refs/remotes/%s/", rename.old_name); + + for (int i = 0; i < oldremote->fetch.nr && !refspecs_need_update; i++) + refspecs_need_update = !!strstr(oldremote->fetch.items[i].raw, + old_remote_context.buf); + if (oldremote->fetch.nr) { strbuf_reset(&buf); strbuf_addf(&buf, "remote.%s.fetch", rename.new_name); git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE); - strbuf_addf(&old_remote_context, ":refs/remotes/%s/", rename.old_name); for (int i = 0; i < oldremote->fetch.nr; i++) { char *ptr; @@ -794,7 +799,6 @@ static int mv(int argc, const char **argv, const char *prefix, strbuf_addstr(&buf2, oldremote->fetch.items[i].raw); ptr = strstr(buf2.buf, old_remote_context.buf); if (ptr) { - refspec_updated = 1; strbuf_splice(&buf2, ptr-buf2.buf + strlen(":refs/remotes/"), strlen(rename.old_name), rename.new_name, @@ -825,7 +829,7 @@ static int mv(int argc, const char **argv, const char *prefix, } } - if (!refspec_updated) + if (!refspecs_need_update) goto out; /* From 68d090a6829a46522da0d1b15099efd6d1cdb28c Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 31 Jul 2025 16:56:53 +0200 Subject: [PATCH 033/695] builtin/remote: rework how remote refs get renamed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It was recently reported [1] that renaming a remote that has dangling symrefs is broken. This issue can be trivially reproduced: $ git init repo Initialized empty Git repository in /tmp/repo/.git/ $ cd repo/ $ git remote add origin /dev/null $ git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/master $ git remote rename origin renamed $ git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/master $ git symbolic-ref refs/remotes/renamed/HEAD fatal: ref refs/remotes/renamed/HEAD is not a symbolic ref As one can see, the "HEAD" reference did not get renamed but stays in the same place. There are two issues here: - We use `refs_resolve_ref_unsafe()` to resolve references, but we don't pass the `RESOLVE_REF_NO_RECURSE` flag. Consequently, if the reference does not resolve, the function will fail and we thus ignore this branch. - We use `refs_for_each_ref()` to iterate through the old remote's references, but that function ignores broken references. Both of these issues are easy to fix. But having a closer look at the logic that renames remote references surfaces that it leaves a lot to be desired overall. The problem is that we're using O(|refs| + |symrefs| * 2) many reference transactions to perform the renames. We first delete all symrefs, then individually rename every direct reference and finally we recreate the symrefs. On the one hand this isn't even remotely an atomic operation, so if we hit any error we'll already have deleted all references. But more importantly it is also extremely inefficient. The number of transactions for symrefs doesn't really bother us too much, as there should generally only be a single symref anyway ("HEAD"). But the renames are very expensive: - For the "reftable" backend we perform auto-compaction after every single rename, which does add up. - For the "files" backend we potentially have to rewrite the "packed-refs" file on every single rename in case they are packed. The consequence here is quadratic runtime performance. Renaming a 100k references takes hours to complete. Refactor the code to use a single transaction to perform all the reference updates atomically, which speeds up the transaction quite significantly: Benchmark 1: rename remote (refformat = files, revision = HEAD~) Time (mean ± σ): 238.770 s ± 13.857 s [User: 91.473 s, System: 143.793 s] Range (min … max): 204.863 s … 247.699 s 10 runs Benchmark 2: rename remote (refformat = files, revision = HEAD) Time (mean ± σ): 2.103 s ± 0.036 s [User: 0.360 s, System: 1.313 s] Range (min … max): 2.011 s … 2.141 s 10 runs Summary rename remote (refformat = files, revision = HEAD) ran 113.53 ± 6.87 times faster than rename remote (refformat = files, revision = HEAD~) For the "reftable" backend we see a significant speedup, as well, but given that we don't have quadratic runtime behaviour there it's way less extreme: Benchmark 1: rename remote (refformat = reftable, revision = HEAD~) Time (mean ± σ): 8.604 s ± 0.539 s [User: 4.985 s, System: 2.368 s] Range (min … max): 7.880 s … 9.556 s 10 runs Benchmark 2: rename remote (refformat = reftable, revision = HEAD) Time (mean ± σ): 1.177 s ± 0.103 s [User: 0.446 s, System: 0.270 s] Range (min … max): 1.023 s … 1.410 s 10 runs Summary rename remote (refformat = reftable, revision = HEAD) ran 7.31 ± 0.79 times faster than rename remote (refformat = reftable, revision = HEAD~) There is one issue though with using atomic transactions: when nesting a remote into itself it can happen that renamed references conflict with the old referencse. For example, when we have a reference "refs/remotes/origin/foo" and we rename "origin" to "origin/foo", then we'll end up with an F/D conflict when we try to create the renamed reference "refs/remotes/origin/foo/foo". This situation is overall quite unlikely to happen: people tend to not use nested remotes, and if they do they must at the same time also have a conflicting refname. But the end result would be that the old remote references stay intact whereas all the other parts of the repository have been adjusted for the new remote name. Address this by queueing and preparing the reference update before we touch any other part of the repository. Like this we can make sure that the reference update will go through before rewriting the configuration. Otherwise, if the transaction fails to prepare we can gracefully abort the whole operation without any changes having been performed in the repository yet. Furthermore, we can detect the conflict and print some helpful advice for how the user can resolve this situation. So overall, the tradeoff is that: - Reference transactions are now all-or-nothing. This is a significant improvement over the previous state where we may have ended up with partially-renamed references. - Rewriting references is now significantly faster. - We only rewrite the configuration in case we know that all references can be updated. - But we may refuse to rename a remote in case references conflict. Overall this seems like an acceptable tradeoff. While at it, fix the handling of symbolic/broken references by using `refs_for_each_rawref()`. Add tests that cover both this reported issue and tests that exercise nesting of remotes. One thing to note: with this change we cannot provide a proper progress monitor anymore as we queue the references into the transactions as we iterate through them. Consequently, as we don't know yet how many refs there are in total, we cannot report how many percent of the operation is done anymore. But that's a small price to pay considering that you now shouldn't need the progress monitor in most situations at all anymore. [1]: Reported-by: Han Jiang Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/remote.c | 296 ++++++++++++++++++++++++++++++---------------- t/t5505-remote.sh | 73 ++++++++++++ 2 files changed, 270 insertions(+), 99 deletions(-) diff --git a/builtin/remote.c b/builtin/remote.c index 34ddcaf5f6fb8f..db481f39bc9af2 100644 --- a/builtin/remote.c +++ b/builtin/remote.c @@ -1,8 +1,11 @@ #define USE_THE_REPOSITORY_VARIABLE #include "builtin.h" +#include "advice.h" #include "config.h" +#include "date.h" #include "gettext.h" +#include "ident.h" #include "parse-options.h" #include "path.h" #include "transport.h" @@ -610,36 +613,161 @@ static int add_branch_for_removal(const char *refname, struct rename_info { const char *old_name; const char *new_name; - struct string_list *remote_branches; - uint32_t symrefs_nr; + struct ref_transaction *transaction; + struct progress *progress; + struct strbuf *err; + uint32_t progress_nr; + uint64_t index; }; -static int read_remote_branches(const char *refname, const char *referent UNUSED, - const struct object_id *oid UNUSED, - int flags UNUSED, void *cb_data) +static void compute_renamed_ref(struct rename_info *rename, + const char *refname, + struct strbuf *out) +{ + strbuf_reset(out); + strbuf_addstr(out, refname); + strbuf_splice(out, strlen("refs/remotes/"), strlen(rename->old_name), + rename->new_name, strlen(rename->new_name)); +} + +static int rename_one_reflog_entry(const char *old_refname, + struct object_id *old_oid, + struct object_id *new_oid, + const char *committer, + timestamp_t timestamp, int tz, + const char *msg, void *cb_data) { struct rename_info *rename = cb_data; - struct strbuf buf = STRBUF_INIT; - struct string_list_item *item; - int flag; - const char *symref; - - strbuf_addf(&buf, "refs/remotes/%s/", rename->old_name); - if (starts_with(refname, buf.buf)) { - item = string_list_append(rename->remote_branches, refname); - symref = refs_resolve_ref_unsafe(get_main_ref_store(the_repository), - refname, RESOLVE_REF_READING, - NULL, &flag); - if (symref && (flag & REF_ISSYMREF)) { - item->util = xstrdup(symref); - rename->symrefs_nr++; - } else { - item->util = NULL; - } + struct strbuf new_refname = STRBUF_INIT; + struct strbuf identity = STRBUF_INIT; + struct strbuf name = STRBUF_INIT; + struct strbuf mail = STRBUF_INIT; + struct ident_split ident; + const char *date; + int error; + + compute_renamed_ref(rename, old_refname, &new_refname); + + if (split_ident_line(&ident, committer, strlen(committer)) < 0) { + error = -1; + goto out; } - strbuf_release(&buf); - return 0; + strbuf_add(&name, ident.name_begin, ident.name_end - ident.name_begin); + strbuf_add(&mail, ident.mail_begin, ident.mail_end - ident.mail_begin); + + date = show_date(timestamp, tz, DATE_MODE(NORMAL)); + strbuf_addstr(&identity, fmt_ident(name.buf, mail.buf, + WANT_BLANK_IDENT, date, 0)); + + error = ref_transaction_update_reflog(rename->transaction, new_refname.buf, + new_oid, old_oid, identity.buf, msg, + rename->index++, rename->err); + +out: + strbuf_release(&new_refname); + strbuf_release(&identity); + strbuf_release(&name); + strbuf_release(&mail); + return error; +} + +static int rename_one_reflog(const char *old_refname, + const struct object_id *old_oid, + struct rename_info *rename) +{ + struct strbuf new_refname = STRBUF_INIT; + struct strbuf message = STRBUF_INIT; + int error; + + if (!refs_reflog_exists(get_main_ref_store(the_repository), old_refname)) + return 0; + + error = refs_for_each_reflog_ent(get_main_ref_store(the_repository), + old_refname, rename_one_reflog_entry, rename); + if (error < 0) + goto out; + + compute_renamed_ref(rename, old_refname, &new_refname); + + /* + * Manually write the reflog entry for the now-renamed ref. We cannot + * rely on `rename_one_ref()` to do this for us as that would screw + * over order in which reflog entries are being written. + * + * Furthermore, we only append the entry in case the reference + * resolves. Missing references shouldn't have reflogs anyway. + */ + strbuf_addf(&message, "remote: renamed %s to %s", old_refname, + new_refname.buf); + + error = ref_transaction_update_reflog(rename->transaction, new_refname.buf, + old_oid, old_oid, git_committer_info(0), + message.buf, rename->index++, rename->err); + if (error < 0) + return error; + +out: + strbuf_release(&new_refname); + strbuf_release(&message); + return error; +} + +static int rename_one_ref(const char *old_refname, const char *referent, + const struct object_id *oid, + int flags, void *cb_data) +{ + struct strbuf new_referent = STRBUF_INIT; + struct strbuf new_refname = STRBUF_INIT; + struct rename_info *rename = cb_data; + const char *ptr = old_refname; + int error; + + if (!skip_prefix(ptr, "refs/remotes/", &ptr) || + !skip_prefix(ptr, rename->old_name, &ptr) || + !skip_prefix(ptr, "/", &ptr)) { + error = 0; + goto out; + } + + compute_renamed_ref(rename, old_refname, &new_refname); + + if (flags & REF_ISSYMREF) { + /* + * Stupidly enough `referent` is not pointing to the immediate + * target of a symref, but it's the recursively resolved value. + * So symrefs pointing to symrefs would be misresolved, and + * unborn symrefs don't have any value for the `referent` at all. + */ + referent = refs_resolve_ref_unsafe(get_main_ref_store(the_repository), + old_refname, RESOLVE_REF_NO_RECURSE, + NULL, NULL); + compute_renamed_ref(rename, referent, &new_referent); + oid = NULL; + } + + error = ref_transaction_delete(rename->transaction, old_refname, + oid, referent, REF_NO_DEREF, NULL, rename->err); + if (error < 0) + goto out; + + error = ref_transaction_update(rename->transaction, new_refname.buf, oid, null_oid(the_hash_algo), + (flags & REF_ISSYMREF) ? new_referent.buf : NULL, NULL, + REF_SKIP_CREATE_REFLOG | REF_NO_DEREF | REF_SKIP_OID_VERIFICATION, + NULL, rename->err); + if (error < 0) + goto out; + + error = rename_one_reflog(old_refname, oid, rename); + if (error < 0) + goto out; + + display_progress(rename->progress, ++rename->progress_nr); + +out: + strbuf_release(&new_referent); + strbuf_release(&new_refname); + return error; } static int migrate_file(struct remote *remote) @@ -727,6 +855,14 @@ static void handle_push_default(const char* old_name, const char* new_name) strbuf_release(&push_default.origin); } +static const char conflicting_remote_refs_advice[] = N_( + "The remote you are trying to rename has conflicting references in the\n" + "new target refspec. This is most likely caused by you trying to nest\n" + "a remote into itself, e.g. by renaming 'parent' into 'parent/child'\n" + "or by unnesting a remote, e.g. the other way round.\n" + "\n" + "If that is the case, you can address this by first renaming the\n" + "remote to a different name.\n"); static int mv(int argc, const char **argv, const char *prefix, struct repository *repo UNUSED) @@ -738,11 +874,11 @@ static int mv(int argc, const char **argv, const char *prefix, }; struct remote *oldremote, *newremote; struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT, buf3 = STRBUF_INIT, - old_remote_context = STRBUF_INIT; - struct string_list remote_branches = STRING_LIST_INIT_DUP; - struct rename_info rename; - int refs_renamed_nr = 0, refspecs_need_update = 0; - struct progress *progress = NULL; + old_remote_context = STRBUF_INIT, err = STRBUF_INIT; + struct rename_info rename = { + .err = &err, + }; + int refspecs_need_update = 0; int result = 0; argc = parse_options(argc, argv, prefix, options, @@ -753,8 +889,6 @@ static int mv(int argc, const char **argv, const char *prefix, rename.old_name = argv[0]; rename.new_name = argv[1]; - rename.remote_branches = &remote_branches; - rename.symrefs_nr = 0; oldremote = remote_get(rename.old_name); if (!remote_is_configured(oldremote, 1)) { @@ -788,6 +922,30 @@ static int mv(int argc, const char **argv, const char *prefix, refspecs_need_update = !!strstr(oldremote->fetch.items[i].raw, old_remote_context.buf); + if (refspecs_need_update) { + rename.transaction = ref_store_transaction_begin(get_main_ref_store(the_repository), + 0, &err); + if (!rename.transaction) + goto out; + + if (show_progress) + rename.progress = start_delayed_progress(the_repository, + _("Renaming remote references"), 0); + + result = refs_for_each_rawref(get_main_ref_store(the_repository), + rename_one_ref, &rename); + if (result < 0) + die(_("queueing remote ref renames failed: %s"), rename.err->buf); + + result = ref_transaction_prepare(rename.transaction, &err); + if (result < 0) { + error("renaming remote references failed: %s", err.buf); + if (result == REF_TRANSACTION_ERROR_NAME_CONFLICT) + advise(conflicting_remote_refs_advice); + die(NULL); + } + } + if (oldremote->fetch.nr) { strbuf_reset(&buf); strbuf_addf(&buf, "remote.%s.fetch", rename.new_name); @@ -829,83 +987,23 @@ static int mv(int argc, const char **argv, const char *prefix, } } - if (!refspecs_need_update) - goto out; - - /* - * First remove symrefs, then rename the rest, finally create - * the new symrefs. - */ - refs_for_each_ref(get_main_ref_store(the_repository), - read_remote_branches, &rename); - if (show_progress) { - /* - * Count symrefs twice, since "renaming" them is done by - * deleting and recreating them in two separate passes. - */ - progress = start_progress(the_repository, - _("Renaming remote references"), - rename.remote_branches->nr + rename.symrefs_nr); - } - for (size_t i = 0; i < remote_branches.nr; i++) { - struct string_list_item *item = remote_branches.items + i; - struct strbuf referent = STRBUF_INIT; - - if (refs_read_symbolic_ref(get_main_ref_store(the_repository), item->string, - &referent)) - continue; - if (refs_delete_ref(get_main_ref_store(the_repository), NULL, item->string, NULL, REF_NO_DEREF)) - die(_("deleting '%s' failed"), item->string); - - strbuf_release(&referent); - display_progress(progress, ++refs_renamed_nr); - } - for (size_t i = 0; i < remote_branches.nr; i++) { - struct string_list_item *item = remote_branches.items + i; + if (refspecs_need_update) { + result = ref_transaction_commit(rename.transaction, &err); + if (result < 0) + die(_("renaming remote refs failed: %s"), rename.err->buf); - if (item->util) - continue; - strbuf_reset(&buf); - strbuf_addstr(&buf, item->string); - strbuf_splice(&buf, strlen("refs/remotes/"), strlen(rename.old_name), - rename.new_name, strlen(rename.new_name)); - strbuf_reset(&buf2); - strbuf_addf(&buf2, "remote: renamed %s to %s", - item->string, buf.buf); - if (refs_rename_ref(get_main_ref_store(the_repository), item->string, buf.buf, buf2.buf)) - die(_("renaming '%s' failed"), item->string); - display_progress(progress, ++refs_renamed_nr); - } - for (size_t i = 0; i < remote_branches.nr; i++) { - struct string_list_item *item = remote_branches.items + i; + stop_progress(&rename.progress); - if (!item->util) - continue; - strbuf_reset(&buf); - strbuf_addstr(&buf, item->string); - strbuf_splice(&buf, strlen("refs/remotes/"), strlen(rename.old_name), - rename.new_name, strlen(rename.new_name)); - strbuf_reset(&buf2); - strbuf_addstr(&buf2, item->util); - strbuf_splice(&buf2, strlen("refs/remotes/"), strlen(rename.old_name), - rename.new_name, strlen(rename.new_name)); - strbuf_reset(&buf3); - strbuf_addf(&buf3, "remote: renamed %s to %s", - item->string, buf.buf); - if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf)) - die(_("creating '%s' failed"), buf.buf); - display_progress(progress, ++refs_renamed_nr); + handle_push_default(rename.old_name, rename.new_name); } - stop_progress(&progress); - - handle_push_default(rename.old_name, rename.new_name); out: - string_list_clear(&remote_branches, 1); + ref_transaction_free(rename.transaction); strbuf_release(&old_remote_context); strbuf_release(&buf); strbuf_release(&buf2); strbuf_release(&buf3); + strbuf_release(&err); return result; } diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh index 2701eef85e9ef8..e592c0bcde91e9 100755 --- a/t/t5505-remote.sh +++ b/t/t5505-remote.sh @@ -1658,4 +1658,77 @@ test_expect_success 'forbid adding superset of existing remote' ' test_grep ".outer. is a superset of existing remote .outer/inner." err ' +test_expect_success 'rename handles unborn HEAD' ' + test_when_finished "git remote remove unborn-renamed" && + git remote add unborn url && + git symbolic-ref refs/remotes/unborn/HEAD refs/remotes/unborn/nonexistent && + git remote rename unborn unborn-renamed && + git symbolic-ref refs/remotes/unborn-renamed/HEAD >actual && + echo refs/remotes/unborn-renamed/nonexistent >expected && + test_cmp expected actual +' + +test_expect_success 'rename can nest a remote into itself' ' + test_commit parent-commit && + COMMIT_ID=$(git rev-parse HEAD) && + test_when_finished "git remote remove parent || true" && + git remote add parent url && + git update-ref refs/remotes/parent/branch $COMMIT_ID && + test_when_finished "git remote remove parent/child" && + git remote rename parent parent/child && + git for-each-ref refs/remotes/ >actual && + printf "$COMMIT_ID commit\trefs/remotes/parent/child/branch\n" >expected && + test_cmp expected actual +' + +test_expect_success 'rename can nest a remote into itself with a conflicting branch name' ' + test_commit parent-conflict && + COMMIT_ID=$(git rev-parse HEAD) && + test_when_finished "git remote remove parent || true" && + git remote add parent url && + git update-ref refs/remotes/parent/child $COMMIT_ID && + test_when_finished "git remote remove parent/child" && + test_must_fail git remote rename parent parent/child 2>err && + test_grep "renaming remote references failed" err && + test_grep "The remote you are trying to rename has conflicting references" err && + git for-each-ref refs/remotes/ >actual && + printf "$COMMIT_ID commit\trefs/remotes/parent/child\n" >expected && + test_cmp expected actual +' + +test_expect_success 'rename can unnest a remote' ' + test_commit parent-child-commit && + COMMIT_ID=$(git rev-parse HEAD) && + test_when_finished "git remote remove parent/child || true" && + git remote add parent/child url && + git update-ref refs/remotes/parent/child/branch $COMMIT_ID && + git remote rename parent/child parent && + git for-each-ref refs/remotes/ >actual && + printf "$COMMIT_ID commit\trefs/remotes/parent/branch\n" >expected && + test_cmp expected actual +' + +test_expect_success 'rename moves around the reflog' ' + test_commit reflog-old && + COMMIT_ID=$(git rev-parse HEAD) && + test_config core.logAllRefUpdates true && + test_when_finished "git remote remove reflog-old || true" && + git remote add reflog-old url && + git update-ref refs/remotes/reflog-old/branch $COMMIT_ID && + test-tool ref-store main for-each-reflog >actual && + test_grep refs/remotes/reflog-old/branch actual && + test-tool ref-store main for-each-reflog-ent refs/remotes/reflog-old/branch >reflog-entries-old && + test_line_count = 1 reflog-entries-old && + git remote rename reflog-old reflog-new && + test-tool ref-store main for-each-reflog >actual && + test_grep ! refs/remotes/reflog-old actual && + test_grep refs/remotes/reflog-new/branch actual && + test-tool ref-store main for-each-reflog-ent refs/remotes/reflog-new/branch >reflog-entries-new && + cat >expect <<-EOF && + $(cat reflog-entries-old) + $COMMIT_ID $COMMIT_ID $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1112912173 -0700 remote: renamed refs/remotes/reflog-old/branch to refs/remotes/reflog-new/branch + EOF + test_cmp expect reflog-entries-new +' + test_done From 16c4fa26b99e6f6c24dc93575ffa884c13b1fe5f Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 31 Jul 2025 16:56:54 +0200 Subject: [PATCH 034/695] builtin/remote: only iterate through refs that are to be renamed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When renaming a remote we also need to rename all references accordingly. But while we only need to rename references that are contained in the "refs/remotes/$OLDNAME/" namespace, we end up using `refs_for_each_rawref()` that iterates through _all_ references. We know to exit early in the callback in case we see an irrelevant reference, but ultimately this is still a waste of compute as we knowingly iterate through references that we won't ever care about. Improve this by using `refs_for_each_rawref_in()`, which knows to only iterate through (potentially broken) references in a given prefix. The following benchmark renames a remote with a single reference in a repository that has 100k unrelated references. This shows a sizeable improvement with the "files" backend: Benchmark 1: rename remote (refformat = files, revision = HEAD~) Time (mean ± σ): 42.6 ms ± 0.9 ms [User: 29.1 ms, System: 8.4 ms] Range (min … max): 40.1 ms … 43.3 ms 10 runs Benchmark 2: rename remote (refformat = files, revision = HEAD) Time (mean ± σ): 31.7 ms ± 4.0 ms [User: 19.6 ms, System: 6.9 ms] Range (min … max): 27.1 ms … 36.0 ms 10 runs Summary rename remote (refformat = files, revision = HEAD) ran 1.35 ± 0.17 times faster than rename remote (refformat = files, revision = HEAD~) The "reftable" backend shows roughly the same absolute improvement, but given that it's already significantly faster than the "files" backend this translates to a much larger relative improvement: Benchmark 1: rename remote (refformat = reftable, revision = HEAD~) Time (mean ± σ): 18.2 ms ± 0.5 ms [User: 12.7 ms, System: 3.0 ms] Range (min … max): 17.3 ms … 21.4 ms 110 runs Benchmark 2: rename remote (refformat = reftable, revision = HEAD) Time (mean ± σ): 8.8 ms ± 0.5 ms [User: 3.8 ms, System: 2.9 ms] Range (min … max): 7.5 ms … 9.9 ms 167 runs Summary rename remote (refformat = reftable, revision = HEAD) ran 2.07 ± 0.12 times faster than rename remote (refformat = reftable, revision = HEAD~) Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/remote.c | 13 ++++--------- refs.c | 8 +++++++- refs.h | 2 ++ 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/builtin/remote.c b/builtin/remote.c index db481f39bc9af2..60e67f1b74c463 100644 --- a/builtin/remote.c +++ b/builtin/remote.c @@ -720,16 +720,8 @@ static int rename_one_ref(const char *old_refname, const char *referent, struct strbuf new_referent = STRBUF_INIT; struct strbuf new_refname = STRBUF_INIT; struct rename_info *rename = cb_data; - const char *ptr = old_refname; int error; - if (!skip_prefix(ptr, "refs/remotes/", &ptr) || - !skip_prefix(ptr, rename->old_name, &ptr) || - !skip_prefix(ptr, "/", &ptr)) { - error = 0; - goto out; - } - compute_renamed_ref(rename, old_refname, &new_refname); if (flags & REF_ISSYMREF) { @@ -932,7 +924,10 @@ static int mv(int argc, const char **argv, const char *prefix, rename.progress = start_delayed_progress(the_repository, _("Renaming remote references"), 0); - result = refs_for_each_rawref(get_main_ref_store(the_repository), + strbuf_reset(&buf); + strbuf_addf(&buf, "refs/remotes/%s/", rename.old_name); + + result = refs_for_each_rawref_in(get_main_ref_store(the_repository), buf.buf, rename_one_ref, &rename); if (result < 0) die(_("queueing remote ref renames failed: %s"), rename.err->buf); diff --git a/refs.c b/refs.c index 04c9ace7932ace..7e2f02dddf8b72 100644 --- a/refs.c +++ b/refs.c @@ -1839,7 +1839,13 @@ int refs_for_each_namespaced_ref(struct ref_store *refs, int refs_for_each_rawref(struct ref_store *refs, each_ref_fn fn, void *cb_data) { - return do_for_each_ref(refs, "", NULL, fn, 0, + return refs_for_each_rawref_in(refs, "", fn, cb_data); +} + +int refs_for_each_rawref_in(struct ref_store *refs, const char *prefix, + each_ref_fn fn, void *cb_data) +{ + return do_for_each_ref(refs, prefix, NULL, fn, 0, DO_FOR_EACH_INCLUDE_BROKEN, cb_data); } diff --git a/refs.h b/refs.h index 0bf50ce25cc067..19fb1d924adc1b 100644 --- a/refs.h +++ b/refs.h @@ -428,6 +428,8 @@ int refs_for_each_namespaced_ref(struct ref_store *refs, /* can be used to learn about broken ref and symref */ int refs_for_each_rawref(struct ref_store *refs, each_ref_fn fn, void *cb_data); +int refs_for_each_rawref_in(struct ref_store *refs, const char *prefix, + each_ref_fn fn, void *cb_data); /* * Iterates over all refs including root refs, i.e. pseudorefs and HEAD. From 9bb4abe6cd1b25107e6cd49af7a200242fd91f90 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 7 Aug 2025 22:52:56 +0200 Subject: [PATCH 035/695] combine-diff: zero memory used for callback filepairs In commit 25e5e2bf85 (combine-diff: support format_callback, 2011-08-19), the combined-diff code learned how to make a multi-sourced `diff_filepair` to pass to a diff callback. When we create each filepair, we do not bother to fill in many of the fields, because they would make no sense (e.g. there can be no rename score or broken_pair flag because we do not go through the diffcore filters). However, we did not even bother to zero them, leading to random values. Let's make sure everything is blank with xcalloc(), just as the regular diff code does. We would potentially want to set the `status` flag to something non-zero, but it is not clear to what. Possibly a new DIFF_STATUS_COMBINED would make sense, as this is not strictly a modification, nor does it fit any other category. Since it is not yet clear what callers would want, this patch simply leaves it as `0`, the same empty flag that is seen when `diffcore_std` is not used at all. Signed-off-by: Jeff King Signed-off-by: Toon Claes Signed-off-by: Junio C Hamano --- combine-diff.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/combine-diff.c b/combine-diff.c index dfae9f7995da51..5d6bdf436442c4 100644 --- a/combine-diff.c +++ b/combine-diff.c @@ -1315,7 +1315,7 @@ static struct diff_filepair *combined_pair(struct combine_diff_path *p, struct diff_filepair *pair; struct diff_filespec *pool; - pair = xmalloc(sizeof(*pair)); + CALLOC_ARRAY(pair, 1); CALLOC_ARRAY(pool, st_add(num_parent, 1)); pair->one = pool + 1; pair->two = pool; From 2a43e0e5503f52fd4c06faddf6c83b5678dedfe3 Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Thu, 7 Aug 2025 22:52:57 +0200 Subject: [PATCH 036/695] within_depth: fix return for empty path The within_depth() function is used to check whether pathspecs limited by a max-depth parameter are acceptable. It takes a path to check, a maximum depth, and a "base" depth. It counts the components in the path (by counting slashes), adds them to the base, and compares them to the maximum. However, if the base does not have any slashes at all, we always return `true`. If the base depth is 0, then this is correct; no matter what the maximum is, we are always within it. However, if the base depth is greater than 0, then we might return an erroneous result. This ends up not causing any user-visible bugs in the current code. The call sites in dir.c always pass a base depth of 0, so are unaffected. But tree_entry_interesting() uses this function differently: it will pass the prefix of the current entry, along with a `1` if the entry is a directory, in essence checking whether items inside the entry would be of interest. It turns out not to make a difference in behavior, but the reasoning is complex. Given a tree like: file a/file a/b/file walking the tree and calling tree_entry_interesting() will yield the following results: (with max_depth=0): file: yes a: yes a/file: no a/b: no (with max_depth=1): file: yes a: yes a/file: yes a/b: no So we have inconsistent behavior in considering directories interesting. If they are at the edge of our depth but at the root, we will recurse into them, but then find all of their entries uninteresting (e.g., in the first case, we will look at "a" but find "a/*" uninteresting). But if they are at the edge of our depth and not at the root, then we will not recurse (in the second example, we do not even bother entering "a/b"). This turns out not to matter because the only caller which uses max-depth pathspecs is cmd_grep(), which only cares about blob entries. From its perspective, it is exactly the same to not recurse into a subtree, or to recurse and find that it contains no matching entries. Not recursing is merely an optimization. It is debatable whether tree_entry_interesting() should consider such an entry interesting. The only caller does not care if it sees the tree itself, and can benefit from the optimization. But if we add a "max-depth" limiter to regular diffs, then a diff with DIFF_OPT_TREE_IN_RECURSIVE would probably want to show the tree itself, but not what it contains. This patch just fixes within_depth(), which means we consider such entries uninteresting (and makes the current caller happy). If we want to change that in the future, then this fix is still the correct first step, as the current behavior is simply inconsistent. This has the effect the function tree_entry_interesting() now behaves like following on the first example: (with max_depth=0): file: yes a: no a/file: no a/b: no Meaning we won't step in "a/" no more to realize all "a/*" entries are uninterested, but we stop at the tree entry itself. Based-on-patch-by: Jeff King Signed-off-by: Toon Claes Signed-off-by: Junio C Hamano --- Makefile | 1 + dir.c | 2 +- t/meson.build | 1 + t/unit-tests/u-dir.c | 47 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 t/unit-tests/u-dir.c diff --git a/Makefile b/Makefile index 70d1543b6b8688..b5fce1205d9e51 100644 --- a/Makefile +++ b/Makefile @@ -1356,6 +1356,7 @@ THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/% THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/% CLAR_TEST_SUITES += u-ctype +CLAR_TEST_SUITES += u-dir CLAR_TEST_SUITES += u-example-decorate CLAR_TEST_SUITES += u-hash CLAR_TEST_SUITES += u-hashmap diff --git a/dir.c b/dir.c index a374972b6243b6..2ee108eeb6d5d6 100644 --- a/dir.c +++ b/dir.c @@ -277,7 +277,7 @@ int within_depth(const char *name, int namelen, if (depth > max_depth) return 0; } - return 1; + return depth <= max_depth; } /* diff --git a/t/meson.build b/t/meson.build index d052fc3e23d2ec..56ea96f04ade18 100644 --- a/t/meson.build +++ b/t/meson.build @@ -1,5 +1,6 @@ clar_test_suites = [ 'unit-tests/u-ctype.c', + 'unit-tests/u-dir.c', 'unit-tests/u-example-decorate.c', 'unit-tests/u-hash.c', 'unit-tests/u-hashmap.c', diff --git a/t/unit-tests/u-dir.c b/t/unit-tests/u-dir.c new file mode 100644 index 00000000000000..2d0adaa39ed3d2 --- /dev/null +++ b/t/unit-tests/u-dir.c @@ -0,0 +1,47 @@ +#include "unit-test.h" +#include "dir.h" + +#define TEST_WITHIN_DEPTH(path, depth, max_depth, expect) do { \ + int actual = within_depth(path, strlen(path), \ + depth, max_depth); \ + if (actual != expect) \ + cl_failf("path '%s' with depth '%d' and max-depth '%d': expected %d, got %d", \ + path, depth, max_depth, expect, actual); \ + } while (0) + +void test_dir__within_depth(void) +{ + /* depth = 0; max_depth = 0 */ + TEST_WITHIN_DEPTH("", 0, 0, 1); + TEST_WITHIN_DEPTH("file", 0, 0, 1); + TEST_WITHIN_DEPTH("a", 0, 0, 1); + TEST_WITHIN_DEPTH("a/file", 0, 0, 0); + TEST_WITHIN_DEPTH("a/b", 0, 0, 0); + TEST_WITHIN_DEPTH("a/b/file", 0, 0, 0); + + /* depth = 0; max_depth = 1 */ + TEST_WITHIN_DEPTH("", 0, 1, 1); + TEST_WITHIN_DEPTH("file", 0, 1, 1); + TEST_WITHIN_DEPTH("a", 0, 1, 1); + TEST_WITHIN_DEPTH("a/file", 0, 1, 1); + TEST_WITHIN_DEPTH("a/b", 0, 1, 1); + TEST_WITHIN_DEPTH("a/b/file", 0, 1, 0); + + /* depth = 1; max_depth = 1 */ + TEST_WITHIN_DEPTH("", 1, 1, 1); + TEST_WITHIN_DEPTH("file", 1, 1, 1); + TEST_WITHIN_DEPTH("a", 1, 1, 1); + TEST_WITHIN_DEPTH("a/file", 1, 1, 0); + TEST_WITHIN_DEPTH("a/b", 1, 1, 0); + TEST_WITHIN_DEPTH("a/b/file", 1, 1, 0); + + /* depth = 1; max_depth = 0 */ + TEST_WITHIN_DEPTH("", 1, 0, 0); + TEST_WITHIN_DEPTH("file", 1, 0, 0); + TEST_WITHIN_DEPTH("a", 1, 0, 0); + TEST_WITHIN_DEPTH("a/file", 1, 0, 0); + TEST_WITHIN_DEPTH("a/b", 1, 0, 0); + TEST_WITHIN_DEPTH("a/b/file", 1, 0, 0); + + +} From a1dfa5448d583bbfd1ec45642a4495ad499970c9 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 7 Aug 2025 22:52:58 +0200 Subject: [PATCH 037/695] diff: teach tree-diff a max-depth parameter When you are doing a tree-diff, there are basically two options: do not recurse into subtrees at all, or recurse indefinitely. While most callers would want to always recurse and see full pathnames, some may want the efficiency of looking only at a particular level of the tree. This is currently easy to do for the top-level (just turn off recursion), but you cannot say "show me what changed in subdir/, but do not recurse". This patch adds a max-depth parameter which is measured from the closest pathspec match, so that you can do: git log --raw --max-depth=1 -- a/b/c and see the raw output for a/b/c/, but not those of a/b/c/d/ (instead of the raw output you would see for a/b/c/d). Co-authored-by: Toon Claes Signed-off-by: Toon Claes Signed-off-by: Junio C Hamano --- Documentation/diff-options.adoc | 28 ++++++++ diff-lib.c | 5 ++ diff.c | 24 +++++++ diff.h | 8 +++ t/meson.build | 1 + t/t4072-diff-max-depth.sh | 116 ++++++++++++++++++++++++++++++++ tree-diff.c | 78 ++++++++++++++++++++- 7 files changed, 257 insertions(+), 3 deletions(-) create mode 100755 t/t4072-diff-max-depth.sh diff --git a/Documentation/diff-options.adoc b/Documentation/diff-options.adoc index 640eb6e7db58a5..18a902038938f3 100644 --- a/Documentation/diff-options.adoc +++ b/Documentation/diff-options.adoc @@ -887,5 +887,33 @@ endif::git-format-patch[] reverted with `--ita-visible-in-index`. Both options are experimental and could be removed in future. +--max-depth=:: + For each pathspec given on command line, descend at most `` + levels of directories. A value of `-1` means no limit. + Cannot be combined with wildcards in the pathspec. + Given a tree containing `foo/bar/baz`, the following list shows the + matches generated by each set of options: ++ +-- + - `--max-depth=0 -- foo`: `foo` + + - `--max-depth=1 -- foo`: `foo/bar` + + - `--max-depth=1 -- foo/bar`: `foo/bar/baz` + + - `--max-depth=1 -- foo foo/bar`: `foo/bar/baz` + + - `--max-depth=2 -- foo`: `foo/bar/baz` +-- ++ +If no pathspec is given, the depth is measured as if all +top-level entries were specified. Note that this is different +than measuring from the root, in that `--max-depth=0` would +still return `foo`. This allows you to still limit depth while +asking for a subset of the top-level entries. ++ +Note that this option is only supported for diffs between tree objects, +not against the index or working tree. + For more detailed explanation on these common options, see also linkgit:gitdiffcore[7]. diff --git a/diff-lib.c b/diff-lib.c index 244468dd1a2b30..b8f8f3bc312fbe 100644 --- a/diff-lib.c +++ b/diff-lib.c @@ -115,6 +115,9 @@ void run_diff_files(struct rev_info *revs, unsigned int option) uint64_t start = getnanotime(); struct index_state *istate = revs->diffopt.repo->index; + if (revs->diffopt.max_depth_valid) + die(_("max-depth is not supported for worktree diffs")); + diff_set_mnemonic_prefix(&revs->diffopt, "i/", "w/"); refresh_fsmonitor(istate); @@ -560,6 +563,8 @@ static int diff_cache(struct rev_info *revs, opts.dst_index = NULL; opts.pathspec = &revs->diffopt.pathspec; opts.pathspec->recursive = 1; + if (revs->diffopt.max_depth_valid) + die(_("max-depth is not supported for index diffs")); init_tree_desc(&t, &tree->object.oid, tree->buffer, tree->size); return unpack_trees(1, &t, &opts); diff --git a/diff.c b/diff.c index 90e8003dd11e4d..434627f2495eb0 100644 --- a/diff.c +++ b/diff.c @@ -4988,6 +4988,9 @@ void diff_setup_done(struct diff_options *options) options->filter = ~filter_bit[DIFF_STATUS_FILTER_AON]; options->filter &= ~options->filter_not; } + + if (options->pathspec.has_wildcard && options->max_depth_valid) + die("max-depth cannot be used with wildcard pathspecs"); } int parse_long_opt(const char *opt, const char **argv, @@ -5622,6 +5625,23 @@ static int diff_opt_rotate_to(const struct option *opt, const char *arg, int uns return 0; } +static int diff_opt_max_depth(const struct option *opt, + const char *arg, int unset) +{ + struct diff_options *options = opt->value; + + BUG_ON_OPT_NEG(unset); + + if (!git_parse_int(arg, &options->max_depth)) + return error(_("invalid value for '%s': '%s'"), + "--max-depth", arg); + + options->flags.recursive = 1; + options->max_depth_valid = options->max_depth >= 0; + + return 0; +} + /* * Consider adding new flags to __git_diff_common_options * in contrib/completion/git-completion.bash @@ -5894,6 +5914,10 @@ struct option *add_diff_options(const struct option *opts, OPT_CALLBACK_F(0, "diff-filter", options, N_("[(A|C|D|M|R|T|U|X|B)...[*]]"), N_("select files by diff type"), PARSE_OPT_NONEG, diff_opt_diff_filter), + OPT_CALLBACK_F(0, "max-depth", options, N_(""), + N_("maximum tree depth to recurse"), + PARSE_OPT_NONEG, diff_opt_max_depth), + { .type = OPTION_CALLBACK, .long_name = "output", diff --git a/diff.h b/diff.h index 62e5768a9a379e..bbced5f745cf67 100644 --- a/diff.h +++ b/diff.h @@ -404,6 +404,14 @@ struct diff_options { struct strmap *additional_path_headers; int no_free; + + /* + * The value '0' is a valid max-depth (for no recursion), and value '-1' + * also (for unlimited recursion), so the extra "valid" flag is used to + * determined whether the user specified option --max-depth. + */ + int max_depth; + int max_depth_valid; }; unsigned diff_filter_bit(char status); diff --git a/t/meson.build b/t/meson.build index 56ea96f04ade18..74d72bc5311f9e 100644 --- a/t/meson.build +++ b/t/meson.build @@ -503,6 +503,7 @@ integration_tests = [ 't4069-remerge-diff.sh', 't4070-diff-pairs.sh', 't4071-diff-minimal.sh', + 't4072-diff-max-depth.sh', 't4100-apply-stat.sh', 't4101-apply-nonl.sh', 't4102-apply-rename.sh', diff --git a/t/t4072-diff-max-depth.sh b/t/t4072-diff-max-depth.sh new file mode 100755 index 00000000000000..0fbf1321f76048 --- /dev/null +++ b/t/t4072-diff-max-depth.sh @@ -0,0 +1,116 @@ +#!/bin/sh + +test_description='check that diff --max-depth will limit recursion' +. ./test-lib.sh + +make_dir() { + mkdir -p "$1" && + echo "$2" >"$1/file" +} + +make_files() { + echo "$1" >file && + make_dir one "$1" && + make_dir one/two "$1" && + make_dir one/two/three "$1" +} + +test_expect_success 'setup' ' + git commit --allow-empty -m empty && + git tag empty && + make_files added && + git add . && + git commit -m added && + make_files modified && + git add . && + git commit -m modified && + make_files index && + git add . && + make_files worktree +' + +test_expect_success '--max-depth is disallowed with wildcard pathspecs' ' + test_must_fail git diff-tree --max-depth=0 HEAD^ HEAD -- "f*" +' + +check_one() { + type=$1; shift + args=$1; shift + path=$1; shift + depth=$1; shift + test_expect_${expect:-success} "diff-$type $args, path=$path, depth=$depth" " + for i in $*; do echo \$i; done >expect && + git diff-$type --max-depth=$depth --name-only $args -- $path >actual && + test_cmp expect actual + " +} + +# For tree comparisons, we expect to see subtrees at the boundary +# get their own entry. +check_trees() { + check_one tree "$*" '' 0 file one + check_one tree "$*" '' 1 file one/file one/two + check_one tree "$*" '' 2 file one/file one/two/file one/two/three + check_one tree "$*" '' 3 file one/file one/two/file one/two/three/file + check_one tree "$*" '' -1 file one/file one/two/file one/two/three/file + check_one tree "$*" one 0 one + check_one tree "$*" one 1 one/file one/two + check_one tree "$*" one 2 one/file one/two/file one/two/three + check_one tree "$*" one 3 one/file one/two/file one/two/three/file + check_one tree "$*" one/two 0 one/two + check_one tree "$*" one/two 1 one/two/file one/two/three + check_one tree "$*" one/two 2 one/two/file one/two/three/file + check_one tree "$*" one/two 2 one/two/file one/two/three/file + check_one tree "$*" one/two/three 0 one/two/three + check_one tree "$*" one/two/three 1 one/two/three/file +} + +# But for index comparisons, we do not store subtrees at all, so we do not +# expect them. +check_index() { + check_one "$@" '' 0 file + check_one "$@" '' 1 file one/file + check_one "$@" '' 2 file one/file one/two/file + check_one "$@" '' 3 file one/file one/two/file one/two/three/file + check_one "$@" one 0 + check_one "$@" one 1 one/file + check_one "$@" one 2 one/file one/two/file + check_one "$@" one 3 one/file one/two/file one/two/three/file + check_one "$@" one/two 0 + check_one "$@" one/two 1 one/two/file + check_one "$@" one/two 2 one/two/file one/two/three/file + check_one "$@" one/two/three 0 + check_one "$@" one/two/three 1 one/two/three/file + + # Value '-1' for '--max-depth is the same as recursion without limit, + # and thus should always succeed. + local expect= + check_one "$@" '' -1 file one/file one/two/file one/two/three/file +} + +# Check as a modification... +check_trees HEAD^ HEAD +# ...and as an addition... +check_trees empty HEAD +# ...and as a deletion. +check_trees HEAD empty + +# We currently only implement max-depth for trees. +expect=failure +# Check index against a tree +check_index index "--cached HEAD" +# and index against the worktree +check_index files "" +expect= + +test_expect_success 'find shortest path within embedded pathspecs' ' + cat >expect <<-\EOF && + one/file + one/two/file + one/two/three/file + EOF + git diff-tree --max-depth=2 --name-only HEAD^ HEAD -- one one/two >actual && + test_cmp expect actual +' + +test_done diff --git a/tree-diff.c b/tree-diff.c index e00fc2f450d116..5988148b602536 100644 --- a/tree-diff.c +++ b/tree-diff.c @@ -13,6 +13,7 @@ #include "tree-walk.h" #include "environment.h" #include "repository.h" +#include "dir.h" /* * Some mode bits are also used internally for computations. @@ -48,6 +49,73 @@ free((x)); \ } while(0) +/* Returns true if and only if "dir" is a leading directory of "path" */ +static int is_dir_prefix(const char *path, const char *dir, int dirlen) +{ + return !strncmp(path, dir, dirlen) && + (!path[dirlen] || path[dirlen] == '/'); +} + +static int check_recursion_depth(const struct strbuf *name, + const struct pathspec *ps, + int max_depth) +{ + int i; + + if (!ps->nr) + return within_depth(name->buf, name->len, 1, max_depth); + + /* + * We look through the pathspecs in reverse-sorted order, because we + * want to find the longest match first (e.g., "a/b" is better for + * checking depth than "a/b/c"). + */ + for (i = ps->nr - 1; i >= 0; i--) { + const struct pathspec_item *item = ps->items+i; + + /* + * If the name to match is longer than the pathspec, then we + * are only interested if the pathspec matches and we are + * within the allowed depth. + */ + if (name->len >= item->len) { + if (!is_dir_prefix(name->buf, item->match, item->len)) + continue; + return within_depth(name->buf + item->len, + name->len - item->len, + 1, max_depth); + } + + /* + * Otherwise, our name is shorter than the pathspec. We need to + * check if it is a prefix of the pathspec; if so, we must + * always recurse in order to process further (the resulting + * paths we find might or might not match our pathspec, but we + * cannot know until we recurse). + */ + if (is_dir_prefix(item->match, name->buf, name->len)) + return 1; + } + return 0; +} + +static int should_recurse(const struct strbuf *name, struct diff_options *opt) +{ + if (!opt->flags.recursive) + return 0; + if (!opt->max_depth_valid) + return 1; + + /* + * We catch this during diff_setup_done, but let's double-check + * against any internal munging. + */ + if (opt->pathspec.has_wildcard) + BUG("wildcard pathspecs are incompatible with max-depth"); + + return check_recursion_depth(name, &opt->pathspec, opt->max_depth); +} + static void ll_diff_tree_paths( struct combine_diff_path ***tail, const struct object_id *oid, const struct object_id **parents_oid, int nparent, @@ -170,9 +238,13 @@ static void emit_path(struct combine_diff_path ***tail, mode = 0; } - if (opt->flags.recursive && isdir) { - recurse = 1; - emitthis = opt->flags.tree_in_recursive; + if (isdir) { + strbuf_add(base, path, pathlen); + if (should_recurse(base, opt)) { + recurse = 1; + emitthis = opt->flags.tree_in_recursive; + } + strbuf_setlen(base, old_baselen); } if (emitthis) { From 39fc4085620b60f8a06239a249f6877111e5ac11 Mon Sep 17 00:00:00 2001 From: Usman Akinyemi Date: Fri, 8 Aug 2025 06:36:49 +0530 Subject: [PATCH 038/695] t/t1517: automate `git subcmd -h` tests outside a repository Replace manual `-h` tests with a loop over all subcommands using `git --list-cmds=main`. This ensures consistent coverage of `-h` behavior outside a repo and future-proofs the test by covering new commands automatically. Known exceptions are skipped or marked as expected failures. Suggested-by: Patrick Steinhardt Helped-by: Junio C Hamano Helped-by: D. Ben Knoble Signed-off-by: Usman Akinyemi Signed-off-by: Junio C Hamano --- t/t1517-outside-repo.sh | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh index 8f59b867f2701f..e9f6d03e1bc948 100755 --- a/t/t1517-outside-repo.sh +++ b/t/t1517-outside-repo.sh @@ -109,8 +109,6 @@ test_expect_success LIBCURL 'remote-http outside repository' ' test_expect_success 'update-server-info does not crash with -h' ' test_expect_code 129 git update-server-info -h >usage && - test_grep "[Uu]sage: git update-server-info " usage && - test_expect_code 129 nongit git update-server-info -h >usage && test_grep "[Uu]sage: git update-server-info " usage ' @@ -121,4 +119,32 @@ test_expect_success 'prune does not crash with -h' ' test_grep "[Uu]sage: git prune " usage ' +for cmd in $(git --list-cmds=main) +do + cmd=${cmd%.*} # strip .sh, .perl, etc. + case "$cmd" in + archimport | cvsexportcommit | cvsimport | cvsserver | daemon | \ + difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \ + http-backend | http-fetch | http-push | init-db | \ + merge-octopus | merge-one-file | merge-resolve | mergetool | \ + mktag | p4 | p4.py | pickaxe | remote-ftp | remote-ftps | \ + remote-http | remote-https | replay | send-email | \ + sh-i18n--envsubst | shell | show | stage | submodule | svn | \ + upload-archive--writer | upload-pack | web--browse | whatchanged) + expect_outcome=expect_failure ;; + *) + expect_outcome=expect_success ;; + esac + case "$cmd" in + instaweb) + prereq=PERL ;; + *) + prereq= ;; + esac + test_$expect_outcome $prereq "'git $cmd -h' outside a repository" ' + test_expect_code 129 nongit git $cmd -h >usage && + test_grep "[Uu]sage: git $cmd " usage + ' +done + test_done From 18aae638cbb7e6fe148b879c5b4e5ad4e5cc006d Mon Sep 17 00:00:00 2001 From: Usman Akinyemi Date: Fri, 8 Aug 2025 06:36:50 +0530 Subject: [PATCH 039/695] t5200: move `update-server-info -h` test from t1517 t1517 is now focused on testing subcommands outside a repository. Move the in-repo `-h` test for `update-server-info` to t5200, which covers this command. Suggested-by: Patrick Steinhardt Helped-by: Junio C Hamano Signed-off-by: Usman Akinyemi Signed-off-by: Junio C Hamano --- t/t1517-outside-repo.sh | 5 ----- t/t5200-update-server-info.sh | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh index e9f6d03e1bc948..4eba3f486d733d 100755 --- a/t/t1517-outside-repo.sh +++ b/t/t1517-outside-repo.sh @@ -107,11 +107,6 @@ test_expect_success LIBCURL 'remote-http outside repository' ' test_grep "^error: remote-curl" actual ' -test_expect_success 'update-server-info does not crash with -h' ' - test_expect_code 129 git update-server-info -h >usage && - test_grep "[Uu]sage: git update-server-info " usage -' - test_expect_success 'prune does not crash with -h' ' test_expect_code 129 git prune -h >usage && test_grep "[Uu]sage: git prune " usage && diff --git a/t/t5200-update-server-info.sh b/t/t5200-update-server-info.sh index 83659070559952..a551e955b5524b 100755 --- a/t/t5200-update-server-info.sh +++ b/t/t5200-update-server-info.sh @@ -46,4 +46,9 @@ test_expect_success 'midx does not create duplicate pack entries' ' test_must_be_empty dups ' +test_expect_success 'update-server-info does not crash with -h' ' + test_expect_code 129 git update-server-info -h >usage && + test_grep "[Uu]sage: git update-server-info " usage +' + test_done From 529a60a885c1f65ff0870f6d69915dd9d02d7ee9 Mon Sep 17 00:00:00 2001 From: Usman Akinyemi Date: Fri, 8 Aug 2025 06:36:51 +0530 Subject: [PATCH 040/695] t5304: move `prune -h` test from t1517 t1517 is now focused on testing subcommands outside a repository. Move the in-repo `-h` test for `prune` to t5304, which covers this command. Suggested-by: Patrick Steinhardt Signed-off-by: Usman Akinyemi Signed-off-by: Junio C Hamano --- t/t1517-outside-repo.sh | 7 ------- t/t5304-prune.sh | 5 +++++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh index 4eba3f486d733d..3dc602872a0037 100755 --- a/t/t1517-outside-repo.sh +++ b/t/t1517-outside-repo.sh @@ -107,13 +107,6 @@ test_expect_success LIBCURL 'remote-http outside repository' ' test_grep "^error: remote-curl" actual ' -test_expect_success 'prune does not crash with -h' ' - test_expect_code 129 git prune -h >usage && - test_grep "[Uu]sage: git prune " usage && - test_expect_code 129 nongit git prune -h >usage && - test_grep "[Uu]sage: git prune " usage -' - for cmd in $(git --list-cmds=main) do cmd=${cmd%.*} # strip .sh, .perl, etc. diff --git a/t/t5304-prune.sh b/t/t5304-prune.sh index 1f1f664871ece6..2be7cd30dece6e 100755 --- a/t/t5304-prune.sh +++ b/t/t5304-prune.sh @@ -364,4 +364,9 @@ test_expect_success 'gc.recentObjectsHook' ' git cat-file -p $BLOB ' +test_expect_success 'prune does not crash with -h' ' + test_expect_code 129 git prune -h >usage && + test_grep "[Uu]sage: git prune " usage +' + test_done From f175b349a579e19cde1afe87d3be0d1f8358853c Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Fri, 8 Aug 2025 11:59:42 +0200 Subject: [PATCH 041/695] t0450: fix test for out-of-tree builds When using Meson, builds are out-of-tree and $GIT_BUILD_DIR gets set to the path where the build output is landing. To locate the Documentation sources, test 't0450' was using that path. Modify test 't0450' to use `$GIT_SOURCE_DIR/Documentation` to find the documentation sources. Signed-off-by: Toon Claes Signed-off-by: Junio C Hamano --- t/t0450-txt-doc-vs-help.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/t0450-txt-doc-vs-help.sh b/t/t0450-txt-doc-vs-help.sh index 2f7504ae7e9090..da2d0af5b07810 100755 --- a/t/t0450-txt-doc-vs-help.sh +++ b/t/t0450-txt-doc-vs-help.sh @@ -41,7 +41,7 @@ help_to_synopsis () { } builtin_to_adoc () { - echo "$GIT_BUILD_DIR/Documentation/git-$1.adoc" + echo "$GIT_SOURCE_DIR/Documentation/git-$1.adoc" } adoc_to_synopsis () { From fe54b9ef02cc8c5499fa83f8ed51a614b1014c0b Mon Sep 17 00:00:00 2001 From: "D. Ben Knoble" Date: Sun, 3 Aug 2025 12:10:26 -0400 Subject: [PATCH 042/695] parse-options: refactor flags for usage_with_options_internal When reading or editing calls to usage_with_options_internal, it is difficult to tell what trailing "0, 0", "0, 1", "1, 0" arguments mean (NB there is never a "1, 1" case). Give the flags readable names to improve call-sites without changing any behavior. Signed-off-by: D. Ben Knoble Signed-off-by: Junio C Hamano --- parse-options.c | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/parse-options.c b/parse-options.c index 5224203ffe7bf8..169d76fb65594b 100644 --- a/parse-options.c +++ b/parse-options.c @@ -953,10 +953,16 @@ static void free_preprocessed_options(struct option *options) free(options); } +#define USAGE_NORMAL 0 +#define USAGE_FULL 1 +#define USAGE_TO_STDOUT 0 +#define USAGE_TO_STDERR 1 + static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t *, const char * const *, const struct option *, - int, int); + int full_usage, + int usage_to_stderr); enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx, const struct option *options, @@ -1088,7 +1094,8 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx, } if (internal_help && !strcmp(arg + 2, "help-all")) - return usage_with_options_internal(ctx, usagestr, options, 1, 0); + return usage_with_options_internal(ctx, usagestr, options, + USAGE_FULL, USAGE_TO_STDOUT); if (internal_help && !strcmp(arg + 2, "help")) goto show_usage; switch (parse_long_opt(ctx, arg + 2, options)) { @@ -1129,7 +1136,8 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx, return PARSE_OPT_DONE; show_usage: - return usage_with_options_internal(ctx, usagestr, options, 0, 0); + return usage_with_options_internal(ctx, usagestr, options, + USAGE_NORMAL, USAGE_TO_STDOUT); } int parse_options_end(struct parse_opt_ctx_t *ctx) @@ -1444,7 +1452,8 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t void NORETURN usage_with_options(const char * const *usagestr, const struct option *opts) { - usage_with_options_internal(NULL, usagestr, opts, 0, 1); + usage_with_options_internal(NULL, usagestr, opts, + USAGE_NORMAL, USAGE_TO_STDERR); exit(129); } @@ -1453,7 +1462,8 @@ void show_usage_with_options_if_asked(int ac, const char **av, const struct option *opts) { if (ac == 2 && !strcmp(av[1], "-h")) { - usage_with_options_internal(NULL, usagestr, opts, 0, 0); + usage_with_options_internal(NULL, usagestr, opts, + USAGE_NORMAL, USAGE_TO_STDOUT); exit(129); } } From 129b3632f35a1c46fb30d9e6f275a95119a9d521 Mon Sep 17 00:00:00 2001 From: "D. Ben Knoble" Date: Sun, 3 Aug 2025 12:10:27 -0400 Subject: [PATCH 043/695] builtin: also setup gently for --help-all Git experts often check the help summary of a command to make sure they spell options right when suggesting advice to colleagues. Further, they might check hidden options when responding to queries about deprecated options like git-rebase(1)'s "preserve merges" option. But some commands don't support "--help-all" outside of a git directory. Running (for example) git rebase --help-all outside a directory fails in "setup_git_directory", erroring with the localized form of fatal: not a git repository (or any of the parent directories): .git Like 99caeed05d (Let 'git -h' show usage without a git dir, 2009-11-09), we want to show the "--help-all" output even without a git dir. Make "--help-all" where we expect "-h" to mean "setup_git_directory_gently", and interpose early in the natural place ("show_usage_with_options_if_asked"). Do the same for usage callers with show_usage_if_asked. The exception is merge-recursive, whose help block doesn't use newer APIs. Best-viewed-with: --ignore-space-change Signed-off-by: D. Ben Knoble Signed-off-by: Junio C Hamano --- builtin/merge-recursive.c | 3 ++- git.c | 2 +- parse-options.c | 14 ++++++++++---- t/t1517-outside-repo.sh | 4 ++++ usage.c | 3 ++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/builtin/merge-recursive.c b/builtin/merge-recursive.c index 03b5100cfae918..17aa4db37abb59 100644 --- a/builtin/merge-recursive.c +++ b/builtin/merge-recursive.c @@ -38,7 +38,8 @@ int cmd_merge_recursive(int argc, if (argv[0] && ends_with(argv[0], "-subtree")) o.subtree_shift = ""; - if (argc == 2 && !strcmp(argv[1], "-h")) { + if (argc == 2 && (!strcmp(argv[1], "-h") || + !strcmp(argv[1], "--help-all"))) { struct strbuf msg = STRBUF_INIT; strbuf_addf(&msg, builtin_merge_recursive_usage, argv[0]); show_usage_if_asked(argc, argv, msg.buf); diff --git a/git.c b/git.c index 07a5fe39fb69f0..40d3df1b763ddf 100644 --- a/git.c +++ b/git.c @@ -445,7 +445,7 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv, struct const char *prefix; int run_setup = (p->option & (RUN_SETUP | RUN_SETUP_GENTLY)); - help = argc == 2 && !strcmp(argv[1], "-h"); + help = argc == 2 && (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help-all")); if (help && (run_setup & RUN_SETUP)) /* demote to GENTLY to allow 'git cmd -h' outside repo */ run_setup = RUN_SETUP_GENTLY; diff --git a/parse-options.c b/parse-options.c index 169d76fb65594b..d9f960b7b508e8 100644 --- a/parse-options.c +++ b/parse-options.c @@ -1461,10 +1461,16 @@ void show_usage_with_options_if_asked(int ac, const char **av, const char * const *usagestr, const struct option *opts) { - if (ac == 2 && !strcmp(av[1], "-h")) { - usage_with_options_internal(NULL, usagestr, opts, - USAGE_NORMAL, USAGE_TO_STDOUT); - exit(129); + if (ac == 2) { + if (!strcmp(av[1], "-h")) { + usage_with_options_internal(NULL, usagestr, opts, + USAGE_NORMAL, USAGE_TO_STDOUT); + exit(129); + } else if (!strcmp(av[1], "--help-all")) { + usage_with_options_internal(NULL, usagestr, opts, + USAGE_FULL, USAGE_TO_STDOUT); + exit(129); + } } } diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh index 3dc602872a0037..e34321dd446abf 100755 --- a/t/t1517-outside-repo.sh +++ b/t/t1517-outside-repo.sh @@ -133,6 +133,10 @@ do test_expect_code 129 nongit git $cmd -h >usage && test_grep "[Uu]sage: git $cmd " usage ' + test_$expect_outcome $prereq "'git $cmd --help-all' outside a repository" ' + test_expect_code 129 nongit git $cmd --help-all >usage && + test_grep "[Uu]sage: git $cmd " usage + ' done test_done diff --git a/usage.c b/usage.c index 81913236a4a2ab..4c245ba0cbaf01 100644 --- a/usage.c +++ b/usage.c @@ -192,7 +192,8 @@ static void show_usage_if_asked_helper(const char *err, ...) void show_usage_if_asked(int ac, const char **av, const char *err) { - if (ac == 2 && !strcmp(av[1], "-h")) + if (ac == 2 && (!strcmp(av[1], "-h") || + !strcmp(av[1], "--help-all"))) show_usage_if_asked_helper(err); } From 6d192462eb3ca605600731a9717fd0d9aa72eff0 Mon Sep 17 00:00:00 2001 From: Lidong Yan Date: Mon, 11 Aug 2025 14:01:37 +0800 Subject: [PATCH 044/695] bloom: enable bloom filter with wildcard pathspec in revision traversal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When traversing commits, a pathspec item can be used to limit the traversal to commits that modify the specified paths. And the commit-graph includes a Bloom filter to exclude commits that definitely did not modify a given pathspec item. During commit traversal, the Bloom filter can significantly improve performance. However, it is disabled if the specified pathspec item contains wildcard characters or magic signatures. For performance reason, enable Bloom filter even if a pathspec item contains wildcard characters by filtering only the non-wildcard part of the pathspec item. The function of pathspec magic signature is generally to narrow down the path specified by the pathspecs. So, enable Bloom filter when the magic signature is "top", "glob", "attr", "--depth" or "literal". "exclude" is used to select paths other than the specified path, rather than serving as a filtering function, so it cannot be used together with the Bloom filter. Since Bloom filter is not case insensitive even in case insensitive system (e.g. MacOS), it cannot be used together with "icase" magic. With this optimization, we get some improvements for pathspecs with wildcards or magic signatures. First, in the Git repository we see these modest results: git log -100 -- "t/*" Benchmark 1: new Time (mean ± σ): 20.4 ms ± 0.6 ms Range (min … max): 19.3 ms … 24.4 ms Benchmark 2: old Time (mean ± σ): 23.4 ms ± 0.5 ms Range (min … max): 22.5 ms … 24.7 ms git log -100 -- ":(top)t" Benchmark 1: new Time (mean ± σ): 16.2 ms ± 0.4 ms Range (min … max): 15.3 ms … 17.2 ms Benchmark 2: old Time (mean ± σ): 18.6 ms ± 0.5 ms Range (min … max): 17.6 ms … 20.4 ms But in a larger repo, such as the LLVM project repo below, we get even better results: git log -100 -- "libc/*" Benchmark 1: new Time (mean ± σ): 16.0 ms ± 0.6 ms Range (min … max): 14.7 ms … 17.8 ms Benchmark 2: old Time (mean ± σ): 26.7 ms ± 0.5 ms Range (min … max): 25.4 ms … 27.8 ms git log -100 -- ":(top)libc" Benchmark 1: new Time (mean ± σ): 15.6 ms ± 0.6 ms Range (min … max): 14.4 ms … 17.7 ms Benchmark 2: old Time (mean ± σ): 19.6 ms ± 0.5 ms Range (min … max): 18.6 ms … 20.6 ms Helped-by: Junio C Hamano Signed-off-by: Lidong Yan Signed-off-by: Junio C Hamano --- revision.c | 42 +++++++++++++++++++++++++++++------------- t/t4216-log-bloom.sh | 31 +++++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/revision.c b/revision.c index 18f300d4555552..7449064def03a0 100644 --- a/revision.c +++ b/revision.c @@ -671,12 +671,17 @@ static void trace2_bloom_filter_statistics_atexit(void) static int forbid_bloom_filters(struct pathspec *spec) { - if (spec->has_wildcard) - return 1; - if (spec->magic & ~PATHSPEC_LITERAL) + unsigned int allowed_magic = + PATHSPEC_FROMTOP | + PATHSPEC_MAXDEPTH | + PATHSPEC_LITERAL | + PATHSPEC_GLOB | + PATHSPEC_ATTR; + + if (spec->magic & ~allowed_magic) return 1; for (size_t nr = 0; nr < spec->nr; nr++) - if (spec->items[nr].magic & ~PATHSPEC_LITERAL) + if (spec->items[nr].magic & ~allowed_magic) return 1; return 0; @@ -691,23 +696,34 @@ static int convert_pathspec_to_bloom_keyvec(struct bloom_keyvec **out, char *path_alloc = NULL; const char *path; size_t len; - int res = 0; + int res = -1; + len = pi->nowildcard_len; + if (len != pi->len) { + /* + * for path like "dir/file*", nowildcard part would be + * "dir/file", but only "dir" should be used for the + * bloom filter. + */ + while (len > 0 && pi->match[len - 1] != '/') + len--; + } /* remove single trailing slash from path, if needed */ - if (pi->len > 0 && pi->match[pi->len - 1] == '/') { - path_alloc = xmemdupz(pi->match, pi->len - 1); + if (len > 0 && pi->match[len - 1] == '/') + len--; + + if (!len) + goto cleanup; + + if (len != pi->len) { + path_alloc = xmemdupz(pi->match, len); path = path_alloc; } else path = pi->match; - len = strlen(path); - if (!len) { - res = -1; - goto cleanup; - } - *out = bloom_keyvec_new(path, len, settings); + res = 0; cleanup: free(path_alloc); return res; diff --git a/t/t4216-log-bloom.sh b/t/t4216-log-bloom.sh index 639868ac562f9e..1064990de31413 100755 --- a/t/t4216-log-bloom.sh +++ b/t/t4216-log-bloom.sh @@ -154,11 +154,34 @@ test_expect_success 'git log with multiple literal paths uses Bloom filter' ' test_bloom_filters_used "-- file*" ' -test_expect_success 'git log with path contains a wildcard does not use Bloom filter' ' +test_expect_success 'git log with paths all contain non-wildcard part uses Bloom filter' ' + test_bloom_filters_used "-- A/\* file4" && + test_bloom_filters_used "-- A/file\*" && + test_bloom_filters_used "-- * A/\*" +' + +test_expect_success 'git log with path only contains wildcard part does not use Bloom filter' ' test_bloom_filters_not_used "-- file\*" && - test_bloom_filters_not_used "-- A/\* file4" && - test_bloom_filters_not_used "-- file4 A/\*" && - test_bloom_filters_not_used "-- * A/\*" + test_bloom_filters_not_used "-- file\* A/\*" && + test_bloom_filters_not_used "-- file\* *" && + test_bloom_filters_not_used "-- \*" +' + +test_expect_success 'git log with path contains various magic signatures' ' + cd A && + test_bloom_filters_used "-- \:\(top\)B" && + cd .. && + + test_bloom_filters_used "-- \:\(glob\)A/\*\*/C" && + test_bloom_filters_not_used "-- \:\(icase\)FILE4" && + test_bloom_filters_not_used "-- \:\(exclude\)A/B/C" && + + test_when_finished "rm -f .gitattributes" && + cat >.gitattributes <<-EOF && + A/file1 text + A/B/file2 -text + EOF + test_bloom_filters_used "-- \:\(attr\:text\)A" ' test_expect_success 'setup - add commit-graph to the chain without Bloom filters' ' From 621ce9c1c6cfe5a6467ef62cb81992b3a318b70e Mon Sep 17 00:00:00 2001 From: Greg Hurrell Date: Mon, 11 Aug 2025 11:55:23 +0000 Subject: [PATCH 045/695] git-jump: make `diff` work with filenames containing spaces In diff.c, we output a trailing "\t" at the end of any filename that contains a space: case DIFF_SYMBOL_FILEPAIR_PLUS: meta = diff_get_color_opt(o, DIFF_METAINFO); reset = diff_get_color_opt(o, DIFF_RESET); fprintf(o->file, "%s%s+++ %s%s%s\n", diff_line_prefix(o), meta, line, reset, strchr(line, ' ') ? "\t" : ""); break; That is, for a file "foo.txt", `git diff --no-prefix` will emit: +++ foo.txt but for "foo bar.txt" it will emit: +++ foo bar.txt\t This in turn leads `git-jump` to produce a quickfix format like this: foo bar.txt\t:1:1:contents Because no "foo bar.txt\t" file actually exists on disk, opening it in Vim will just land the user in an empty buffer. This commit takes the simple approach of unconditionally stripping any trailing tab. Consider the following three examples: 1. For file "foo", Git will emit "foo". 2. For file "foo bar", Git will emit "foo bar\t". 3. For file "foo\t", Git will emit "\"foo\t\"". 4. For file "foo bar\t", Git will emit "\"foo bar\t\"". Before this commit, `git-jump` correctly handled only case "1". After this commit, `git-jump` correctly handles cases "1" and "2". In reality, these are the only cases people are going to run into with any regularity, and the other two are rare edge cases, which probably aren't worth the effort to support unless somebody actually complains about them. Signed-off-by: Greg Hurrell Signed-off-by: Junio C Hamano --- contrib/git-jump/git-jump | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/git-jump/git-jump b/contrib/git-jump/git-jump index 3f696759617beb..8d1d5d79a69854 100755 --- a/contrib/git-jump/git-jump +++ b/contrib/git-jump/git-jump @@ -44,7 +44,7 @@ open_editor() { mode_diff() { git diff --no-prefix --relative "$@" | perl -ne ' - if (m{^\+\+\+ (.*)}) { $file = $1 eq "/dev/null" ? undef : $1; next } + if (m{^\+\+\+ (.*?)\t?$}) { $file = $1 eq "/dev/null" ? undef : $1; next } defined($file) or next; if (m/^@@ .*?\+(\d+)/) { $line = $1; next } defined($line) or next; From 9a49aef8dcdf899e94cddab14eacc7118c611524 Mon Sep 17 00:00:00 2001 From: Ayush Chandekar Date: Mon, 11 Aug 2025 05:15:45 +0530 Subject: [PATCH 046/695] environment: remove the global variable 'merge_log_config' The global variable 'merge_log_config', set via the "merge.log" or "merge.summary" settings, is only used in 'cmd_fmt_merge_msg()' and 'cmd_merge()' to adjust the 'shortlog_len' variable. Remove 'merge_log_config' globally and localize it in 'cmd_fmt_merge_msg()' and 'cmd_merge()'. Set its value by passing it in 'fmt_merge_msg_config()' by passing its pointer to the function via the callback parameter. This change is part of an ongoing effort to eliminate global variables, improve modularity and help libify the codebase. Mentored-by: Christian Couder Mentored-by: Ghanshyam Thakkar Signed-off-by: Ayush Chandekar Signed-off-by: Junio C Hamano --- builtin/fmt-merge-msg.c | 3 ++- builtin/merge.c | 3 ++- environment.c | 1 - fmt-merge-msg.c | 10 ++++++---- fmt-merge-msg.h | 1 - 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c index 3b6aac2cf7faab..4b24de32fb499a 100644 --- a/builtin/fmt-merge-msg.c +++ b/builtin/fmt-merge-msg.c @@ -19,6 +19,7 @@ int cmd_fmt_merge_msg(int argc, const char *message = NULL; char *into_name = NULL; int shortlog_len = -1; + int merge_log_config = -1; struct option options[] = { { .type = OPTION_INTEGER, @@ -53,7 +54,7 @@ int cmd_fmt_merge_msg(int argc, int ret; struct fmt_merge_msg_opts opts; - git_config(fmt_merge_msg_config, NULL); + git_config(fmt_merge_msg_config, &merge_log_config); argc = parse_options(argc, argv, prefix, options, fmt_merge_msg_usage, 0); if (argc > 0) diff --git a/builtin/merge.c b/builtin/merge.c index ce90e52fe451d6..1c921b12f5b7de 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -1316,6 +1316,7 @@ int cmd_merge(int argc, struct commit_list *remoteheads = NULL, *p; void *branch_to_free; int orig_argc = argc; + int merge_log_config = -1; show_usage_with_options_if_asked(argc, argv, builtin_merge_usage, builtin_merge_options); @@ -1334,7 +1335,7 @@ int cmd_merge(int argc, skip_prefix(branch, "refs/heads/", &branch); init_diff_ui_defaults(); - git_config(git_merge_config, NULL); + git_config(git_merge_config, &merge_log_config); if (!branch || is_null_oid(&head_oid)) head_commit = NULL; diff --git a/environment.c b/environment.c index c61d773e7e8ff0..2babf53aa3f0ad 100644 --- a/environment.c +++ b/environment.c @@ -67,7 +67,6 @@ int grafts_keep_true_parents; int core_apply_sparse_checkout; int core_sparse_checkout_cone; int sparse_expect_files_outside_of_patterns; -int merge_log_config = -1; int precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */ unsigned long pack_size_limit_cfg; int max_allowed_tree_depth = diff --git a/fmt-merge-msg.c b/fmt-merge-msg.c index 501b5acdd44c22..6bd395bb65d48f 100644 --- a/fmt-merge-msg.c +++ b/fmt-merge-msg.c @@ -26,13 +26,15 @@ static struct string_list suppress_dest_patterns = STRING_LIST_INIT_DUP; int fmt_merge_msg_config(const char *key, const char *value, const struct config_context *ctx, void *cb) { + int *merge_log_config = cb; + if (!strcmp(key, "merge.log") || !strcmp(key, "merge.summary")) { int is_bool; - merge_log_config = git_config_bool_or_int(key, value, ctx->kvi, &is_bool); - if (!is_bool && merge_log_config < 0) + *merge_log_config = git_config_bool_or_int(key, value, ctx->kvi, &is_bool); + if (!is_bool && *merge_log_config < 0) return error("%s: negative length %s", key, value); - if (is_bool && merge_log_config) - merge_log_config = DEFAULT_MERGE_LOG_LEN; + if (is_bool && *merge_log_config) + *merge_log_config = DEFAULT_MERGE_LOG_LEN; } else if (!strcmp(key, "merge.branchdesc")) { use_branch_desc = git_config_bool(key, value); } else if (!strcmp(key, "merge.suppressdest")) { diff --git a/fmt-merge-msg.h b/fmt-merge-msg.h index 73ca3e44652204..c066d837610fa5 100644 --- a/fmt-merge-msg.h +++ b/fmt-merge-msg.h @@ -12,7 +12,6 @@ struct fmt_merge_msg_opts { const char *into_name; }; -extern int merge_log_config; int fmt_merge_msg_config(const char *key, const char *value, const struct config_context *ctx, void *cb); int fmt_merge_msg(struct strbuf *in, struct strbuf *out, From 22d421fed9cd5757a9da5a97e5b53ded54e93fe9 Mon Sep 17 00:00:00 2001 From: Ayush Chandekar Date: Mon, 11 Aug 2025 05:15:46 +0530 Subject: [PATCH 047/695] builtin/fmt-merge-msg: stop depending on 'the_repository' Refactor builtin/fmt-merge-msg.c to remove the dependancy on the global 'the_repository'. Remove the 'UNUSED' macro from the 'struct repository' parameter and replace 'git_config()' with 'repo_config()' so that configuration is read from the passed repository. Also, add a test to make sure that "git fmt-merge-msg -h" can be called outside a repository. Mentored-by: Christian Couder Mentored-by: Ghanshyam Thakkar Signed-off-by: Ayush Chandekar Signed-off-by: Junio C Hamano --- builtin/fmt-merge-msg.c | 5 ++--- t/t1517-outside-repo.sh | 7 +++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c index 4b24de32fb499a..cf4273a52c2bae 100644 --- a/builtin/fmt-merge-msg.c +++ b/builtin/fmt-merge-msg.c @@ -1,4 +1,3 @@ -#define USE_THE_REPOSITORY_VARIABLE #include "builtin.h" #include "config.h" #include "fmt-merge-msg.h" @@ -13,7 +12,7 @@ static const char * const fmt_merge_msg_usage[] = { int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, - struct repository *repo UNUSED) + struct repository *repo) { char *inpath = NULL; const char *message = NULL; @@ -54,7 +53,7 @@ int cmd_fmt_merge_msg(int argc, int ret; struct fmt_merge_msg_opts opts; - git_config(fmt_merge_msg_config, &merge_log_config); + repo_config(repo, fmt_merge_msg_config, &merge_log_config); argc = parse_options(argc, argv, prefix, options, fmt_merge_msg_usage, 0); if (argc > 0) diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh index 6824581317411a..f6d3206cfe381f 100755 --- a/t/t1517-outside-repo.sh +++ b/t/t1517-outside-repo.sh @@ -114,4 +114,11 @@ test_expect_success 'update-server-info does not crash with -h' ' test_grep "[Uu]sage: git update-server-info " usage ' +test_expect_success 'fmt-merge-msg does not crash with -h' ' + test_expect_code 129 git fmt-merge-msg -h >usage && + test_grep "[Uu]sage: git fmt-merge-msg " usage && + test_expect_code 129 nongit git fmt-merge-msg -h >usage && + test_grep "[Uu]sage: git fmt-merge-msg " usage +' + test_done From 595bef7180b57889a4dec4b675a7fc6084c863ac Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 11 Aug 2025 15:46:41 +0200 Subject: [PATCH 048/695] odb: store locality in object database sources Object database sources are classified either as: - Local, which means that the source is the repository's primary source. This is typically ".git/objects". - Non-local, which is everything else. Most importantly this includes alternates and quarantine directories. This locality is often computed ad-hoc by checking whether a given object source is the first one. This works, but it is quite roundabout. Refactor the code so that we store locality when creating the sources in the first place. This makes it both more accessible and robust. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- midx.c | 5 +++-- midx.h | 2 +- odb.c | 1 + odb.h | 8 ++++++++ packfile.c | 9 ++++----- repository.c | 1 + 6 files changed, 18 insertions(+), 8 deletions(-) diff --git a/midx.c b/midx.c index 7d407682e60a6f..b9ca0915a67f10 100644 --- a/midx.c +++ b/midx.c @@ -723,7 +723,7 @@ int midx_preferred_pack(struct multi_pack_index *m, uint32_t *pack_int_id) return 0; } -int prepare_multi_pack_index_one(struct odb_source *source, int local) +int prepare_multi_pack_index_one(struct odb_source *source) { struct repository *r = source->odb->repo; @@ -734,7 +734,8 @@ int prepare_multi_pack_index_one(struct odb_source *source, int local) if (source->midx) return 1; - source->midx = load_multi_pack_index(r, source->path, local); + source->midx = load_multi_pack_index(r, source->path, + source->local); return !!source->midx; } diff --git a/midx.h b/midx.h index 076382de8acd26..28c426a8232997 100644 --- a/midx.h +++ b/midx.h @@ -122,7 +122,7 @@ int fill_midx_entry(struct repository *r, const struct object_id *oid, struct pa int midx_contains_pack(struct multi_pack_index *m, const char *idx_or_pack_name); int midx_preferred_pack(struct multi_pack_index *m, uint32_t *pack_int_id); -int prepare_multi_pack_index_one(struct odb_source *source, int local); +int prepare_multi_pack_index_one(struct odb_source *source); /* * Variant of write_midx_file which writes a MIDX containing only the packs diff --git a/odb.c b/odb.c index 1f48a0448e398a..1761a50840ddf8 100644 --- a/odb.c +++ b/odb.c @@ -176,6 +176,7 @@ static int link_alt_odb_entry(struct object_database *odb, CALLOC_ARRAY(alternate, 1); alternate->odb = odb; + alternate->local = false; /* pathbuf.buf is already in r->objects->source_by_path */ alternate->path = strbuf_detach(&pathbuf, NULL); diff --git a/odb.h b/odb.h index 09177bf430dc38..f9300439bab3af 100644 --- a/odb.h +++ b/odb.h @@ -63,6 +63,14 @@ struct odb_source { */ struct multi_pack_index *midx; + /* + * Figure out whether this is the local source of the owning + * repository, which would typically be its ".git/objects" directory. + * This local object directory is usually where objects would be + * written to. + */ + bool local; + /* * This is a temporary object store created by the tmp_objdir * facility. Disable ref updates since the objects in the store diff --git a/packfile.c b/packfile.c index 5d73932f50ce68..a38544b87bf2f1 100644 --- a/packfile.c +++ b/packfile.c @@ -935,14 +935,14 @@ static void prepare_pack(const char *full_name, size_t full_name_len, report_garbage(PACKDIR_FILE_GARBAGE, full_name); } -static void prepare_packed_git_one(struct odb_source *source, int local) +static void prepare_packed_git_one(struct odb_source *source) { struct string_list garbage = STRING_LIST_INIT_DUP; struct prepare_pack_data data = { .m = source->midx, .r = source->odb->repo, .garbage = &garbage, - .local = local, + .local = source->local, }; for_each_file_in_pack_dir(source->path, prepare_pack, &data); @@ -1037,9 +1037,8 @@ static void prepare_packed_git(struct repository *r) odb_prepare_alternates(r->objects); for (source = r->objects->sources; source; source = source->next) { - int local = (source == r->objects->sources); - prepare_multi_pack_index_one(source, local); - prepare_packed_git_one(source, local); + prepare_multi_pack_index_one(source); + prepare_packed_git_one(source); } rearrange_packed_git(r); diff --git a/repository.c b/repository.c index ecd691181fc97d..97f0578381d894 100644 --- a/repository.c +++ b/repository.c @@ -168,6 +168,7 @@ void repo_set_gitdir(struct repository *repo, if (!repo->objects->sources) { CALLOC_ARRAY(repo->objects->sources, 1); repo->objects->sources->odb = repo->objects; + repo->objects->sources->local = true; repo->objects->sources_tail = &repo->objects->sources->next; } expand_base_dir(&repo->objects->sources->path, o->object_dir, From 0d61933b8f9a0392310196578e1374283496843c Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 11 Aug 2025 15:46:42 +0200 Subject: [PATCH 049/695] odb: allow `odb_find_source()` to fail When trying to locate a source for an unknown object directory we will die right away. In subsequent patches we will add new callsites though that want to handle this situation gracefully instead. Refactor the function to return a `NULL` pointer if the source could not be found and adapt the callsites to die instead. Introduce a new wrapper `odb_find_source_or_die()` that continues to die in case the source could not be found. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/commit-graph.c | 4 ++-- midx-write.c | 2 +- odb.c | 6 ++++++ odb.h | 7 +++++-- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/builtin/commit-graph.c b/builtin/commit-graph.c index 25018a0b9df464..33fb7a5145c694 100644 --- a/builtin/commit-graph.c +++ b/builtin/commit-graph.c @@ -101,7 +101,7 @@ static int graph_verify(int argc, const char **argv, const char *prefix, if (opts.progress) flags |= COMMIT_GRAPH_WRITE_PROGRESS; - source = odb_find_source(the_repository->objects, opts.obj_dir); + source = odb_find_source_or_die(the_repository->objects, opts.obj_dir); graph_name = get_commit_graph_filename(source); chain_name = get_commit_graph_chain_filename(source); if (open_commit_graph(graph_name, &fd, &st)) @@ -289,7 +289,7 @@ static int graph_write(int argc, const char **argv, const char *prefix, git_env_bool(GIT_TEST_COMMIT_GRAPH_CHANGED_PATHS, 0)) flags |= COMMIT_GRAPH_WRITE_BLOOM_FILTERS; - source = odb_find_source(the_repository->objects, opts.obj_dir); + source = odb_find_source_or_die(the_repository->objects, opts.obj_dir); if (opts.reachable) { if (write_commit_graph_reachable(source, flags, &write_opts)) diff --git a/midx-write.c b/midx-write.c index c1ae62d3549425..d38caceadb4fa4 100644 --- a/midx-write.c +++ b/midx-write.c @@ -916,7 +916,7 @@ static int write_midx_bitmap(struct write_midx_context *ctx, static struct multi_pack_index *lookup_multi_pack_index(struct repository *r, const char *object_dir) { - struct odb_source *source = odb_find_source(r->objects, object_dir); + struct odb_source *source = odb_find_source_or_die(r->objects, object_dir); return get_multi_pack_index(source); } diff --git a/odb.c b/odb.c index 1761a50840ddf8..4e7f14be4a004c 100644 --- a/odb.c +++ b/odb.c @@ -464,6 +464,12 @@ struct odb_source *odb_find_source(struct object_database *odb, const char *obj_ free(obj_dir_real); strbuf_release(&odb_path_real); + return source; +} + +struct odb_source *odb_find_source_or_die(struct object_database *odb, const char *obj_dir) +{ + struct odb_source *source = odb_find_source(odb, obj_dir); if (!source) die(_("could not find object directory matching %s"), obj_dir); return source; diff --git a/odb.h b/odb.h index f9300439bab3af..312921077b857a 100644 --- a/odb.h +++ b/odb.h @@ -186,11 +186,14 @@ struct object_database *odb_new(struct repository *repo); void odb_clear(struct object_database *o); /* - * Find source by its object directory path. Dies in case the source couldn't - * be found. + * Find source by its object directory path. Returns a `NULL` pointer in case + * the source could not be found. */ struct odb_source *odb_find_source(struct object_database *odb, const char *obj_dir); +/* Same as `odb_find_source()`, but dies in case the source doesn't exist. */ +struct odb_source *odb_find_source_or_die(struct object_database *odb, const char *obj_dir); + /* * Replace the current writable object directory with the specified temporary * object directory; returns the former primary source. From 25c532f6e0797ef501ce43835fb4af4bd9c33de5 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 11 Aug 2025 15:46:43 +0200 Subject: [PATCH 050/695] odb: consistently use "dir" to refer to alternate's directory The functions that add an alternate object directory to the object database are somewhat inconsistent in how they call the paramater that refers to the directory path: in our headers we refer to it as "dir", whereas in the implementation we often call it "reference" or "entry". Unify this and consistently call the parameter "dir". While at it, refactor `link_alt_odb_entry()` to accept a C string instead of a `struct strbuf` as parameter to clarify that we really only need the path and nothing else. Suggested-by: Taylor Blau Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb.c | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/odb.c b/odb.c index 4e7f14be4a004c..e41e3952ea0a28 100644 --- a/odb.c +++ b/odb.c @@ -140,7 +140,7 @@ static void read_info_alternates(struct object_database *odb, int depth); static int link_alt_odb_entry(struct object_database *odb, - const struct strbuf *entry, + const char *dir, const char *relative_base, int depth, const char *normalized_objdir) @@ -151,11 +151,11 @@ static int link_alt_odb_entry(struct object_database *odb, khiter_t pos; int ret = -1; - if (!is_absolute_path(entry->buf) && relative_base) { + if (!is_absolute_path(dir) && relative_base) { strbuf_realpath(&pathbuf, relative_base, 1); strbuf_addch(&pathbuf, '/'); } - strbuf_addbuf(&pathbuf, entry); + strbuf_addstr(&pathbuf, dir); if (!strbuf_realpath(&tmp, pathbuf.buf, 0)) { error(_("unable to normalize alternate object path: %s"), @@ -229,7 +229,7 @@ static void link_alt_odb_entries(struct object_database *odb, const char *alt, int sep, const char *relative_base, int depth) { struct strbuf objdirbuf = STRBUF_INIT; - struct strbuf entry = STRBUF_INIT; + struct strbuf dir = STRBUF_INIT; if (!alt || !*alt) return; @@ -243,13 +243,13 @@ static void link_alt_odb_entries(struct object_database *odb, const char *alt, strbuf_realpath(&objdirbuf, odb->sources->path, 1); while (*alt) { - alt = parse_alt_odb_entry(alt, sep, &entry); - if (!entry.len) + alt = parse_alt_odb_entry(alt, sep, &dir); + if (!dir.len) continue; - link_alt_odb_entry(odb, &entry, + link_alt_odb_entry(odb, dir.buf, relative_base, depth, objdirbuf.buf); } - strbuf_release(&entry); + strbuf_release(&dir); strbuf_release(&objdirbuf); } @@ -273,7 +273,7 @@ static void read_info_alternates(struct object_database *odb, } void odb_add_to_alternates_file(struct object_database *odb, - const char *reference) + const char *dir) { struct lock_file lock = LOCK_INIT; char *alts = repo_git_path(odb->repo, "objects/info/alternates"); @@ -290,7 +290,7 @@ void odb_add_to_alternates_file(struct object_database *odb, struct strbuf line = STRBUF_INIT; while (strbuf_getline(&line, in) != EOF) { - if (!strcmp(reference, line.buf)) { + if (!strcmp(dir, line.buf)) { found = 1; break; } @@ -306,18 +306,17 @@ void odb_add_to_alternates_file(struct object_database *odb, if (found) { rollback_lock_file(&lock); } else { - fprintf_or_die(out, "%s\n", reference); + fprintf_or_die(out, "%s\n", dir); if (commit_lock_file(&lock)) die_errno(_("unable to move new alternates file into place")); if (odb->loaded_alternates) - link_alt_odb_entries(odb, reference, - '\n', NULL, 0); + link_alt_odb_entries(odb, dir, '\n', NULL, 0); } free(alts); } void odb_add_to_alternates_memory(struct object_database *odb, - const char *reference) + const char *dir) { /* * Make sure alternates are initialized, or else our entry may be @@ -325,8 +324,7 @@ void odb_add_to_alternates_memory(struct object_database *odb, */ odb_prepare_alternates(odb); - link_alt_odb_entries(odb, reference, - '\n', NULL, 0); + link_alt_odb_entries(odb, dir, '\n', NULL, 0); } struct odb_source *odb_set_temporary_primary_source(struct object_database *odb, From a59d44ff3f0f308f9577b05c858c063d2466b061 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 11 Aug 2025 15:46:44 +0200 Subject: [PATCH 051/695] odb: return newly created in-memory sources Callers have no trivial way to obtain the newly created object database source when adding it to the in-memory list of alternates. While not yet needed anywhere, a subsequent commit will want to obtain that pointer. Refactor the function to return the source to make it easily accessible. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb.c | 30 ++++++++++++++++++------------ odb.h | 4 ++-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/odb.c b/odb.c index e41e3952ea0a28..0c808bb288f168 100644 --- a/odb.c +++ b/odb.c @@ -139,17 +139,16 @@ static void read_info_alternates(struct object_database *odb, const char *relative_base, int depth); -static int link_alt_odb_entry(struct object_database *odb, - const char *dir, - const char *relative_base, - int depth, - const char *normalized_objdir) +static struct odb_source *link_alt_odb_entry(struct object_database *odb, + const char *dir, + const char *relative_base, + int depth, + const char *normalized_objdir) { - struct odb_source *alternate; + struct odb_source *alternate = NULL; struct strbuf pathbuf = STRBUF_INIT; struct strbuf tmp = STRBUF_INIT; khiter_t pos; - int ret = -1; if (!is_absolute_path(dir) && relative_base) { strbuf_realpath(&pathbuf, relative_base, 1); @@ -189,11 +188,11 @@ static int link_alt_odb_entry(struct object_database *odb, /* recursively add alternates */ read_info_alternates(odb, alternate->path, depth + 1); - ret = 0; + error: strbuf_release(&tmp); strbuf_release(&pathbuf); - return ret; + return alternate; } static const char *parse_alt_odb_entry(const char *string, @@ -315,16 +314,23 @@ void odb_add_to_alternates_file(struct object_database *odb, free(alts); } -void odb_add_to_alternates_memory(struct object_database *odb, - const char *dir) +struct odb_source *odb_add_to_alternates_memory(struct object_database *odb, + const char *dir) { + struct odb_source *alternate; + char *objdir; + /* * Make sure alternates are initialized, or else our entry may be * overwritten when they are. */ odb_prepare_alternates(odb); - link_alt_odb_entries(odb, dir, '\n', NULL, 0); + objdir = real_pathdup(odb->sources->path, 1); + alternate = link_alt_odb_entry(odb, dir, NULL, 0, objdir); + + free(objdir); + return alternate; } struct odb_source *odb_set_temporary_primary_source(struct object_database *odb, diff --git a/odb.h b/odb.h index 312921077b857a..d7691326997947 100644 --- a/odb.h +++ b/odb.h @@ -268,8 +268,8 @@ void odb_add_to_alternates_file(struct object_database *odb, * recursive alternates it points to), but do not modify the on-disk alternates * file. */ -void odb_add_to_alternates_memory(struct object_database *odb, - const char *dir); +struct odb_source *odb_add_to_alternates_memory(struct object_database *odb, + const char *dir); /* * Read an object from the database. Returns the object data and assigns object From 57363dfa0dce05aac735d5cfd626e6aac8cb706c Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 11 Aug 2025 15:46:45 +0200 Subject: [PATCH 052/695] odb: simplify calling `link_alt_odb_entry()` Callers of `link_alt_odb_entry()` are expected to pass in three different paths: - The (potentially relative) path of the object directory that we're about to add. - The base that should be used to resolve a relative object directory path. - The resolved path to the object database's objects directory. Juggling those three paths makes the calling convention somewhat hard to grok at first. As it turns out, the third parameter is redundant: we always pass in the resolved path of the object database's primary source, and we already pass in the database itself. So instead, we can resolve that path in the function itself. One downside of this is that one caller of `link_alt_odb_entry()` calls this function in a loop, so we were able to resolve the directory a single time, only. But ultimately, we only ever end up with a rather limited number of alternates anyway, so the extra couple of cycles we save feels more like a micro optimization. Refactor the code accordingly. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb.c | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/odb.c b/odb.c index 0c808bb288f168..4f884e3b509a59 100644 --- a/odb.c +++ b/odb.c @@ -142,8 +142,7 @@ static void read_info_alternates(struct object_database *odb, static struct odb_source *link_alt_odb_entry(struct object_database *odb, const char *dir, const char *relative_base, - int depth, - const char *normalized_objdir) + int depth) { struct odb_source *alternate = NULL; struct strbuf pathbuf = STRBUF_INIT; @@ -170,7 +169,10 @@ static struct odb_source *link_alt_odb_entry(struct object_database *odb, while (pathbuf.len && pathbuf.buf[pathbuf.len - 1] == '/') strbuf_setlen(&pathbuf, pathbuf.len - 1); - if (!alt_odb_usable(odb, &pathbuf, normalized_objdir, &pos)) + strbuf_reset(&tmp); + strbuf_realpath(&tmp, odb->sources->path, 1); + + if (!alt_odb_usable(odb, &pathbuf, tmp.buf, &pos)) goto error; CALLOC_ARRAY(alternate, 1); @@ -227,7 +229,6 @@ static const char *parse_alt_odb_entry(const char *string, static void link_alt_odb_entries(struct object_database *odb, const char *alt, int sep, const char *relative_base, int depth) { - struct strbuf objdirbuf = STRBUF_INIT; struct strbuf dir = STRBUF_INIT; if (!alt || !*alt) @@ -239,17 +240,13 @@ static void link_alt_odb_entries(struct object_database *odb, const char *alt, return; } - strbuf_realpath(&objdirbuf, odb->sources->path, 1); - while (*alt) { alt = parse_alt_odb_entry(alt, sep, &dir); if (!dir.len) continue; - link_alt_odb_entry(odb, dir.buf, - relative_base, depth, objdirbuf.buf); + link_alt_odb_entry(odb, dir.buf, relative_base, depth); } strbuf_release(&dir); - strbuf_release(&objdirbuf); } static void read_info_alternates(struct object_database *odb, @@ -317,20 +314,12 @@ void odb_add_to_alternates_file(struct object_database *odb, struct odb_source *odb_add_to_alternates_memory(struct object_database *odb, const char *dir) { - struct odb_source *alternate; - char *objdir; - /* * Make sure alternates are initialized, or else our entry may be * overwritten when they are. */ odb_prepare_alternates(odb); - - objdir = real_pathdup(odb->sources->path, 1); - alternate = link_alt_odb_entry(odb, dir, NULL, 0, objdir); - - free(objdir); - return alternate; + return link_alt_odb_entry(odb, dir, NULL, 0); } struct odb_source *odb_set_temporary_primary_source(struct object_database *odb, From 9ff212961506679c1e2c1541b17ab2bd8563ff15 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 11 Aug 2025 15:46:46 +0200 Subject: [PATCH 053/695] midx: drop redundant `struct repository` parameter There are a couple of functions that take both a `struct repository` and a `struct multi_pack_index`. This provides redundant information though without much benefit given that the multi-pack index already has a pointer to its owning repository. Drop the `struct repository` parameter from such functions. While at it, reorder the list of parameters of `fill_midx_entry()` so that the MIDX comes first to better align with our coding guidelines. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/pack-objects.c | 2 +- midx-write.c | 16 +++++++--------- midx.c | 18 +++++++++--------- midx.h | 6 +++--- pack-bitmap.c | 4 ++-- packfile.c | 4 ++-- t/helper/test-read-midx.c | 4 ++-- 7 files changed, 26 insertions(+), 28 deletions(-) diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 3dd84495b869e0..b9fd685b8fcc8f 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -1733,7 +1733,7 @@ static int want_object_in_pack_mtime(const struct object_id *oid, struct multi_pack_index *m = get_multi_pack_index(source); struct pack_entry e; - if (m && fill_midx_entry(the_repository, oid, &e, m)) { + if (m && fill_midx_entry(m, oid, &e)) { want = want_object_in_pack_one(e.p, oid, exclude, found_pack, found_offset, found_mtime); if (want != -1) return want; diff --git a/midx-write.c b/midx-write.c index d38caceadb4fa4..b858be475fc3fa 100644 --- a/midx-write.c +++ b/midx-write.c @@ -942,8 +942,7 @@ static int fill_packs_from_midx(struct write_midx_context *ctx, */ if (flags & MIDX_WRITE_REV_INDEX || preferred_pack_name) { - if (prepare_midx_pack(ctx->repo, m, - m->num_packs_in_base + i)) { + if (prepare_midx_pack(m, m->num_packs_in_base + i)) { error(_("could not load pack")); return 1; } @@ -1566,7 +1565,7 @@ int expire_midx_packs(struct repository *r, const char *object_dir, unsigned fla if (count[i]) continue; - if (prepare_midx_pack(r, m, i)) + if (prepare_midx_pack(m, i)) continue; if (m->packs[i]->pack_keep || m->packs[i]->is_cruft) @@ -1612,13 +1611,12 @@ static int compare_by_mtime(const void *a_, const void *b_) return 0; } -static int want_included_pack(struct repository *r, - struct multi_pack_index *m, +static int want_included_pack(struct multi_pack_index *m, int pack_kept_objects, uint32_t pack_int_id) { struct packed_git *p; - if (prepare_midx_pack(r, m, pack_int_id)) + if (prepare_midx_pack(m, pack_int_id)) return 0; p = m->packs[pack_int_id]; if (!pack_kept_objects && p->pack_keep) @@ -1640,7 +1638,7 @@ static void fill_included_packs_all(struct repository *r, repo_config_get_bool(r, "repack.packkeptobjects", &pack_kept_objects); for (i = 0; i < m->num_packs; i++) { - if (!want_included_pack(r, m, pack_kept_objects, i)) + if (!want_included_pack(m, pack_kept_objects, i)) continue; include_pack[i] = 1; @@ -1664,7 +1662,7 @@ static void fill_included_packs_batch(struct repository *r, for (i = 0; i < m->num_packs; i++) { pack_info[i].pack_int_id = i; - if (prepare_midx_pack(r, m, i)) + if (prepare_midx_pack(m, i)) continue; pack_info[i].mtime = m->packs[i]->mtime; @@ -1683,7 +1681,7 @@ static void fill_included_packs_batch(struct repository *r, struct packed_git *p = m->packs[pack_int_id]; uint64_t expected_size; - if (!want_included_pack(r, m, pack_kept_objects, pack_int_id)) + if (!want_included_pack(m, pack_kept_objects, pack_int_id)) continue; /* diff --git a/midx.c b/midx.c index b9ca0915a67f10..8459dda8c9e810 100644 --- a/midx.c +++ b/midx.c @@ -450,9 +450,10 @@ static uint32_t midx_for_pack(struct multi_pack_index **_m, return pack_int_id - m->num_packs_in_base; } -int prepare_midx_pack(struct repository *r, struct multi_pack_index *m, +int prepare_midx_pack(struct multi_pack_index *m, uint32_t pack_int_id) { + struct repository *r = m->repo; struct strbuf pack_name = STRBUF_INIT; struct strbuf key = STRBUF_INIT; struct packed_git *p; @@ -507,7 +508,7 @@ struct packed_git *nth_midxed_pack(struct multi_pack_index *m, #define MIDX_CHUNK_BITMAPPED_PACKS_WIDTH (2 * sizeof(uint32_t)) -int nth_bitmapped_pack(struct repository *r, struct multi_pack_index *m, +int nth_bitmapped_pack(struct multi_pack_index *m, struct bitmapped_pack *bp, uint32_t pack_int_id) { uint32_t local_pack_int_id = midx_for_pack(&m, pack_int_id); @@ -515,7 +516,7 @@ int nth_bitmapped_pack(struct repository *r, struct multi_pack_index *m, if (!m->chunk_bitmapped_packs) return error(_("MIDX does not contain the BTMP chunk")); - if (prepare_midx_pack(r, m, pack_int_id)) + if (prepare_midx_pack(m, pack_int_id)) return error(_("could not load bitmapped pack %"PRIu32), pack_int_id); bp->p = m->packs[local_pack_int_id]; @@ -600,10 +601,9 @@ uint32_t nth_midxed_pack_int_id(struct multi_pack_index *m, uint32_t pos) (off_t)pos * MIDX_CHUNK_OFFSET_WIDTH); } -int fill_midx_entry(struct repository *r, +int fill_midx_entry(struct multi_pack_index *m, const struct object_id *oid, - struct pack_entry *e, - struct multi_pack_index *m) + struct pack_entry *e) { uint32_t pos; uint32_t pack_int_id; @@ -615,7 +615,7 @@ int fill_midx_entry(struct repository *r, midx_for_object(&m, pos); pack_int_id = nth_midxed_pack_int_id(m, pos); - if (prepare_midx_pack(r, m, pack_int_id)) + if (prepare_midx_pack(m, pack_int_id)) return 0; p = m->packs[pack_int_id - m->num_packs_in_base]; @@ -912,7 +912,7 @@ int verify_midx_file(struct repository *r, const char *object_dir, unsigned flag _("Looking for referenced packfiles"), m->num_packs + m->num_packs_in_base); for (i = 0; i < m->num_packs + m->num_packs_in_base; i++) { - if (prepare_midx_pack(r, m, i)) + if (prepare_midx_pack(m, i)) midx_report("failed to load pack in position %d", i); display_progress(progress, i + 1); @@ -989,7 +989,7 @@ int verify_midx_file(struct repository *r, const char *object_dir, unsigned flag nth_midxed_object_oid(&oid, m, pairs[i].pos); - if (!fill_midx_entry(r, &oid, &e, m)) { + if (!fill_midx_entry(m, &oid, &e)) { midx_report(_("failed to load pack entry for oid[%d] = %s"), pairs[i].pos, oid_to_hex(&oid)); continue; diff --git a/midx.h b/midx.h index 28c426a8232997..f7e07083e1f9de 100644 --- a/midx.h +++ b/midx.h @@ -103,10 +103,10 @@ void get_split_midx_filename_ext(const struct git_hash_algo *hash_algo, struct multi_pack_index *load_multi_pack_index(struct repository *r, const char *object_dir, int local); -int prepare_midx_pack(struct repository *r, struct multi_pack_index *m, uint32_t pack_int_id); +int prepare_midx_pack(struct multi_pack_index *m, uint32_t pack_int_id); struct packed_git *nth_midxed_pack(struct multi_pack_index *m, uint32_t pack_int_id); -int nth_bitmapped_pack(struct repository *r, struct multi_pack_index *m, +int nth_bitmapped_pack(struct multi_pack_index *m, struct bitmapped_pack *bp, uint32_t pack_int_id); int bsearch_one_midx(const struct object_id *oid, struct multi_pack_index *m, uint32_t *result); @@ -118,7 +118,7 @@ uint32_t nth_midxed_pack_int_id(struct multi_pack_index *m, uint32_t pos); struct object_id *nth_midxed_object_oid(struct object_id *oid, struct multi_pack_index *m, uint32_t n); -int fill_midx_entry(struct repository *r, const struct object_id *oid, struct pack_entry *e, struct multi_pack_index *m); +int fill_midx_entry(struct multi_pack_index *m, const struct object_id *oid, struct pack_entry *e); int midx_contains_pack(struct multi_pack_index *m, const char *idx_or_pack_name); int midx_preferred_pack(struct multi_pack_index *m, uint32_t *pack_int_id); diff --git a/pack-bitmap.c b/pack-bitmap.c index d14421ee204414..fb0b11ca073856 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -493,7 +493,7 @@ static int open_midx_bitmap_1(struct bitmap_index *bitmap_git, } for (i = 0; i < bitmap_git->midx->num_packs + bitmap_git->midx->num_packs_in_base; i++) { - if (prepare_midx_pack(bitmap_repo(bitmap_git), bitmap_git->midx, i)) { + if (prepare_midx_pack(bitmap_git->midx, i)) { warning(_("could not open pack %s"), bitmap_git->midx->pack_names[i]); goto cleanup; @@ -2466,7 +2466,7 @@ void reuse_partial_packfile_from_bitmap(struct bitmap_index *bitmap_git, struct multi_pack_index *m = bitmap_git->midx; for (i = 0; i < m->num_packs + m->num_packs_in_base; i++) { struct bitmapped_pack pack; - if (nth_bitmapped_pack(r, bitmap_git->midx, &pack, i) < 0) { + if (nth_bitmapped_pack(bitmap_git->midx, &pack, i) < 0) { warning(_("unable to load pack: '%s', disabling pack-reuse"), bitmap_git->midx->pack_names[i]); free(packs); diff --git a/packfile.c b/packfile.c index a38544b87bf2f1..acb680966dacf9 100644 --- a/packfile.c +++ b/packfile.c @@ -1091,7 +1091,7 @@ struct packed_git *get_all_packs(struct repository *r) if (!m) continue; for (uint32_t i = 0; i < m->num_packs + m->num_packs_in_base; i++) - prepare_midx_pack(r, m, i); + prepare_midx_pack(m, i); } return r->objects->packed_git; @@ -2077,7 +2077,7 @@ int find_pack_entry(struct repository *r, const struct object_id *oid, struct pa prepare_packed_git(r); for (struct odb_source *source = r->objects->sources; source; source = source->next) - if (source->midx && fill_midx_entry(r, oid, e, source->midx)) + if (source->midx && fill_midx_entry(source->midx, oid, e)) return 1; if (!r->objects->packed_git) diff --git a/t/helper/test-read-midx.c b/t/helper/test-read-midx.c index da2aa036b57ef6..e430aa247c6f34 100644 --- a/t/helper/test-read-midx.c +++ b/t/helper/test-read-midx.c @@ -65,7 +65,7 @@ static int read_midx_file(const char *object_dir, const char *checksum, for (i = 0; i < m->num_objects; i++) { nth_midxed_object_oid(&oid, m, i + m->num_objects_in_base); - fill_midx_entry(the_repository, &oid, &e, m); + fill_midx_entry(m, &oid, &e); printf("%s %"PRIu64"\t%s\n", oid_to_hex(&oid), e.offset, e.p->pack_name); @@ -126,7 +126,7 @@ static int read_midx_bitmapped_packs(const char *object_dir) return 1; for (i = 0; i < midx->num_packs + midx->num_packs_in_base; i++) { - if (nth_bitmapped_pack(the_repository, midx, &pack, i) < 0) { + if (nth_bitmapped_pack(midx, &pack, i) < 0) { close_midx(midx); return 1; } From 017db7bb14246dea55b678fc20e34ce91c28968a Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 11 Aug 2025 15:46:47 +0200 Subject: [PATCH 054/695] midx: load multi-pack indices via their source To load a multi-pack index the caller is expected to pass both the repository and the object directory where the multi-pack index is located. While this works, this layout has a couple of downsides: - We need to pass in information reduntant with the owning source, namely its object directory and whether the source is local or not. - We don't have access to the source when loading the multi-pack index. If we had that access, we could store a pointer to the owning source in the MIDX and thus deduplicate some information. - Multi-pack indices are inherently specific to the object source and its format. With the goal of pluggable object backends in mind we will eventually want the backends to own the logic of reading and writing multi-pack indices. Making the logic work on top of object sources is a step into that direction. Refactor loading of multi-pack indices accordingly. This surfaces one small problem though: git-multi-pack-index(1) and our MIDX test helper both know to read and write multi-pack-indices located in a different object directory. This issue is addressed by adding the user-provided object directory as an in-memory alternate. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/multi-pack-index.c | 18 ++++++++++-- midx.c | 57 ++++++++++++++++--------------------- midx.h | 6 ++-- t/helper/test-read-midx.c | 25 +++++++++------- t/t5319-multi-pack-index.sh | 8 +++--- 5 files changed, 62 insertions(+), 52 deletions(-) diff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c index aa25b06f9d0f89..e4a9305af3ab75 100644 --- a/builtin/multi-pack-index.c +++ b/builtin/multi-pack-index.c @@ -64,12 +64,20 @@ static int parse_object_dir(const struct option *opt, const char *arg, char **value = opt->value; free(*value); if (unset) - *value = xstrdup(repo_get_object_directory(the_repository)); + *value = xstrdup(the_repository->objects->sources->path); else *value = real_pathdup(arg, 1); return 0; } +static struct odb_source *handle_object_dir_option(struct repository *repo) +{ + struct odb_source *source = odb_find_source(repo->objects, opts.object_dir); + if (!source) + source = odb_add_to_alternates_memory(repo->objects, opts.object_dir); + return source; +} + static struct option common_opts[] = { OPT_CALLBACK(0, "object-dir", &opts.object_dir, N_("directory"), @@ -157,6 +165,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv, if (argc) usage_with_options(builtin_multi_pack_index_write_usage, options); + handle_object_dir_option(repo); FREE_AND_NULL(options); @@ -193,6 +202,8 @@ static int cmd_multi_pack_index_verify(int argc, const char **argv, N_("force progress reporting"), MIDX_PROGRESS), OPT_END(), }; + struct odb_source *source; + options = add_common_options(builtin_multi_pack_index_verify_options); trace2_cmd_mode(argv[0]); @@ -205,10 +216,11 @@ static int cmd_multi_pack_index_verify(int argc, const char **argv, if (argc) usage_with_options(builtin_multi_pack_index_verify_usage, options); + source = handle_object_dir_option(the_repository); FREE_AND_NULL(options); - return verify_midx_file(the_repository, opts.object_dir, opts.flags); + return verify_midx_file(source, opts.flags); } static int cmd_multi_pack_index_expire(int argc, const char **argv, @@ -233,6 +245,7 @@ static int cmd_multi_pack_index_expire(int argc, const char **argv, if (argc) usage_with_options(builtin_multi_pack_index_expire_usage, options); + handle_object_dir_option(the_repository); FREE_AND_NULL(options); @@ -265,6 +278,7 @@ static int cmd_multi_pack_index_repack(int argc, const char **argv, if (argc) usage_with_options(builtin_multi_pack_index_repack_usage, options); + handle_object_dir_option(the_repository); FREE_AND_NULL(options); diff --git a/midx.c b/midx.c index 8459dda8c9e810..831a7e9b5f2c08 100644 --- a/midx.c +++ b/midx.c @@ -95,11 +95,10 @@ static int midx_read_object_offsets(const unsigned char *chunk_start, return 0; } -static struct multi_pack_index *load_multi_pack_index_one(struct repository *r, - const char *object_dir, - const char *midx_name, - int local) +static struct multi_pack_index *load_multi_pack_index_one(struct odb_source *source, + const char *midx_name) { + struct repository *r = source->odb->repo; struct multi_pack_index *m = NULL; int fd; struct stat st; @@ -129,10 +128,10 @@ static struct multi_pack_index *load_multi_pack_index_one(struct repository *r, midx_map = xmmap(NULL, midx_size, PROT_READ, MAP_PRIVATE, fd, 0); close(fd); - FLEX_ALLOC_STR(m, object_dir, object_dir); + FLEX_ALLOC_STR(m, object_dir, source->path); m->data = midx_map; m->data_len = midx_size; - m->local = local; + m->local = source->local; m->repo = r; m->signature = get_be32(m->data); @@ -297,19 +296,18 @@ static int add_midx_to_chain(struct multi_pack_index *midx, return 1; } -static struct multi_pack_index *load_midx_chain_fd_st(struct repository *r, - const char *object_dir, - int local, +static struct multi_pack_index *load_midx_chain_fd_st(struct odb_source *source, int fd, struct stat *st, int *incomplete_chain) { + const struct git_hash_algo *hash_algo = source->odb->repo->hash_algo; struct multi_pack_index *midx_chain = NULL; struct strbuf buf = STRBUF_INIT; int valid = 1; uint32_t i, count; FILE *fp = xfdopen(fd, "r"); - count = st->st_size / (r->hash_algo->hexsz + 1); + count = st->st_size / (hash_algo->hexsz + 1); for (i = 0; i < count; i++) { struct multi_pack_index *m; @@ -318,7 +316,7 @@ static struct multi_pack_index *load_midx_chain_fd_st(struct repository *r, if (strbuf_getline_lf(&buf, fp) == EOF) break; - if (get_oid_hex_algop(buf.buf, &layer, r->hash_algo)) { + if (get_oid_hex_algop(buf.buf, &layer, hash_algo)) { warning(_("invalid multi-pack-index chain: line '%s' " "not a hash"), buf.buf); @@ -329,9 +327,9 @@ static struct multi_pack_index *load_midx_chain_fd_st(struct repository *r, valid = 0; strbuf_reset(&buf); - get_split_midx_filename_ext(r->hash_algo, &buf, object_dir, + get_split_midx_filename_ext(hash_algo, &buf, source->path, layer.hash, MIDX_EXT_MIDX); - m = load_multi_pack_index_one(r, object_dir, buf.buf, local); + m = load_multi_pack_index_one(source, buf.buf); if (m) { if (add_midx_to_chain(m, midx_chain)) { @@ -354,40 +352,35 @@ static struct multi_pack_index *load_midx_chain_fd_st(struct repository *r, return midx_chain; } -static struct multi_pack_index *load_multi_pack_index_chain(struct repository *r, - const char *object_dir, - int local) +static struct multi_pack_index *load_multi_pack_index_chain(struct odb_source *source) { struct strbuf chain_file = STRBUF_INIT; struct stat st; int fd; struct multi_pack_index *m = NULL; - get_midx_chain_filename(&chain_file, object_dir); - if (open_multi_pack_index_chain(r->hash_algo, chain_file.buf, &fd, &st)) { + get_midx_chain_filename(&chain_file, source->path); + if (open_multi_pack_index_chain(source->odb->repo->hash_algo, chain_file.buf, &fd, &st)) { int incomplete; /* ownership of fd is taken over by load function */ - m = load_midx_chain_fd_st(r, object_dir, local, fd, &st, - &incomplete); + m = load_midx_chain_fd_st(source, fd, &st, &incomplete); } strbuf_release(&chain_file); return m; } -struct multi_pack_index *load_multi_pack_index(struct repository *r, - const char *object_dir, - int local) +struct multi_pack_index *load_multi_pack_index(struct odb_source *source) { struct strbuf midx_name = STRBUF_INIT; struct multi_pack_index *m; - get_midx_filename(r->hash_algo, &midx_name, object_dir); + get_midx_filename(source->odb->repo->hash_algo, &midx_name, + source->path); - m = load_multi_pack_index_one(r, object_dir, - midx_name.buf, local); + m = load_multi_pack_index_one(source, midx_name.buf); if (!m) - m = load_multi_pack_index_chain(r, object_dir, local); + m = load_multi_pack_index_chain(source); strbuf_release(&midx_name); @@ -734,8 +727,7 @@ int prepare_multi_pack_index_one(struct odb_source *source) if (source->midx) return 1; - source->midx = load_multi_pack_index(r, source->path, - source->local); + source->midx = load_multi_pack_index(source); return !!source->midx; } @@ -880,12 +872,13 @@ static int compare_pair_pos_vs_id(const void *_a, const void *_b) display_progress(progress, _n); \ } while (0) -int verify_midx_file(struct repository *r, const char *object_dir, unsigned flags) +int verify_midx_file(struct odb_source *source, unsigned flags) { + struct repository *r = source->odb->repo; struct pair_pos_vs_id *pairs = NULL; uint32_t i; struct progress *progress = NULL; - struct multi_pack_index *m = load_multi_pack_index(r, object_dir, 1); + struct multi_pack_index *m = load_multi_pack_index(source); struct multi_pack_index *curr; verify_midx_error = 0; @@ -894,7 +887,7 @@ int verify_midx_file(struct repository *r, const char *object_dir, unsigned flag struct stat sb; struct strbuf filename = STRBUF_INIT; - get_midx_filename(r->hash_algo, &filename, object_dir); + get_midx_filename(r->hash_algo, &filename, source->path); if (!stat(filename.buf, &sb)) { error(_("multi-pack-index file exists, but failed to parse")); diff --git a/midx.h b/midx.h index f7e07083e1f9de..970d043989a5b0 100644 --- a/midx.h +++ b/midx.h @@ -100,9 +100,7 @@ void get_split_midx_filename_ext(const struct git_hash_algo *hash_algo, struct strbuf *buf, const char *object_dir, const unsigned char *hash, const char *ext); -struct multi_pack_index *load_multi_pack_index(struct repository *r, - const char *object_dir, - int local); +struct multi_pack_index *load_multi_pack_index(struct odb_source *source); int prepare_midx_pack(struct multi_pack_index *m, uint32_t pack_int_id); struct packed_git *nth_midxed_pack(struct multi_pack_index *m, uint32_t pack_int_id); @@ -136,7 +134,7 @@ int write_midx_file_only(struct repository *r, const char *object_dir, const char *preferred_pack_name, const char *refs_snapshot, unsigned flags); void clear_midx_file(struct repository *r); -int verify_midx_file(struct repository *r, const char *object_dir, unsigned flags); +int verify_midx_file(struct odb_source *source, unsigned flags); int expire_midx_packs(struct repository *r, const char *object_dir, unsigned flags); int midx_repack(struct repository *r, const char *object_dir, size_t batch_size, unsigned flags); diff --git a/t/helper/test-read-midx.c b/t/helper/test-read-midx.c index e430aa247c6f34..bcb8ea767179a6 100644 --- a/t/helper/test-read-midx.c +++ b/t/helper/test-read-midx.c @@ -11,14 +11,24 @@ #include "gettext.h" #include "pack-revindex.h" +static struct multi_pack_index *setup_midx(const char *object_dir) +{ + struct odb_source *source; + setup_git_directory(); + source = odb_find_source(the_repository->objects, object_dir); + if (!source) + source = odb_add_to_alternates_memory(the_repository->objects, + object_dir); + return load_multi_pack_index(source); +} + static int read_midx_file(const char *object_dir, const char *checksum, int show_objects) { uint32_t i; struct multi_pack_index *m; - setup_git_directory(); - m = load_multi_pack_index(the_repository, object_dir, 1); + m = setup_midx(object_dir); if (!m) return 1; @@ -81,8 +91,7 @@ static int read_midx_checksum(const char *object_dir) { struct multi_pack_index *m; - setup_git_directory(); - m = load_multi_pack_index(the_repository, object_dir, 1); + m = setup_midx(object_dir); if (!m) return 1; printf("%s\n", hash_to_hex(get_midx_checksum(m))); @@ -96,9 +105,7 @@ static int read_midx_preferred_pack(const char *object_dir) struct multi_pack_index *midx = NULL; uint32_t preferred_pack; - setup_git_directory(); - - midx = load_multi_pack_index(the_repository, object_dir, 1); + midx = setup_midx(object_dir); if (!midx) return 1; @@ -119,9 +126,7 @@ static int read_midx_bitmapped_packs(const char *object_dir) struct bitmapped_pack pack; uint32_t i; - setup_git_directory(); - - midx = load_multi_pack_index(the_repository, object_dir, 1); + midx = setup_midx(object_dir); if (!midx) return 1; diff --git a/t/t5319-multi-pack-index.sh b/t/t5319-multi-pack-index.sh index bd75dea9501ed7..4e5e882989fc45 100755 --- a/t/t5319-multi-pack-index.sh +++ b/t/t5319-multi-pack-index.sh @@ -28,11 +28,11 @@ midx_read_expect () { EOF if test $NUM_PACKS -ge 1 then - ls $OBJECT_DIR/pack/ | grep idx | sort + ls "$OBJECT_DIR"/pack/ | grep idx | sort fi && printf "object-dir: $OBJECT_DIR\n" } >expect && - test-tool read-midx $OBJECT_DIR >actual && + test-tool read-midx "$OBJECT_DIR" >actual && test_cmp expect actual } @@ -305,7 +305,7 @@ test_expect_success 'midx picks objects from preferred pack' ' ofs=$(git show-index expect && grep ^$b out >actual && @@ -639,7 +639,7 @@ test_expect_success 'force some 64-bit offsets with pack-objects' ' ( cd ../objects64 && pwd ) >.git/objects/info/alternates && midx64=$(git multi-pack-index --object-dir=../objects64 write) ) && - midx_read_expect 1 63 5 objects64 " large-offsets" + midx_read_expect 1 63 5 "$(pwd)/objects64" " large-offsets" ' test_expect_success 'verify multi-pack-index with 64-bit offsets' ' From c3f5d251469525a52074b0373671a588f0e5b972 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 11 Aug 2025 15:46:48 +0200 Subject: [PATCH 055/695] midx: write multi-pack indices via their source Similar to the preceding commit, refactor the writing side of multi-pack indices so that we pass in the object database source where the index should be written to. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/multi-pack-index.c | 19 ++++++----- builtin/repack.c | 2 +- midx-write.c | 67 ++++++++++++++++++-------------------- midx.h | 8 ++--- 4 files changed, 47 insertions(+), 49 deletions(-) diff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c index e4a9305af3ab75..b1e971e535d20e 100644 --- a/builtin/multi-pack-index.c +++ b/builtin/multi-pack-index.c @@ -147,6 +147,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv, N_("refs snapshot for selecting bitmap commits")), OPT_END(), }; + struct odb_source *source; int ret; opts.flags |= MIDX_WRITE_BITMAP_HASH_CACHE; @@ -165,7 +166,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv, if (argc) usage_with_options(builtin_multi_pack_index_write_usage, options); - handle_object_dir_option(repo); + source = handle_object_dir_option(repo); FREE_AND_NULL(options); @@ -174,7 +175,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv, read_packs_from_stdin(&packs); - ret = write_midx_file_only(repo, opts.object_dir, &packs, + ret = write_midx_file_only(source, &packs, opts.preferred_pack, opts.refs_snapshot, opts.flags); @@ -185,7 +186,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv, } - ret = write_midx_file(repo, opts.object_dir, opts.preferred_pack, + ret = write_midx_file(source, opts.preferred_pack, opts.refs_snapshot, opts.flags); free(opts.refs_snapshot); @@ -233,6 +234,8 @@ static int cmd_multi_pack_index_expire(int argc, const char **argv, N_("force progress reporting"), MIDX_PROGRESS), OPT_END(), }; + struct odb_source *source; + options = add_common_options(builtin_multi_pack_index_expire_options); trace2_cmd_mode(argv[0]); @@ -245,11 +248,11 @@ static int cmd_multi_pack_index_expire(int argc, const char **argv, if (argc) usage_with_options(builtin_multi_pack_index_expire_usage, options); - handle_object_dir_option(the_repository); + source = handle_object_dir_option(the_repository); FREE_AND_NULL(options); - return expire_midx_packs(the_repository, opts.object_dir, opts.flags); + return expire_midx_packs(source, opts.flags); } static int cmd_multi_pack_index_repack(int argc, const char **argv, @@ -264,6 +267,7 @@ static int cmd_multi_pack_index_repack(int argc, const char **argv, N_("force progress reporting"), MIDX_PROGRESS), OPT_END(), }; + struct odb_source *source; options = add_common_options(builtin_multi_pack_index_repack_options); @@ -278,12 +282,11 @@ static int cmd_multi_pack_index_repack(int argc, const char **argv, if (argc) usage_with_options(builtin_multi_pack_index_repack_usage, options); - handle_object_dir_option(the_repository); + source = handle_object_dir_option(the_repository); FREE_AND_NULL(options); - return midx_repack(the_repository, opts.object_dir, - (size_t)opts.batch_size, opts.flags); + return midx_repack(source, (size_t)opts.batch_size, opts.flags); } int cmd_multi_pack_index(int argc, diff --git a/builtin/repack.c b/builtin/repack.c index 21723866b9cdeb..94dec26f185be8 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -1711,7 +1711,7 @@ int cmd_repack(int argc, unsigned flags = 0; if (git_env_bool(GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL, 0)) flags |= MIDX_WRITE_INCREMENTAL; - write_midx_file(the_repository, repo_get_object_directory(the_repository), + write_midx_file(the_repository->objects->sources, NULL, NULL, flags); } diff --git a/midx-write.c b/midx-write.c index b858be475fc3fa..bf7c01d4b1d67c 100644 --- a/midx-write.c +++ b/midx-write.c @@ -913,13 +913,6 @@ static int write_midx_bitmap(struct write_midx_context *ctx, return ret; } -static struct multi_pack_index *lookup_multi_pack_index(struct repository *r, - const char *object_dir) -{ - struct odb_source *source = odb_find_source_or_die(r->objects, object_dir); - return get_multi_pack_index(source); -} - static int fill_packs_from_midx(struct write_midx_context *ctx, const char *preferred_pack_name, uint32_t flags) { @@ -1010,7 +1003,7 @@ static int link_midx_to_chain(struct multi_pack_index *m) return ret; } -static void clear_midx_files(struct repository *r, const char *object_dir, +static void clear_midx_files(struct odb_source *source, const char **hashes, uint32_t hashes_nr, unsigned incremental) { @@ -1029,16 +1022,16 @@ static void clear_midx_files(struct repository *r, const char *object_dir, uint32_t i, j; for (i = 0; i < ARRAY_SIZE(exts); i++) { - clear_incremental_midx_files_ext(object_dir, exts[i], + clear_incremental_midx_files_ext(source->path, exts[i], hashes, hashes_nr); for (j = 0; j < hashes_nr; j++) - clear_midx_files_ext(object_dir, exts[i], hashes[j]); + clear_midx_files_ext(source->path, exts[i], hashes[j]); } if (incremental) - get_midx_filename(r->hash_algo, &buf, object_dir); + get_midx_filename(source->odb->repo->hash_algo, &buf, source->path); else - get_midx_chain_filename(&buf, object_dir); + get_midx_chain_filename(&buf, source->path); if (unlink(buf.buf) && errno != ENOENT) die_errno(_("failed to clear multi-pack-index at %s"), buf.buf); @@ -1046,13 +1039,14 @@ static void clear_midx_files(struct repository *r, const char *object_dir, strbuf_release(&buf); } -static int write_midx_internal(struct repository *r, const char *object_dir, +static int write_midx_internal(struct odb_source *source, struct string_list *packs_to_include, struct string_list *packs_to_drop, const char *preferred_pack_name, const char *refs_snapshot, unsigned flags) { + struct repository *r = source->odb->repo; struct strbuf midx_name = STRBUF_INIT; unsigned char midx_hash[GIT_MAX_RAWSZ]; uint32_t i, start_pack; @@ -1076,15 +1070,15 @@ static int write_midx_internal(struct repository *r, const char *object_dir, if (ctx.incremental) strbuf_addf(&midx_name, "%s/pack/multi-pack-index.d/tmp_midx_XXXXXX", - object_dir); + source->path); else - get_midx_filename(r->hash_algo, &midx_name, object_dir); + get_midx_filename(r->hash_algo, &midx_name, source->path); if (safe_create_leading_directories(r, midx_name.buf)) die_errno(_("unable to create leading directories of %s"), midx_name.buf); if (!packs_to_include || ctx.incremental) { - struct multi_pack_index *m = lookup_multi_pack_index(r, object_dir); + struct multi_pack_index *m = get_multi_pack_index(source); if (m && !midx_checksum_valid(m)) { warning(_("ignoring existing multi-pack-index; checksum mismatch")); m = NULL; @@ -1138,7 +1132,7 @@ static int write_midx_internal(struct repository *r, const char *object_dir, ctx.to_include = packs_to_include; - for_each_file_in_pack_dir(object_dir, add_pack_to_midx, &ctx); + for_each_file_in_pack_dir(source->path, add_pack_to_midx, &ctx); stop_progress(&ctx.progress); if ((ctx.m && ctx.nr == ctx.m->num_packs + ctx.m->num_packs_in_base) && @@ -1158,7 +1152,7 @@ static int write_midx_internal(struct repository *r, const char *object_dir, * corresponding bitmap (or one wasn't requested). */ if (!want_bitmap) - clear_midx_files_ext(object_dir, "bitmap", NULL); + clear_midx_files_ext(source->path, "bitmap", NULL); goto cleanup; } } @@ -1326,7 +1320,7 @@ static int write_midx_internal(struct repository *r, const char *object_dir, if (ctx.incremental) { struct strbuf lock_name = STRBUF_INIT; - get_midx_chain_filename(&lock_name, object_dir); + get_midx_chain_filename(&lock_name, source->path); hold_lock_file_for_update(&lk, lock_name.buf, LOCK_DIE_ON_ERROR); strbuf_release(&lock_name); @@ -1389,7 +1383,7 @@ static int write_midx_internal(struct repository *r, const char *object_dir, if (flags & MIDX_WRITE_REV_INDEX && git_env_bool("GIT_TEST_MIDX_WRITE_REV", 0)) - write_midx_reverse_index(&ctx, object_dir, midx_hash); + write_midx_reverse_index(&ctx, source->path, midx_hash); if (flags & MIDX_WRITE_BITMAP) { struct packing_data pdata; @@ -1412,7 +1406,7 @@ static int write_midx_internal(struct repository *r, const char *object_dir, FREE_AND_NULL(ctx.entries); ctx.entries_nr = 0; - if (write_midx_bitmap(&ctx, object_dir, + if (write_midx_bitmap(&ctx, source->path, midx_hash, &pdata, commits, commits_nr, flags) < 0) { error(_("could not write multi-pack bitmap")); @@ -1446,7 +1440,7 @@ static int write_midx_internal(struct repository *r, const char *object_dir, return -1; get_split_midx_filename_ext(r->hash_algo, &final_midx_name, - object_dir, midx_hash, MIDX_EXT_MIDX); + source->path, midx_hash, MIDX_EXT_MIDX); if (rename_tempfile(&incr, final_midx_name.buf) < 0) { error_errno(_("unable to rename new multi-pack-index layer")); @@ -1479,7 +1473,7 @@ static int write_midx_internal(struct repository *r, const char *object_dir, if (commit_lock_file(&lk) < 0) die_errno(_("could not write multi-pack-index")); - clear_midx_files(r, object_dir, keep_hashes, + clear_midx_files(source, keep_hashes, ctx.num_multi_pack_indexes_before + 1, ctx.incremental); @@ -1508,29 +1502,29 @@ static int write_midx_internal(struct repository *r, const char *object_dir, return result; } -int write_midx_file(struct repository *r, const char *object_dir, +int write_midx_file(struct odb_source *source, const char *preferred_pack_name, const char *refs_snapshot, unsigned flags) { - return write_midx_internal(r, object_dir, NULL, NULL, + return write_midx_internal(source, NULL, NULL, preferred_pack_name, refs_snapshot, flags); } -int write_midx_file_only(struct repository *r, const char *object_dir, +int write_midx_file_only(struct odb_source *source, struct string_list *packs_to_include, const char *preferred_pack_name, const char *refs_snapshot, unsigned flags) { - return write_midx_internal(r, object_dir, packs_to_include, NULL, + return write_midx_internal(source, packs_to_include, NULL, preferred_pack_name, refs_snapshot, flags); } -int expire_midx_packs(struct repository *r, const char *object_dir, unsigned flags) +int expire_midx_packs(struct odb_source *source, unsigned flags) { uint32_t i, *count, result = 0; struct string_list packs_to_drop = STRING_LIST_INIT_DUP; - struct multi_pack_index *m = lookup_multi_pack_index(r, object_dir); + struct multi_pack_index *m = get_multi_pack_index(source); struct progress *progress = NULL; if (!m) @@ -1543,7 +1537,7 @@ int expire_midx_packs(struct repository *r, const char *object_dir, unsigned fla if (flags & MIDX_PROGRESS) progress = start_delayed_progress( - r, + source->odb->repo, _("Counting referenced objects"), m->num_objects); for (i = 0; i < m->num_objects; i++) { @@ -1555,7 +1549,7 @@ int expire_midx_packs(struct repository *r, const char *object_dir, unsigned fla if (flags & MIDX_PROGRESS) progress = start_delayed_progress( - r, + source->odb->repo, _("Finding and deleting unreferenced packfiles"), m->num_packs); for (i = 0; i < m->num_packs; i++) { @@ -1583,7 +1577,7 @@ int expire_midx_packs(struct repository *r, const char *object_dir, unsigned fla free(count); if (packs_to_drop.nr) - result = write_midx_internal(r, object_dir, NULL, + result = write_midx_internal(source, NULL, &packs_to_drop, NULL, NULL, flags); string_list_clear(&packs_to_drop, 0); @@ -1708,14 +1702,15 @@ static void fill_included_packs_batch(struct repository *r, free(pack_info); } -int midx_repack(struct repository *r, const char *object_dir, size_t batch_size, unsigned flags) +int midx_repack(struct odb_source *source, size_t batch_size, unsigned flags) { + struct repository *r = source->odb->repo; int result = 0; uint32_t i, packs_to_repack = 0; unsigned char *include_pack; struct child_process cmd = CHILD_PROCESS_INIT; FILE *cmd_in; - struct multi_pack_index *m = lookup_multi_pack_index(r, object_dir); + struct multi_pack_index *m = get_multi_pack_index(source); /* * When updating the default for these configuration @@ -1749,7 +1744,7 @@ int midx_repack(struct repository *r, const char *object_dir, size_t batch_size, strvec_push(&cmd.args, "pack-objects"); - strvec_pushf(&cmd.args, "%s/pack/pack", object_dir); + strvec_pushf(&cmd.args, "%s/pack/pack", source->path); if (delta_base_offset) strvec_push(&cmd.args, "--delta-base-offset"); @@ -1790,7 +1785,7 @@ int midx_repack(struct repository *r, const char *object_dir, size_t batch_size, goto cleanup; } - result = write_midx_internal(r, object_dir, NULL, NULL, NULL, NULL, + result = write_midx_internal(source, NULL, NULL, NULL, NULL, flags); cleanup: diff --git a/midx.h b/midx.h index 970d043989a5b0..d162001fbbe2b3 100644 --- a/midx.h +++ b/midx.h @@ -126,17 +126,17 @@ int prepare_multi_pack_index_one(struct odb_source *source); * Variant of write_midx_file which writes a MIDX containing only the packs * specified in packs_to_include. */ -int write_midx_file(struct repository *r, const char *object_dir, +int write_midx_file(struct odb_source *source, const char *preferred_pack_name, const char *refs_snapshot, unsigned flags); -int write_midx_file_only(struct repository *r, const char *object_dir, +int write_midx_file_only(struct odb_source *source, struct string_list *packs_to_include, const char *preferred_pack_name, const char *refs_snapshot, unsigned flags); void clear_midx_file(struct repository *r); int verify_midx_file(struct odb_source *source, unsigned flags); -int expire_midx_packs(struct repository *r, const char *object_dir, unsigned flags); -int midx_repack(struct repository *r, const char *object_dir, size_t batch_size, unsigned flags); +int expire_midx_packs(struct odb_source *source, unsigned flags); +int midx_repack(struct odb_source *source, size_t batch_size, unsigned flags); void close_midx(struct multi_pack_index *m); From 7744936f374308d6fa3c6e317fb8fe0b685d0ef2 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 11 Aug 2025 15:46:49 +0200 Subject: [PATCH 056/695] midx: stop duplicating info redundant with its owning source Multi-pack indices store some information that is redundant with their owning source: - The locality bit that tracks whether the source is the primary object source or an alternate. - The object directory path the multi-pack index is located in. - The pointer to the owning parent directory. All of this information is already contained in `struct odb_source`. So now that we always have that struct available when loading a multi-pack index we have it readily accessible. Drop the redundant information and instead store a pointer to the object source. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/repack.c | 5 +++-- midx-write.c | 9 +++++---- midx.c | 21 +++++++++++---------- midx.h | 7 ++----- pack-bitmap.c | 13 +++++++------ pack-revindex.c | 14 +++++++------- t/helper/test-read-midx.c | 2 +- 7 files changed, 36 insertions(+), 35 deletions(-) diff --git a/builtin/repack.c b/builtin/repack.c index 94dec26f185be8..5af3e27357c3ec 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -223,9 +223,10 @@ static void mark_packs_for_deletion(struct existing_packs *existing, static void remove_redundant_pack(const char *dir_name, const char *base_name) { struct strbuf buf = STRBUF_INIT; - struct multi_pack_index *m = get_multi_pack_index(the_repository->objects->sources); + struct odb_source *source = the_repository->objects->sources; + struct multi_pack_index *m = get_multi_pack_index(source); strbuf_addf(&buf, "%s.pack", base_name); - if (m && m->local && midx_contains_pack(m, buf.buf)) + if (m && source->local && midx_contains_pack(m, buf.buf)) clear_midx_file(the_repository); strbuf_insertf(&buf, 0, "%s/", dir_name); unlink_pack_path(buf.buf, 1); diff --git a/midx-write.c b/midx-write.c index bf7c01d4b1d67c..84f76856d67ee9 100644 --- a/midx-write.c +++ b/midx-write.c @@ -981,10 +981,11 @@ static int link_midx_to_chain(struct multi_pack_index *m) for (i = 0; i < ARRAY_SIZE(midx_exts); i++) { const unsigned char *hash = get_midx_checksum(m); - get_midx_filename_ext(m->repo->hash_algo, &from, m->object_dir, + get_midx_filename_ext(m->source->odb->repo->hash_algo, &from, + m->source->path, hash, midx_exts[i].non_split); - get_split_midx_filename_ext(m->repo->hash_algo, &to, - m->object_dir, hash, + get_split_midx_filename_ext(m->source->odb->repo->hash_algo, &to, + m->source->path, hash, midx_exts[i].split); if (link(from.buf, to.buf) < 0 && errno != ENOENT) { @@ -1109,7 +1110,7 @@ static int write_midx_internal(struct odb_source *source, if (flags & MIDX_WRITE_BITMAP && load_midx_revindex(m)) { error(_("could not load reverse index for MIDX %s"), hash_to_hex_algop(get_midx_checksum(m), - m->repo->hash_algo)); + m->source->odb->repo->hash_algo)); result = 1; goto cleanup; } diff --git a/midx.c b/midx.c index 831a7e9b5f2c08..81bf3c4d5f351e 100644 --- a/midx.c +++ b/midx.c @@ -26,7 +26,7 @@ int cmp_idx_or_pack_name(const char *idx_or_pack_name, const unsigned char *get_midx_checksum(struct multi_pack_index *m) { - return m->data + m->data_len - m->repo->hash_algo->rawsz; + return m->data + m->data_len - m->source->odb->repo->hash_algo->rawsz; } void get_midx_filename(const struct git_hash_algo *hash_algo, @@ -128,11 +128,10 @@ static struct multi_pack_index *load_multi_pack_index_one(struct odb_source *sou midx_map = xmmap(NULL, midx_size, PROT_READ, MAP_PRIVATE, fd, 0); close(fd); - FLEX_ALLOC_STR(m, object_dir, source->path); + CALLOC_ARRAY(m, 1); m->data = midx_map; m->data_len = midx_size; - m->local = source->local; - m->repo = r; + m->source = source; m->signature = get_be32(m->data); if (m->signature != MIDX_SIGNATURE) @@ -446,7 +445,7 @@ static uint32_t midx_for_pack(struct multi_pack_index **_m, int prepare_midx_pack(struct multi_pack_index *m, uint32_t pack_int_id) { - struct repository *r = m->repo; + struct repository *r = m->source->odb->repo; struct strbuf pack_name = STRBUF_INIT; struct strbuf key = STRBUF_INIT; struct packed_git *p; @@ -458,7 +457,7 @@ int prepare_midx_pack(struct multi_pack_index *m, if (m->packs[pack_int_id]) return 0; - strbuf_addf(&pack_name, "%s/pack/%s", m->object_dir, + strbuf_addf(&pack_name, "%s/pack/%s", m->source->path, m->pack_names[pack_int_id]); /* pack_map holds the ".pack" name, but we have the .idx */ @@ -469,7 +468,8 @@ int prepare_midx_pack(struct multi_pack_index *m, strhash(key.buf), key.buf, struct packed_git, packmap_ent); if (!p) { - p = add_packed_git(r, pack_name.buf, pack_name.len, m->local); + p = add_packed_git(r, pack_name.buf, pack_name.len, + m->source->local); if (p) { install_packed_git(r, p); list_add_tail(&p->mru, &r->objects->packed_git_mru); @@ -528,7 +528,8 @@ int bsearch_one_midx(const struct object_id *oid, struct multi_pack_index *m, uint32_t *result) { int ret = bsearch_hash(oid->hash, m->chunk_oid_fanout, - m->chunk_oid_lookup, m->repo->hash_algo->rawsz, + m->chunk_oid_lookup, + m->source->odb->repo->hash_algo->rawsz, result); if (result) *result += m->num_objects_in_base; @@ -559,7 +560,7 @@ struct object_id *nth_midxed_object_oid(struct object_id *oid, n = midx_for_object(&m, n); oidread(oid, m->chunk_oid_lookup + st_mult(m->hash_len, n), - m->repo->hash_algo); + m->source->odb->repo->hash_algo); return oid; } @@ -734,7 +735,7 @@ int prepare_multi_pack_index_one(struct odb_source *source) int midx_checksum_valid(struct multi_pack_index *m) { - return hashfile_checksum_valid(m->repo->hash_algo, + return hashfile_checksum_valid(m->source->odb->repo->hash_algo, m->data, m->data_len); } diff --git a/midx.h b/midx.h index d162001fbbe2b3..71dbdec66ef618 100644 --- a/midx.h +++ b/midx.h @@ -35,6 +35,8 @@ struct odb_source; "GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL" struct multi_pack_index { + struct odb_source *source; + const unsigned char *data; size_t data_len; @@ -50,7 +52,6 @@ struct multi_pack_index { uint32_t num_objects; int preferred_pack_idx; - int local; int has_chain; const unsigned char *chunk_pack_names; @@ -71,10 +72,6 @@ struct multi_pack_index { const char **pack_names; struct packed_git **packs; - - struct repository *repo; - - char object_dir[FLEX_ARRAY]; }; #define MIDX_PROGRESS (1 << 0) diff --git a/pack-bitmap.c b/pack-bitmap.c index fb0b11ca073856..01e14c34bd0cac 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -216,7 +216,7 @@ static uint32_t bitmap_num_objects(struct bitmap_index *index) static struct repository *bitmap_repo(struct bitmap_index *bitmap_git) { if (bitmap_is_midx(bitmap_git)) - return bitmap_git->midx->repo; + return bitmap_git->midx->source->odb->repo; return bitmap_git->pack->repo; } @@ -418,13 +418,13 @@ char *midx_bitmap_filename(struct multi_pack_index *midx) { struct strbuf buf = STRBUF_INIT; if (midx->has_chain) - get_split_midx_filename_ext(midx->repo->hash_algo, &buf, - midx->object_dir, + get_split_midx_filename_ext(midx->source->odb->repo->hash_algo, &buf, + midx->source->path, get_midx_checksum(midx), MIDX_EXT_BITMAP); else - get_midx_filename_ext(midx->repo->hash_algo, &buf, - midx->object_dir, get_midx_checksum(midx), + get_midx_filename_ext(midx->source->odb->repo->hash_algo, &buf, + midx->source->path, get_midx_checksum(midx), MIDX_EXT_BITMAP); return strbuf_detach(&buf, NULL); @@ -463,7 +463,8 @@ static int open_midx_bitmap_1(struct bitmap_index *bitmap_git, if (bitmap_git->pack || bitmap_git->midx) { struct strbuf buf = STRBUF_INIT; - get_midx_filename(midx->repo->hash_algo, &buf, midx->object_dir); + get_midx_filename(midx->source->odb->repo->hash_algo, &buf, + midx->source->path); trace2_data_string("bitmap", bitmap_repo(bitmap_git), "ignoring extra midx bitmap file", buf.buf); close(fd); diff --git a/pack-revindex.c b/pack-revindex.c index 0cc422a1e67bc8..b206518dcb51dd 100644 --- a/pack-revindex.c +++ b/pack-revindex.c @@ -379,25 +379,25 @@ int load_midx_revindex(struct multi_pack_index *m) * not want to accidentally call munmap() in the middle of the * MIDX. */ - trace2_data_string("load_midx_revindex", m->repo, + trace2_data_string("load_midx_revindex", m->source->odb->repo, "source", "midx"); m->revindex_data = (const uint32_t *)m->chunk_revindex; return 0; } - trace2_data_string("load_midx_revindex", m->repo, + trace2_data_string("load_midx_revindex", m->source->odb->repo, "source", "rev"); if (m->has_chain) - get_split_midx_filename_ext(m->repo->hash_algo, &revindex_name, - m->object_dir, get_midx_checksum(m), + get_split_midx_filename_ext(m->source->odb->repo->hash_algo, &revindex_name, + m->source->path, get_midx_checksum(m), MIDX_EXT_REV); else - get_midx_filename_ext(m->repo->hash_algo, &revindex_name, - m->object_dir, get_midx_checksum(m), + get_midx_filename_ext(m->source->odb->repo->hash_algo, &revindex_name, + m->source->path, get_midx_checksum(m), MIDX_EXT_REV); - ret = load_revindex_from_disk(m->repo->hash_algo, + ret = load_revindex_from_disk(m->source->odb->repo->hash_algo, revindex_name.buf, m->num_objects, &m->revindex_map, diff --git a/t/helper/test-read-midx.c b/t/helper/test-read-midx.c index bcb8ea767179a6..6de5d1665afbfc 100644 --- a/t/helper/test-read-midx.c +++ b/t/helper/test-read-midx.c @@ -66,7 +66,7 @@ static int read_midx_file(const char *object_dir, const char *checksum, for (i = 0; i < m->num_packs; i++) printf("%s\n", m->pack_names[i]); - printf("object-dir: %s\n", m->object_dir); + printf("object-dir: %s\n", m->source->path); if (show_objects) { struct object_id oid; From 13296ac909d53e14712f89a7f4fda94dd0465479 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 11 Aug 2025 15:46:50 +0200 Subject: [PATCH 057/695] midx: compute paths via their source With the preceding commits we started to always have the object database source available when we load, write or access multi-pack indices. With this in place we can change how MIDX paths are computed so that we don't have to pass in the combination of a hash algorithm and object directory anymore, but only the object database source. Refactor the code accordingly. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- midx-write.c | 52 ++++++++++++++++++++++------------------------- midx.c | 54 +++++++++++++++++++++++-------------------------- midx.h | 13 +++++------- pack-bitmap.c | 10 ++++----- pack-revindex.c | 8 ++++---- 5 files changed, 62 insertions(+), 75 deletions(-) diff --git a/midx-write.c b/midx-write.c index 84f76856d67ee9..1dcdf3dc0f151b 100644 --- a/midx-write.c +++ b/midx-write.c @@ -26,9 +26,9 @@ #define MIDX_CHUNK_LARGE_OFFSET_WIDTH (sizeof(uint64_t)) extern int midx_checksum_valid(struct multi_pack_index *m); -extern void clear_midx_files_ext(const char *object_dir, const char *ext, +extern void clear_midx_files_ext(struct odb_source *source, const char *ext, const char *keep_hash); -extern void clear_incremental_midx_files_ext(const char *object_dir, +extern void clear_incremental_midx_files_ext(struct odb_source *source, const char *ext, const char **keep_hashes, uint32_t hashes_nr); @@ -112,6 +112,7 @@ struct write_midx_context { struct string_list *to_include; struct repository *repo; + struct odb_source *source; }; static int should_include_pack(const struct write_midx_context *ctx, @@ -648,7 +649,6 @@ static uint32_t *midx_pack_order(struct write_midx_context *ctx) } static void write_midx_reverse_index(struct write_midx_context *ctx, - const char *object_dir, unsigned char *midx_hash) { struct strbuf buf = STRBUF_INIT; @@ -657,11 +657,10 @@ static void write_midx_reverse_index(struct write_midx_context *ctx, trace2_region_enter("midx", "write_midx_reverse_index", ctx->repo); if (ctx->incremental) - get_split_midx_filename_ext(ctx->repo->hash_algo, &buf, - object_dir, midx_hash, - MIDX_EXT_REV); + get_split_midx_filename_ext(ctx->source, &buf, + midx_hash, MIDX_EXT_REV); else - get_midx_filename_ext(ctx->repo->hash_algo, &buf, object_dir, + get_midx_filename_ext(ctx->source, &buf, midx_hash, MIDX_EXT_REV); tmp_file = write_rev_file_order(ctx->repo, NULL, ctx->pack_order, @@ -836,7 +835,6 @@ static struct commit **find_commits_for_midx_bitmap(uint32_t *indexed_commits_nr } static int write_midx_bitmap(struct write_midx_context *ctx, - const char *object_dir, const unsigned char *midx_hash, struct packing_data *pdata, struct commit **commits, @@ -852,12 +850,11 @@ static int write_midx_bitmap(struct write_midx_context *ctx, trace2_region_enter("midx", "write_midx_bitmap", ctx->repo); if (ctx->incremental) - get_split_midx_filename_ext(ctx->repo->hash_algo, &bitmap_name, - object_dir, midx_hash, - MIDX_EXT_BITMAP); + get_split_midx_filename_ext(ctx->source, &bitmap_name, + midx_hash, MIDX_EXT_BITMAP); else - get_midx_filename_ext(ctx->repo->hash_algo, &bitmap_name, - object_dir, midx_hash, MIDX_EXT_BITMAP); + get_midx_filename_ext(ctx->source, &bitmap_name, + midx_hash, MIDX_EXT_BITMAP); if (flags & MIDX_WRITE_BITMAP_HASH_CACHE) options |= BITMAP_OPT_HASH_CACHE; @@ -981,11 +978,9 @@ static int link_midx_to_chain(struct multi_pack_index *m) for (i = 0; i < ARRAY_SIZE(midx_exts); i++) { const unsigned char *hash = get_midx_checksum(m); - get_midx_filename_ext(m->source->odb->repo->hash_algo, &from, - m->source->path, + get_midx_filename_ext(m->source, &from, hash, midx_exts[i].non_split); - get_split_midx_filename_ext(m->source->odb->repo->hash_algo, &to, - m->source->path, hash, + get_split_midx_filename_ext(m->source, &to, hash, midx_exts[i].split); if (link(from.buf, to.buf) < 0 && errno != ENOENT) { @@ -1023,16 +1018,16 @@ static void clear_midx_files(struct odb_source *source, uint32_t i, j; for (i = 0; i < ARRAY_SIZE(exts); i++) { - clear_incremental_midx_files_ext(source->path, exts[i], + clear_incremental_midx_files_ext(source, exts[i], hashes, hashes_nr); for (j = 0; j < hashes_nr; j++) - clear_midx_files_ext(source->path, exts[i], hashes[j]); + clear_midx_files_ext(source, exts[i], hashes[j]); } if (incremental) - get_midx_filename(source->odb->repo->hash_algo, &buf, source->path); + get_midx_filename(source, &buf); else - get_midx_chain_filename(&buf, source->path); + get_midx_chain_filename(source, &buf); if (unlink(buf.buf) && errno != ENOENT) die_errno(_("failed to clear multi-pack-index at %s"), buf.buf); @@ -1065,6 +1060,7 @@ static int write_midx_internal(struct odb_source *source, trace2_region_enter("midx", "write_midx_internal", r); ctx.repo = r; + ctx.source = source; ctx.incremental = !!(flags & MIDX_WRITE_INCREMENTAL); @@ -1073,7 +1069,7 @@ static int write_midx_internal(struct odb_source *source, "%s/pack/multi-pack-index.d/tmp_midx_XXXXXX", source->path); else - get_midx_filename(r->hash_algo, &midx_name, source->path); + get_midx_filename(source, &midx_name); if (safe_create_leading_directories(r, midx_name.buf)) die_errno(_("unable to create leading directories of %s"), midx_name.buf); @@ -1153,7 +1149,7 @@ static int write_midx_internal(struct odb_source *source, * corresponding bitmap (or one wasn't requested). */ if (!want_bitmap) - clear_midx_files_ext(source->path, "bitmap", NULL); + clear_midx_files_ext(source, "bitmap", NULL); goto cleanup; } } @@ -1321,7 +1317,7 @@ static int write_midx_internal(struct odb_source *source, if (ctx.incremental) { struct strbuf lock_name = STRBUF_INIT; - get_midx_chain_filename(&lock_name, source->path); + get_midx_chain_filename(source, &lock_name); hold_lock_file_for_update(&lk, lock_name.buf, LOCK_DIE_ON_ERROR); strbuf_release(&lock_name); @@ -1384,7 +1380,7 @@ static int write_midx_internal(struct odb_source *source, if (flags & MIDX_WRITE_REV_INDEX && git_env_bool("GIT_TEST_MIDX_WRITE_REV", 0)) - write_midx_reverse_index(&ctx, source->path, midx_hash); + write_midx_reverse_index(&ctx, midx_hash); if (flags & MIDX_WRITE_BITMAP) { struct packing_data pdata; @@ -1407,7 +1403,7 @@ static int write_midx_internal(struct odb_source *source, FREE_AND_NULL(ctx.entries); ctx.entries_nr = 0; - if (write_midx_bitmap(&ctx, source->path, + if (write_midx_bitmap(&ctx, midx_hash, &pdata, commits, commits_nr, flags) < 0) { error(_("could not write multi-pack bitmap")); @@ -1440,8 +1436,8 @@ static int write_midx_internal(struct odb_source *source, if (link_midx_to_chain(ctx.base_midx) < 0) return -1; - get_split_midx_filename_ext(r->hash_algo, &final_midx_name, - source->path, midx_hash, MIDX_EXT_MIDX); + get_split_midx_filename_ext(source, &final_midx_name, + midx_hash, MIDX_EXT_MIDX); if (rename_tempfile(&incr, final_midx_name.buf) < 0) { error_errno(_("unable to rename new multi-pack-index layer")); diff --git a/midx.c b/midx.c index 81bf3c4d5f351e..7726c13d7e7bc0 100644 --- a/midx.c +++ b/midx.c @@ -16,9 +16,9 @@ #define MIDX_PACK_ERROR ((void *)(intptr_t)-1) int midx_checksum_valid(struct multi_pack_index *m); -void clear_midx_files_ext(const char *object_dir, const char *ext, +void clear_midx_files_ext(struct odb_source *source, const char *ext, const char *keep_hash); -void clear_incremental_midx_files_ext(const char *object_dir, const char *ext, +void clear_incremental_midx_files_ext(struct odb_source *source, const char *ext, char **keep_hashes, uint32_t hashes_nr); int cmp_idx_or_pack_name(const char *idx_or_pack_name, @@ -29,19 +29,17 @@ const unsigned char *get_midx_checksum(struct multi_pack_index *m) return m->data + m->data_len - m->source->odb->repo->hash_algo->rawsz; } -void get_midx_filename(const struct git_hash_algo *hash_algo, - struct strbuf *out, const char *object_dir) +void get_midx_filename(struct odb_source *source, struct strbuf *out) { - get_midx_filename_ext(hash_algo, out, object_dir, NULL, NULL); + get_midx_filename_ext(source, out, NULL, NULL); } -void get_midx_filename_ext(const struct git_hash_algo *hash_algo, - struct strbuf *out, const char *object_dir, +void get_midx_filename_ext(struct odb_source *source, struct strbuf *out, const unsigned char *hash, const char *ext) { - strbuf_addf(out, "%s/pack/multi-pack-index", object_dir); + strbuf_addf(out, "%s/pack/multi-pack-index", source->path); if (ext) - strbuf_addf(out, "-%s.%s", hash_to_hex_algop(hash, hash_algo), ext); + strbuf_addf(out, "-%s.%s", hash_to_hex_algop(hash, source->odb->repo->hash_algo), ext); } static int midx_read_oid_fanout(const unsigned char *chunk_start, @@ -222,24 +220,23 @@ static struct multi_pack_index *load_multi_pack_index_one(struct odb_source *sou return NULL; } -void get_midx_chain_dirname(struct strbuf *buf, const char *object_dir) +void get_midx_chain_dirname(struct odb_source *source, struct strbuf *buf) { - strbuf_addf(buf, "%s/pack/multi-pack-index.d", object_dir); + strbuf_addf(buf, "%s/pack/multi-pack-index.d", source->path); } -void get_midx_chain_filename(struct strbuf *buf, const char *object_dir) +void get_midx_chain_filename(struct odb_source *source, struct strbuf *buf) { - get_midx_chain_dirname(buf, object_dir); + get_midx_chain_dirname(source, buf); strbuf_addstr(buf, "/multi-pack-index-chain"); } -void get_split_midx_filename_ext(const struct git_hash_algo *hash_algo, - struct strbuf *buf, const char *object_dir, +void get_split_midx_filename_ext(struct odb_source *source, struct strbuf *buf, const unsigned char *hash, const char *ext) { - get_midx_chain_dirname(buf, object_dir); + get_midx_chain_dirname(source, buf); strbuf_addf(buf, "/multi-pack-index-%s.%s", - hash_to_hex_algop(hash, hash_algo), ext); + hash_to_hex_algop(hash, source->odb->repo->hash_algo), ext); } static int open_multi_pack_index_chain(const struct git_hash_algo *hash_algo, @@ -326,7 +323,7 @@ static struct multi_pack_index *load_midx_chain_fd_st(struct odb_source *source, valid = 0; strbuf_reset(&buf); - get_split_midx_filename_ext(hash_algo, &buf, source->path, + get_split_midx_filename_ext(source, &buf, layer.hash, MIDX_EXT_MIDX); m = load_multi_pack_index_one(source, buf.buf); @@ -358,7 +355,7 @@ static struct multi_pack_index *load_multi_pack_index_chain(struct odb_source *s int fd; struct multi_pack_index *m = NULL; - get_midx_chain_filename(&chain_file, source->path); + get_midx_chain_filename(source, &chain_file); if (open_multi_pack_index_chain(source->odb->repo->hash_algo, chain_file.buf, &fd, &st)) { int incomplete; /* ownership of fd is taken over by load function */ @@ -374,8 +371,7 @@ struct multi_pack_index *load_multi_pack_index(struct odb_source *source) struct strbuf midx_name = STRBUF_INIT; struct multi_pack_index *m; - get_midx_filename(source->odb->repo->hash_algo, &midx_name, - source->path); + get_midx_filename(source, &midx_name); m = load_multi_pack_index_one(source, midx_name.buf); if (!m) @@ -762,7 +758,7 @@ static void clear_midx_file_ext(const char *full_path, size_t full_path_len UNUS die_errno(_("failed to remove %s"), full_path); } -void clear_midx_files_ext(const char *object_dir, const char *ext, +void clear_midx_files_ext(struct odb_source *source, const char *ext, const char *keep_hash) { struct clear_midx_data data; @@ -776,7 +772,7 @@ void clear_midx_files_ext(const char *object_dir, const char *ext, } data.ext = ext; - for_each_file_in_pack_dir(object_dir, + for_each_file_in_pack_dir(source->path, clear_midx_file_ext, &data); @@ -785,7 +781,7 @@ void clear_midx_files_ext(const char *object_dir, const char *ext, free(data.keep); } -void clear_incremental_midx_files_ext(const char *object_dir, const char *ext, +void clear_incremental_midx_files_ext(struct odb_source *source, const char *ext, char **keep_hashes, uint32_t hashes_nr) { @@ -801,7 +797,7 @@ void clear_incremental_midx_files_ext(const char *object_dir, const char *ext, data.keep_nr = hashes_nr; data.ext = ext; - for_each_file_in_pack_subdir(object_dir, "multi-pack-index.d", + for_each_file_in_pack_subdir(source->path, "multi-pack-index.d", clear_midx_file_ext, &data); for (i = 0; i < hashes_nr; i++) @@ -813,7 +809,7 @@ void clear_midx_file(struct repository *r) { struct strbuf midx = STRBUF_INIT; - get_midx_filename(r->hash_algo, &midx, r->objects->sources->path); + get_midx_filename(r->objects->sources, &midx); if (r->objects) { struct odb_source *source; @@ -828,8 +824,8 @@ void clear_midx_file(struct repository *r) if (remove_path(midx.buf)) die(_("failed to clear multi-pack-index at %s"), midx.buf); - clear_midx_files_ext(r->objects->sources->path, MIDX_EXT_BITMAP, NULL); - clear_midx_files_ext(r->objects->sources->path, MIDX_EXT_REV, NULL); + clear_midx_files_ext(r->objects->sources, MIDX_EXT_BITMAP, NULL); + clear_midx_files_ext(r->objects->sources, MIDX_EXT_REV, NULL); strbuf_release(&midx); } @@ -888,7 +884,7 @@ int verify_midx_file(struct odb_source *source, unsigned flags) struct stat sb; struct strbuf filename = STRBUF_INIT; - get_midx_filename(r->hash_algo, &filename, source->path); + get_midx_filename(source, &filename); if (!stat(filename.buf, &sb)) { error(_("multi-pack-index file exists, but failed to parse")); diff --git a/midx.h b/midx.h index 71dbdec66ef618..e241d2d6900bc3 100644 --- a/midx.h +++ b/midx.h @@ -86,15 +86,12 @@ struct multi_pack_index { #define MIDX_EXT_MIDX "midx" const unsigned char *get_midx_checksum(struct multi_pack_index *m); -void get_midx_filename(const struct git_hash_algo *hash_algo, - struct strbuf *out, const char *object_dir); -void get_midx_filename_ext(const struct git_hash_algo *hash_algo, - struct strbuf *out, const char *object_dir, +void get_midx_filename(struct odb_source *source, struct strbuf *out); +void get_midx_filename_ext(struct odb_source *source, struct strbuf *out, const unsigned char *hash, const char *ext); -void get_midx_chain_dirname(struct strbuf *buf, const char *object_dir); -void get_midx_chain_filename(struct strbuf *buf, const char *object_dir); -void get_split_midx_filename_ext(const struct git_hash_algo *hash_algo, - struct strbuf *buf, const char *object_dir, +void get_midx_chain_dirname(struct odb_source *source, struct strbuf *out); +void get_midx_chain_filename(struct odb_source *source, struct strbuf *out); +void get_split_midx_filename_ext(struct odb_source *source, struct strbuf *buf, const unsigned char *hash, const char *ext); struct multi_pack_index *load_multi_pack_index(struct odb_source *source); diff --git a/pack-bitmap.c b/pack-bitmap.c index 01e14c34bd0cac..058bdb5d7ded0b 100644 --- a/pack-bitmap.c +++ b/pack-bitmap.c @@ -418,13 +418,12 @@ char *midx_bitmap_filename(struct multi_pack_index *midx) { struct strbuf buf = STRBUF_INIT; if (midx->has_chain) - get_split_midx_filename_ext(midx->source->odb->repo->hash_algo, &buf, - midx->source->path, + get_split_midx_filename_ext(midx->source, &buf, get_midx_checksum(midx), MIDX_EXT_BITMAP); else - get_midx_filename_ext(midx->source->odb->repo->hash_algo, &buf, - midx->source->path, get_midx_checksum(midx), + get_midx_filename_ext(midx->source, &buf, + get_midx_checksum(midx), MIDX_EXT_BITMAP); return strbuf_detach(&buf, NULL); @@ -463,8 +462,7 @@ static int open_midx_bitmap_1(struct bitmap_index *bitmap_git, if (bitmap_git->pack || bitmap_git->midx) { struct strbuf buf = STRBUF_INIT; - get_midx_filename(midx->source->odb->repo->hash_algo, &buf, - midx->source->path); + get_midx_filename(midx->source, &buf); trace2_data_string("bitmap", bitmap_repo(bitmap_git), "ignoring extra midx bitmap file", buf.buf); close(fd); diff --git a/pack-revindex.c b/pack-revindex.c index b206518dcb51dd..d0791cc4938fa2 100644 --- a/pack-revindex.c +++ b/pack-revindex.c @@ -389,12 +389,12 @@ int load_midx_revindex(struct multi_pack_index *m) "source", "rev"); if (m->has_chain) - get_split_midx_filename_ext(m->source->odb->repo->hash_algo, &revindex_name, - m->source->path, get_midx_checksum(m), + get_split_midx_filename_ext(m->source, &revindex_name, + get_midx_checksum(m), MIDX_EXT_REV); else - get_midx_filename_ext(m->source->odb->repo->hash_algo, &revindex_name, - m->source->path, get_midx_checksum(m), + get_midx_filename_ext(m->source, &revindex_name, + get_midx_checksum(m), MIDX_EXT_REV); ret = load_revindex_from_disk(m->source->odb->repo->hash_algo, From f81a574f59a61dd85ee918f8759a624d33f3539e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= Date: Mon, 11 Aug 2025 20:53:15 +0000 Subject: [PATCH 058/695] doc: test linkgit macros for well-formedness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some readers of man pages have reported that they found malformed linkgit macros in the documentation (absence or bad spelling). Signed-off-by: Jean-Noël Avila Signed-off-by: Junio C Hamano --- Documentation/gitweb.conf.adoc | 2 +- Documentation/lint-gitlink.perl | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Documentation/gitweb.conf.adoc b/Documentation/gitweb.conf.adoc index 1348e9b12504db..64bebb811c9771 100644 --- a/Documentation/gitweb.conf.adoc +++ b/Documentation/gitweb.conf.adoc @@ -178,7 +178,7 @@ $export_ok:: Show repository only if this file exists (in repository). Only effective if this variable evaluates to true. Can be set when building gitweb by setting `GITWEB_EXPORT_OK`. This path is - relative to `GIT_DIR`. git-daemon[1] uses 'git-daemon-export-ok', + relative to `GIT_DIR`. linkgit:git-daemon[1] uses 'git-daemon-export-ok', unless started with `--export-all`. By default this variable is not set, which means that this feature is turned off. diff --git a/Documentation/lint-gitlink.perl b/Documentation/lint-gitlink.perl index aea564dad7edbd..f183a18df28466 100755 --- a/Documentation/lint-gitlink.perl +++ b/Documentation/lint-gitlink.perl @@ -41,6 +41,13 @@ sub report { @ARGV = $to_check; while (<>) { my $line = $_; + while ($line =~ m/(.{,8})((git[-a-z]+|scalar)\[(\d)*\])/g) { + my $pos = pos $line; + my ($macro, $target, $page, $section) = ($1, $2, $3, $4); + if ( $macro ne "linkgit:" && $macro !~ "ifn?def::" && $macro ne "endif::" ) { + report($pos, $line, $target, "linkgit: macro expected"); + } + } while ($line =~ m/linkgit:((.*?)\[(\d)\])/g) { my $pos = pos $line; my ($target, $page, $section) = ($1, $2, $3); From 63d33eb7f6ba315c3ecdda63295d9f915d184fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= Date: Mon, 11 Aug 2025 20:53:16 +0000 Subject: [PATCH 059/695] doc: check well-formedness of delimited sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Having an empty line before each delimited sections is not required by asciidoc, but it is a safety measure that prevents generating malformed asciidoc when generating translated documentation. When a delimited section appears just after a paragraph, the asciidoc processor checks that the length of the delimited section header is different from the length of the paragraph. If it is not, the asciidoc processor will generate a title. In the original English documentation, this is not a problem because the authors always check the output of the asciidoc processor and fix the length of the delimited section header if it turns out to be the same as the paragraph length. However, this is not the case for translations, where the authors have no way to check the length of the delimited section header or the output of the asciidoc processor. This can lead to a section title that is not intended. Indeed, this test also checks that titles are correctly formed, that is, the length of the underline is equal to the length of the title (otherwise it would not be a title but a section header). Finally, this test checks that the delimited section are terminated within the same file. Signed-off-by: Jean-Noël Avila Signed-off-by: Junio C Hamano --- Documentation/Makefile | 11 ++++- Documentation/RelNotes/1.6.2.4.adoc | 1 + Documentation/diff-format.adoc | 1 + Documentation/git-commit.adoc | 1 + Documentation/git-fast-import.adoc | 2 + Documentation/git-p4.adoc | 1 + Documentation/git-rebase.adoc | 2 +- Documentation/git-svn.adoc | 2 + Documentation/gitprotocol-http.adoc | 2 +- Documentation/gitsubmodules.adoc | 3 +- Documentation/lint-delimited-sections.perl | 48 +++++++++++++++++++ Documentation/mergetools/vimdiff.adoc | 8 ++++ .../long-running-process-protocol.adoc | 1 + shared.mak | 1 + 14 files changed, 80 insertions(+), 4 deletions(-) create mode 100755 Documentation/lint-delimited-sections.perl diff --git a/Documentation/Makefile b/Documentation/Makefile index df2ce187eb84cf..76a9e1d02b2643 100644 --- a/Documentation/Makefile +++ b/Documentation/Makefile @@ -497,9 +497,17 @@ $(LINT_DOCS_FSCK_MSGIDS): ../fsck.h fsck-msgids.adoc $(call mkdir_p_parent_template) $(QUIET_GEN)$(PERL_PATH) lint-fsck-msgids.perl \ ../fsck.h fsck-msgids.adoc $@ - lint-docs-fsck-msgids: $(LINT_DOCS_FSCK_MSGIDS) +## Lint: delimited sections +LINT_DOCS_DELIMITED_SECTIONS = $(patsubst %.adoc,.build/lint-docs/delimited-sections/%.ok,$(MAN_TXT)) +$(LINT_DOCS_DELIMITED_SECTIONS): lint-delimited-sections.perl +$(LINT_DOCS_DELIMITED_SECTIONS): .build/lint-docs/delimited-sections/%.ok: %.adoc + $(call mkdir_p_parent_template) + $(QUIET_LINT_DELIMSEC)$(PERL_PATH) lint-delimited-sections.perl $< >$@ +.PHONY: lint-docs-delimited-sections +lint-docs-delimited-sections: $(LINT_DOCS_DELIMITED_SECTIONS) + lint-docs-manpages: $(QUIET_GEN)./lint-manpages.sh @@ -528,6 +536,7 @@ lint-docs: lint-docs-fsck-msgids lint-docs: lint-docs-gitlink lint-docs: lint-docs-man-end-blurb lint-docs: lint-docs-man-section-order +lint-docs: lint-docs-delimited-sections lint-docs: lint-docs-manpages lint-docs: lint-docs-meson diff --git a/Documentation/RelNotes/1.6.2.4.adoc b/Documentation/RelNotes/1.6.2.4.adoc index f4bf1d09863c71..053dbb604de6c4 100644 --- a/Documentation/RelNotes/1.6.2.4.adoc +++ b/Documentation/RelNotes/1.6.2.4.adoc @@ -37,3 +37,4 @@ exec >/var/tmp/1 echo O=$(git describe maint) O=v1.6.2.3-38-g318b847 git shortlog --no-merges $O..maint +--- diff --git a/Documentation/diff-format.adoc b/Documentation/diff-format.adoc index 80e36e153dac88..9f7e9882418349 100644 --- a/Documentation/diff-format.adoc +++ b/Documentation/diff-format.adoc @@ -103,6 +103,7 @@ if the file was renamed on any side of history. With followed by the name of the path in the merge commit. Examples for `-c` and `--cc` without `--combined-all-paths`: + ------------------------------------------------ ::100644 100644 100644 fabadb8 cc95eb0 4866510 MM desc.c ::100755 100755 100755 52b7a2d 6d1ac04 d2ac7d7 RM bar.sh diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc index ae988a883b5b86..d4d576ce665f19 100644 --- a/Documentation/git-commit.adoc +++ b/Documentation/git-commit.adoc @@ -281,6 +281,7 @@ variable (see linkgit:git-config[1]). + -- It is a rough equivalent for: + ------ $ git reset --soft HEAD^ $ ... do something else to come up with the right tree ... diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc index 6f9763c11b3cfd..6490d67fab56e1 100644 --- a/Documentation/git-fast-import.adoc +++ b/Documentation/git-fast-import.adoc @@ -605,9 +605,11 @@ Marks must be declared (via `mark`) before they can be used. The special case of restarting an incremental import from the current branch value should be written as: + ---- from refs/heads/branch^0 ---- + The `^0` suffix is necessary as fast-import does not permit a branch to start from itself, and the branch is created in memory before the `from` command is even read from the input. Adding `^0` will force diff --git a/Documentation/git-p4.adoc b/Documentation/git-p4.adoc index f97b786bf98a21..59edd241341e0d 100644 --- a/Documentation/git-p4.adoc +++ b/Documentation/git-p4.adoc @@ -66,6 +66,7 @@ Clone ~~~~~ Generally, 'git p4 clone' is used to create a new Git directory from an existing p4 repository: + ------------ $ git p4 clone //depot/path/project ------------ diff --git a/Documentation/git-rebase.adoc b/Documentation/git-rebase.adoc index 956d3048f5a618..727160c6db77fc 100644 --- a/Documentation/git-rebase.adoc +++ b/Documentation/git-rebase.adoc @@ -687,7 +687,7 @@ In addition, the following pairs of options are incompatible: * --fork-point and --root BEHAVIORAL DIFFERENCES ------------------------ +---------------------- `git rebase` has two primary backends: 'apply' and 'merge'. (The 'apply' backend used to be known as the 'am' backend, but the name led to diff --git a/Documentation/git-svn.adoc b/Documentation/git-svn.adoc index bcf7d84a87d1cc..c26c12bab37abf 100644 --- a/Documentation/git-svn.adoc +++ b/Documentation/git-svn.adoc @@ -1012,9 +1012,11 @@ branch. If you do merge, note the following rule: 'git svn dcommit' will attempt to commit on top of the SVN commit named in + ------------------------------------------------------------------------ git log --grep=^git-svn-id: --first-parent -1 ------------------------------------------------------------------------ + You 'must' therefore ensure that the most recent commit of the branch you want to dcommit to is the 'first' parent of the merge. Chaos will ensue otherwise, especially if the first parent is an older commit on diff --git a/Documentation/gitprotocol-http.adoc b/Documentation/gitprotocol-http.adoc index ec40a550ccab88..d024010414aa6d 100644 --- a/Documentation/gitprotocol-http.adoc +++ b/Documentation/gitprotocol-http.adoc @@ -318,7 +318,7 @@ Extra Parameter. Smart Service git-upload-pack ------------------------------- +----------------------------- This service reads from the repository pointed to by `$GIT_URL`. Clients MUST first perform ref discovery with diff --git a/Documentation/gitsubmodules.adoc b/Documentation/gitsubmodules.adoc index f7b5a25a0caa91..20822961999aa8 100644 --- a/Documentation/gitsubmodules.adoc +++ b/Documentation/gitsubmodules.adoc @@ -8,6 +8,7 @@ gitsubmodules - Mounting one repository inside another SYNOPSIS -------- .gitmodules, $GIT_DIR/config + ------------------ git submodule git --recurse-submodules @@ -240,7 +241,7 @@ Workflow for a third party library Workflow for an artificially split repo --------------------------------------- +--------------------------------------- # Enable recursion for relevant commands, such that # regular commands recurse into submodules by default diff --git a/Documentation/lint-delimited-sections.perl b/Documentation/lint-delimited-sections.perl new file mode 100755 index 00000000000000..140b852e5d46c1 --- /dev/null +++ b/Documentation/lint-delimited-sections.perl @@ -0,0 +1,48 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +my $exit_code = 0; +sub report { + my ($msg) = @_; + print STDERR "$ARGV:$.: $msg\n"; + $exit_code = 1; +} + +my $line_length = 0; +my $in_section = 0; +my $section_header = ""; + + +while (my $line = <>) { + if (($line =~ /^\+?$/) || + ($line =~ /^\[.*\]$/) || + ($line =~ /^ifdef::/)) { + $line_length = 0; + } elsif ($line =~ /^[^-.]/) { + $line_length = length($line); + } elsif (($line =~ /^-{3,}$/) || ($line =~ /^\.{3,}$/)) { + if ($in_section) { + if ($line eq $section_header) { + $in_section = 0; + } + next; + } + if ($line_length == 0) { + $in_section = 1; + $section_header = $line; + next; + } + if (($line_length != 0) && (length($line) != $line_length)) { + report("section delimiter not preceded by an empty line"); + } + $line_length = 0; + } +} + +if ($in_section) { + report("section not finished"); +} + +exit $exit_code; diff --git a/Documentation/mergetools/vimdiff.adoc b/Documentation/mergetools/vimdiff.adoc index abfd426f74a079..b4ab83a510e0b0 100644 --- a/Documentation/mergetools/vimdiff.adoc +++ b/Documentation/mergetools/vimdiff.adoc @@ -3,6 +3,7 @@ Description When specifying `--tool=vimdiff` in `git mergetool` Git will open Vim with a 4 windows layout distributed in the following way: + .... ------------------------------------------ | | | | @@ -56,6 +57,7 @@ needed in this case. The next layout definition is equivalent: + -- If, for some reason, we are not interested in the `BASE` buffer. + .... ------------------------------------------ | | | | @@ -72,6 +74,7 @@ If, for some reason, we are not interested in the `BASE` buffer. Only the `MERGED` buffer will be shown. Note, however, that all the other ones are still loaded in vim, and you can access them with the "buffers" command. + .... ------------------------------------------ | | @@ -88,6 +91,7 @@ command. When `MERGED` is not present in the layout, you must "mark" one of the buffers with an arobase (`@`). That will become the buffer you need to edit and save after resolving the conflicts. + .... ------------------------------------------ | | | @@ -106,6 +110,7 @@ save after resolving the conflicts. Three tabs will open: the first one is a copy of the default layout, while the other two only show the differences between (`BASE` and `LOCAL`) and (`BASE` and `REMOTE`) respectively. + .... ------------------------------------------ | | TAB #2 | TAB #3 | | @@ -119,6 +124,7 @@ the other two only show the differences between (`BASE` and `LOCAL`) and | | ------------------------------------------ .... + .... ------------------------------------------ | TAB #1 | | TAB #3 | | @@ -132,6 +138,7 @@ the other two only show the differences between (`BASE` and `LOCAL`) and | | | ------------------------------------------ .... + .... ------------------------------------------ | TAB #1 | TAB #2 | | | @@ -151,6 +158,7 @@ the other two only show the differences between (`BASE` and `LOCAL`) and -- Same as the previous example, but adds a fourth tab with the same information as the first tab, with a different layout. + .... --------------------------------------------- | TAB #1 | TAB #2 | TAB #3 | | diff --git a/Documentation/technical/long-running-process-protocol.adoc b/Documentation/technical/long-running-process-protocol.adoc index 6f33654b4288d4..39bd89d467d6b9 100644 --- a/Documentation/technical/long-running-process-protocol.adoc +++ b/Documentation/technical/long-running-process-protocol.adoc @@ -24,6 +24,7 @@ After the version negotiation Git sends a list of all capabilities that it supports and a flush packet. Git expects to read a list of desired capabilities, which must be a subset of the supported capabilities list, and a flush packet as response: + ------------------------ packet: git> git-filter-client packet: git> version=2 diff --git a/shared.mak b/shared.mak index 1a99848a95174c..57095d6cf96c12 100644 --- a/shared.mak +++ b/shared.mak @@ -88,6 +88,7 @@ ifndef V QUIET_LINT_GITLINK = @echo ' ' LINT GITLINK $<; QUIET_LINT_MANSEC = @echo ' ' LINT MAN SEC $<; + QUIET_LINT_DELIMSEC = @echo ' ' LINT DEL SEC $<; QUIET_LINT_MANEND = @echo ' ' LINT MAN END $<; export V From ed260220948595b1311d4639dbfc20f02c807fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= Date: Mon, 11 Aug 2025 20:53:17 +0000 Subject: [PATCH 060/695] doc: check for absence of multiple terms in each entry of desc list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For simplifying automated translation of the documentation, it is better to only present one term in each entry of a description list of options. This is because most of these terms can automatically be marked as notranslatable. Also, due to portability issues, the script generate-configlist.sh can no longer insert newlines in the output. However, the result is that it no longer correctly handles multiple terms in a single entry of definition lists. As a result, we now check that these entries do not exist in the documentation. Reviewed-by: Collin Funk Signed-off-by: Jean-Noël Avila Signed-off-by: Junio C Hamano --- Documentation/Makefile | 10 +++++++++ Documentation/git-check-attr.adoc | 3 ++- Documentation/git-check-ignore.adoc | 9 +++++--- Documentation/git-http-fetch.adoc | 5 ++++- Documentation/lint-documentation-style.perl | 24 +++++++++++++++++++++ Documentation/technical/api-path-walk.adoc | 5 ++++- shared.mak | 1 + 7 files changed, 51 insertions(+), 6 deletions(-) create mode 100755 Documentation/lint-documentation-style.perl diff --git a/Documentation/Makefile b/Documentation/Makefile index 76a9e1d02b2643..6fb83d0c6ebf22 100644 --- a/Documentation/Makefile +++ b/Documentation/Makefile @@ -508,6 +508,15 @@ $(LINT_DOCS_DELIMITED_SECTIONS): .build/lint-docs/delimited-sections/%.ok: %.ado .PHONY: lint-docs-delimited-sections lint-docs-delimited-sections: $(LINT_DOCS_DELIMITED_SECTIONS) +## Lint: Documentation style +LINT_DOCS_DOC_STYLE = $(patsubst %.adoc,.build/lint-docs/doc-style/%.ok,$(DOC_DEP_TXT)) +$(LINT_DOCS_DOC_STYLE): lint-documentation-style.perl +$(LINT_DOCS_DOC_STYLE): .build/lint-docs/doc-style/%.ok: %.adoc + $(call mkdir_p_parent_template) + $(QUIET_LINT_DOCSTYLE)$(PERL_PATH) lint-documentation-style.perl $< >$@ +.PHONY: lint-docs-doc-style +lint-docs-doc-style: $(LINT_DOCS_DOC_STYLE) + lint-docs-manpages: $(QUIET_GEN)./lint-manpages.sh @@ -537,6 +546,7 @@ lint-docs: lint-docs-gitlink lint-docs: lint-docs-man-end-blurb lint-docs: lint-docs-man-section-order lint-docs: lint-docs-delimited-sections +lint-docs: lint-docs-doc-style lint-docs: lint-docs-manpages lint-docs: lint-docs-meson diff --git a/Documentation/git-check-attr.adoc b/Documentation/git-check-attr.adoc index 503b6446574d18..15a37a38e3f7ff 100644 --- a/Documentation/git-check-attr.adoc +++ b/Documentation/git-check-attr.adoc @@ -19,7 +19,8 @@ For every pathname, this command will list if each attribute is 'unspecified', OPTIONS ------- --a, --all:: +-a:: +--all:: List all attributes that are associated with the specified paths. If this option is used, then 'unspecified' attributes will not be included in the output. diff --git a/Documentation/git-check-ignore.adoc b/Documentation/git-check-ignore.adoc index 3e3b4e344629d9..a6c6c1b6e5bee8 100644 --- a/Documentation/git-check-ignore.adoc +++ b/Documentation/git-check-ignore.adoc @@ -25,11 +25,13 @@ subject to exclude rules; but see `--no-index'. OPTIONS ------- --q, --quiet:: +-q:: +--quiet:: Don't output anything, just set exit status. This is only valid with a single pathname. --v, --verbose:: +-v:: +--verbose:: Instead of printing the paths that are excluded, for each path that matches an exclude pattern, print the exclude pattern together with the path. (Matching an exclude pattern usually @@ -49,7 +51,8 @@ linkgit:gitignore[5]. below). If `--stdin` is also given, input paths are separated with a NUL character instead of a linefeed character. --n, --non-matching:: +-n:: +--non-matching:: Show given paths which don't match any pattern. This only makes sense when `--verbose` is enabled, otherwise it would not be possible to distinguish between paths which match a diff --git a/Documentation/git-http-fetch.adoc b/Documentation/git-http-fetch.adoc index 4ec7c68d3b9ecd..2200f073c47120 100644 --- a/Documentation/git-http-fetch.adoc +++ b/Documentation/git-http-fetch.adoc @@ -25,8 +25,11 @@ commit-id:: Either the hash or the filename under [URL]/refs/ to pull. --a, -c, -t:: +-a:: +-c:: +-t:: These options are ignored for historical reasons. + -v:: Report what is downloaded. diff --git a/Documentation/lint-documentation-style.perl b/Documentation/lint-documentation-style.perl new file mode 100755 index 00000000000000..1f35a6a116da3c --- /dev/null +++ b/Documentation/lint-documentation-style.perl @@ -0,0 +1,24 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +my $exit_code = 0; +sub report { + my ($line, $msg) = @_; + chomp $line; + print STDERR "$ARGV:$.: '$line' $msg\n"; + $exit_code = 1; +} + +my $synopsis_style = 0; + +while (my $line = <>) { + if ($line =~ /^[ \t]*`?[-a-z0-9.]+`?(, `?[-a-z0-9.]+`?)+(::|;;)$/) { + + report($line, "multiple parameters in a definition list item"); + } +} + + +exit $exit_code; diff --git a/Documentation/technical/api-path-walk.adoc b/Documentation/technical/api-path-walk.adoc index 34c905eb9c3130..a67de1b143ab5b 100644 --- a/Documentation/technical/api-path-walk.adoc +++ b/Documentation/technical/api-path-walk.adoc @@ -39,7 +39,10 @@ It is also important that you do not specify the `--objects` flag for the the objects will be walked in a separate way based on those starting commits. -`commits`, `blobs`, `trees`, `tags`:: +`commits`:: +`blobs`:: +`trees`:: +`tags`:: By default, these members are enabled and signal that the path-walk API should call the `path_fn` on objects of these types. Specialized applications could disable some options to make it simpler to walk diff --git a/shared.mak b/shared.mak index 57095d6cf96c12..5c7bc9478544c8 100644 --- a/shared.mak +++ b/shared.mak @@ -89,6 +89,7 @@ ifndef V QUIET_LINT_GITLINK = @echo ' ' LINT GITLINK $<; QUIET_LINT_MANSEC = @echo ' ' LINT MAN SEC $<; QUIET_LINT_DELIMSEC = @echo ' ' LINT DEL SEC $<; + QUIET_LINT_DOCSTYLE = @echo ' ' LINT DOCSTYLE $<; QUIET_LINT_MANEND = @echo ' ' LINT MAN END $<; export V From 03a353bb9759a1c775ba70f1e9ee865fc38291c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= Date: Mon, 11 Aug 2025 20:53:18 +0000 Subject: [PATCH 061/695] doc: check for absence of the form --[no-]parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For better searchability, this commit adds a check to ensure that parameters expressed in the form of `--[no-]parameter` are not used in the documentation. In the place of such parameters, the documentation should list two separate parameters: `--parameter` and `--no-parameter`. Signed-off-by: Jean-Noël Avila Signed-off-by: Junio C Hamano --- Documentation/blame-options.adoc | 3 ++- Documentation/diff-options.adoc | 3 ++- Documentation/fetch-options.adoc | 15 +++++++---- Documentation/git-am.adoc | 3 ++- Documentation/git-backfill.adoc | 3 ++- Documentation/git-cat-file.adoc | 6 +++-- Documentation/git-check-ref-format.adoc | 3 ++- Documentation/git-clone.adoc | 12 ++++++--- Documentation/git-commit-graph.adoc | 3 ++- Documentation/git-commit.adoc | 3 ++- Documentation/git-config.adoc | 3 ++- Documentation/git-difftool.adoc | 9 ++++--- Documentation/git-fast-import.adoc | 3 ++- Documentation/git-fmt-merge-msg.adoc | 3 ++- Documentation/git-format-patch.adoc | 12 ++++++--- Documentation/git-fsck.adoc | 9 ++++--- Documentation/git-gc.adoc | 6 +++-- Documentation/git-index-pack.adoc | 3 ++- Documentation/git-log.adoc | 6 +++-- Documentation/git-merge-tree.adoc | 3 ++- Documentation/git-multi-pack-index.adoc | 3 ++- Documentation/git-pack-objects.adoc | 3 ++- Documentation/git-pull.adoc | 3 ++- Documentation/git-push.adoc | 18 ++++++++----- Documentation/git-range-diff.adoc | 3 ++- Documentation/git-read-tree.adoc | 3 ++- Documentation/git-reset.adoc | 3 ++- Documentation/git-send-email.adoc | 30 ++++++++++++++------- Documentation/git-send-pack.adoc | 3 ++- Documentation/git-submodule.adoc | 6 +++-- Documentation/git-update-index.adoc | 12 ++++++--- Documentation/git-upload-pack.adoc | 3 ++- Documentation/git-worktree.adoc | 12 ++++++--- Documentation/lint-documentation-style.perl | 3 +++ Documentation/merge-options.adoc | 3 ++- Documentation/scalar.adoc | 18 ++++++++----- 36 files changed, 159 insertions(+), 78 deletions(-) diff --git a/Documentation/blame-options.adoc b/Documentation/blame-options.adoc index 19ea1872388ffe..1fb948fc76f3ab 100644 --- a/Documentation/blame-options.adoc +++ b/Documentation/blame-options.adoc @@ -75,7 +75,8 @@ include::line-range-format.adoc[] iso format is used. For supported values, see the discussion of the --date option at linkgit:git-log[1]. ---[no-]progress:: +--progress:: +--no-progress:: Progress status is reported on the standard error stream by default when it is attached to a terminal. This flag enables progress reporting even if not attached to a diff --git a/Documentation/diff-options.adoc b/Documentation/diff-options.adoc index f3a35d81411f1e..f19b85142f4eea 100644 --- a/Documentation/diff-options.adoc +++ b/Documentation/diff-options.adoc @@ -505,7 +505,8 @@ endif::git-format-patch[] Turn off rename detection, even when the configuration file gives the default to do so. -`--[no-]rename-empty`:: +`--rename-empty`:: +`--no-rename-empty`:: Whether to use empty blobs as rename source. ifndef::git-format-patch[] diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc index b01372e4b3c659..d3ac31f4e2a1a7 100644 --- a/Documentation/fetch-options.adoc +++ b/Documentation/fetch-options.adoc @@ -1,4 +1,5 @@ ---[no-]all:: +--all:: +--no-all:: Fetch all remotes, except for the ones that has the `remote..skipFetchAll` configuration variable set. This overrides the configuration variable fetch.all`. @@ -88,7 +89,8 @@ This is incompatible with `--recurse-submodules=[yes|on-demand]` and takes precedence over the `fetch.output` config option. ifndef::git-pull[] ---[no-]write-fetch-head:: +--write-fetch-head:: +--no-write-fetch-head:: Write the list of remote refs fetched in the `FETCH_HEAD` file directly under `$GIT_DIR`. This is the default. Passing `--no-write-fetch-head` from the command line tells @@ -118,13 +120,16 @@ ifndef::git-pull[] Allow several and arguments to be specified. No s may be specified. ---[no-]auto-maintenance:: ---[no-]auto-gc:: +--auto-maintenance:: +--no-auto-maintenance:: +--auto-gc:: +--no-auto-gc:: Run `git maintenance run --auto` at the end to perform automatic repository maintenance if needed. (`--[no-]auto-gc` is a synonym.) This is enabled by default. ---[no-]write-commit-graph:: +--write-commit-graph:: +--no-write-commit-graph:: Write a commit-graph after fetching. This overrides the config setting `fetch.writeCommitGraph`. endif::git-pull[] diff --git a/Documentation/git-am.adoc b/Documentation/git-am.adoc index 221070de481227..b23b4fba2013c2 100644 --- a/Documentation/git-am.adoc +++ b/Documentation/git-am.adoc @@ -48,7 +48,8 @@ OPTIONS --keep-non-patch:: Pass `-b` flag to 'git mailinfo' (see linkgit:git-mailinfo[1]). ---[no-]keep-cr:: +--keep-cr:: +--no-keep-cr:: With `--keep-cr`, call 'git mailsplit' (see linkgit:git-mailsplit[1]) with the same option, to prevent it from stripping CR at the end of lines. `am.keepcr` configuration variable can be used to specify the diff --git a/Documentation/git-backfill.adoc b/Documentation/git-backfill.adoc index 95623051f789b2..b8394dcf22b6e1 100644 --- a/Documentation/git-backfill.adoc +++ b/Documentation/git-backfill.adoc @@ -57,7 +57,8 @@ OPTIONS blobs seen at a given path. The default minimum batch size is 50,000. -`--[no-]sparse`:: +`--sparse`:: +`--no-sparse`:: Only download objects if they appear at a path that matches the current sparse-checkout. If the sparse-checkout feature is enabled, then `--sparse` is assumed and can be disabled with `--no-sparse`. diff --git a/Documentation/git-cat-file.adoc b/Documentation/git-cat-file.adoc index 180d1ad363fdf8..c139f55a168d61 100644 --- a/Documentation/git-cat-file.adoc +++ b/Documentation/git-cat-file.adoc @@ -62,8 +62,10 @@ OPTIONS or to ask for a "blob" with `` being a tag object that points at it. ---[no-]mailmap:: ---[no-]use-mailmap:: +--mailmap:: +--no-mailmap:: +--use-mailmap:: +--no-use-mailmap:: Use mailmap file to map author, committer and tagger names and email addresses to canonical real names and email addresses. See linkgit:git-shortlog[1]. diff --git a/Documentation/git-check-ref-format.adoc b/Documentation/git-check-ref-format.adoc index 2aacfd18088d65..0c3abf91465788 100644 --- a/Documentation/git-check-ref-format.adoc +++ b/Documentation/git-check-ref-format.adoc @@ -98,7 +98,8 @@ a branch. OPTIONS ------- ---[no-]allow-onelevel:: +--allow-onelevel:: +--no-allow-onelevel:: Controls whether one-level refnames are accepted (i.e., refnames that do not contain multiple `/`-separated components). The default is `--no-allow-onelevel`. diff --git a/Documentation/git-clone.adoc b/Documentation/git-clone.adoc index 222d558290ed6b..031b56f09824c8 100644 --- a/Documentation/git-clone.adoc +++ b/Documentation/git-clone.adoc @@ -272,7 +272,8 @@ corresponding `--mirror` and `--no-tags` options instead. reachable from a specified remote branch or tag. This option can be specified multiple times. -`--[no-]single-branch`:: +`--single-branch`:: +`--no-single-branch`:: Clone only the history leading to the tip of a single branch, either specified by the `--branch` option or the primary branch remote's `HEAD` points at. @@ -282,7 +283,8 @@ corresponding `--mirror` and `--no-tags` options instead. branch when `--single-branch` clone was made, no remote-tracking branch is created. -`--[no-]tags`:: +`--tags`:: +`--no-tags`:: Control whether or not tags will be cloned. When `--no-tags` is given, the option will be become permanent by setting the `remote..tagOpt=--no-tags` configuration. This ensures that @@ -313,10 +315,12 @@ the clone is finished. This option is ignored if the cloned repository does not have a worktree/checkout (i.e. if any of `--no-checkout`/`-n`, `--bare`, or `--mirror` is given) -`--[no-]shallow-submodules`:: +`--shallow-submodules`:: +`--no-shallow-submodules`:: All submodules which are cloned will be shallow with a depth of 1. -`--[no-]remote-submodules`:: +`--remote-submodules`:: +`--no-remote-submodules`:: All submodules which are cloned will use the status of the submodule's remote-tracking branch to update the submodule, rather than the superproject's recorded SHA-1. Equivalent to passing `--remote` to diff --git a/Documentation/git-commit-graph.adoc b/Documentation/git-commit-graph.adoc index 50b50168045cc6..e9558173c001f1 100644 --- a/Documentation/git-commit-graph.adoc +++ b/Documentation/git-commit-graph.adoc @@ -34,7 +34,8 @@ OPTIONS object directory, `git commit-graph ...` will exit with non-zero status. ---[no-]progress:: +--progress:: +--no-progress:: Turn progress on/off explicitly. If neither is specified, progress is shown if standard error is connected to a terminal. diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc index d4d576ce665f19..54c207ad45eaa2 100644 --- a/Documentation/git-commit.adoc +++ b/Documentation/git-commit.adoc @@ -214,7 +214,8 @@ include::signoff-option.adoc[] each trailer would appear, and other details. `-n`:: -`--[no-]verify`:: +`--verify`:: +`--no-verify`:: Bypass the `pre-commit` and `commit-msg` hooks. See also linkgit:githooks[5]. diff --git a/Documentation/git-config.adoc b/Documentation/git-config.adoc index 511b2e26bfb00f..36d28451528e79 100644 --- a/Documentation/git-config.adoc +++ b/Documentation/git-config.adoc @@ -295,7 +295,8 @@ Valid ``'s include: When the color setting for `name` is undefined, the command uses `color.ui` as fallback. ---[no-]includes:: +--includes:: +--no-includes:: Respect `include.*` directives in config files when looking up values. Defaults to `off` when a specific file is given (e.g., using `--file`, `--global`, etc) and `on` when searching all diff --git a/Documentation/git-difftool.adoc b/Documentation/git-difftool.adoc index d596205eaf3bfd..064bc683471f21 100644 --- a/Documentation/git-difftool.adoc +++ b/Documentation/git-difftool.adoc @@ -77,7 +77,8 @@ with custom merge tool commands and has the same value as `$MERGED`. --tool-help:: Print a list of diff tools that may be used with `--tool`. ---[no-]symlinks:: +--symlinks:: +--no-symlinks:: 'git difftool''s default behavior is to create symlinks to the working tree when run in `--dir-diff` mode and the right-hand side of the comparison yields the same content as the file in @@ -94,7 +95,8 @@ instead. `--no-symlinks` is the default on Windows. Additionally, `$BASE` is set in the environment. -g:: ---[no-]gui:: +--gui:: +--no-gui:: When 'git-difftool' is invoked with the `-g` or `--gui` option the default diff tool will be read from the configured `diff.guitool` variable instead of `diff.tool`. This may be @@ -104,7 +106,8 @@ instead. `--no-symlinks` is the default on Windows. fallback in the order of `merge.guitool`, `diff.tool`, `merge.tool` until a tool is found. ---[no-]trust-exit-code:: +--trust-exit-code:: +--no-trust-exit-code:: Errors reported by the diff tool are ignored by default. Use `--trust-exit-code` to make 'git-difftool' exit when an invoked diff tool returns a non-zero exit code. diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc index 6490d67fab56e1..3144ffcdb689d5 100644 --- a/Documentation/git-fast-import.adoc +++ b/Documentation/git-fast-import.adoc @@ -111,7 +111,8 @@ Locations of Marks Files Like --import-marks but instead of erroring out, silently skips the file if it does not exist. ---[no-]relative-marks:: +--relative-marks:: +--no-relative-marks:: After specifying --relative-marks the paths specified with --import-marks= and --export-marks= are relative to an internal directory in the current repository. diff --git a/Documentation/git-fmt-merge-msg.adoc b/Documentation/git-fmt-merge-msg.adoc index 0f3328956dfda2..6d91620be979a2 100644 --- a/Documentation/git-fmt-merge-msg.adoc +++ b/Documentation/git-fmt-merge-msg.adoc @@ -35,7 +35,8 @@ OPTIONS Do not list one-line descriptions from the actual commits being merged. ---[no-]summary:: +--summary:: +--no-summary:: Synonyms to --log and --no-log; these are deprecated and will be removed in the future. diff --git a/Documentation/git-format-patch.adoc b/Documentation/git-format-patch.adoc index a8b53db9a6635b..048d1b98152449 100644 --- a/Documentation/git-format-patch.adoc +++ b/Documentation/git-format-patch.adoc @@ -295,7 +295,8 @@ header). Note also that `git send-email` already handles this transformation for you, and this option should not be used if you are feeding the result to `git send-email`. ---[no-]force-in-body-from:: +--force-in-body-from:: +--no-force-in-body-from:: With the e-mail sender specified via the `--from` option, by default, an in-body "From:" to identify the real author of the commit is added at the top of the commit log message if @@ -314,7 +315,8 @@ feeding the result to `git send-email`. `Cc:`, and custom) headers added so far from config or command line. ---[no-]cover-letter:: +--cover-letter:: +--no-cover-letter:: In addition to the patches, generate a cover letter file containing the branch description, shortlog and the overall diffstat. You can fill in a description in the file before sending it out. @@ -379,7 +381,8 @@ configuration options in linkgit:git-notes[1] to use this workflow). The default is `--no-notes`, unless the `format.notes` configuration is set. ---[no-]signature=:: +--signature=:: +--no-signature:: Add a signature to each message produced. Per RFC 3676 the signature is separated from the body by a line with '-- ' on it. If the signature option is omitted the signature defaults to the Git version @@ -411,7 +414,8 @@ you can use `--suffix=-patch` to get `0001-description-of-my-change-patch`. Output an all-zero hash in each patch's From header instead of the hash of the commit. ---[no-]base[=]:: +--no-base:: +--base[=]:: Record the base tree information to identify the state the patch series applies to. See the BASE TREE INFORMATION section below for details. If is "auto", a base commit is diff --git a/Documentation/git-fsck.adoc b/Documentation/git-fsck.adoc index 11203ba925c717..1751f692d42b8c 100644 --- a/Documentation/git-fsck.adoc +++ b/Documentation/git-fsck.adoc @@ -31,7 +31,8 @@ index file, all SHA-1 references in the `refs` namespace, and all reflogs Print out objects that exist but that aren't reachable from any of the reference nodes. ---[no-]dangling:: +--dangling:: +--no-dangling:: Print objects that exist but that are never 'directly' used (default). `--no-dangling` can be used to omit this information from the output. @@ -97,14 +98,16 @@ care about this output and want to speed it up further. compatible with linkgit:git-rev-parse[1], e.g. `HEAD@{1234567890}~25^2:src/`. ---[no-]progress:: +--progress:: +--no-progress:: Progress status is reported on the standard error stream by default when it is attached to a terminal, unless --no-progress or --verbose is specified. --progress forces progress status even if the standard error stream is not directed to a terminal. ---[no-]references:: +--references:: +--no-references:: Control whether to check the references database consistency via 'git refs verify'. See linkgit:git-refs[1] for details. The default is to check the references database. diff --git a/Documentation/git-gc.adoc b/Documentation/git-gc.adoc index 526ce01463d7ff..6fed646dd88394 100644 --- a/Documentation/git-gc.adoc +++ b/Documentation/git-gc.adoc @@ -53,11 +53,13 @@ configuration options such as `gc.auto` and `gc.autoPackLimit`, all other housekeeping tasks (e.g. rerere, working trees, reflog...) will be performed as well. ---[no-]detach:: +--detach:: +--no-detach:: Run in the background if the system supports it. This option overrides the `gc.autoDetach` config. ---[no-]cruft:: +--cruft:: +--no-cruft:: When expiring unreachable objects, pack them separately into a cruft pack instead of storing them as loose objects. `--cruft` is on by default. diff --git a/Documentation/git-index-pack.adoc b/Documentation/git-index-pack.adoc index 270056cf6352bd..18036953c06b22 100644 --- a/Documentation/git-index-pack.adoc +++ b/Documentation/git-index-pack.adoc @@ -36,7 +36,8 @@ OPTIONS fails if the name of packed archive does not end with .pack). ---[no-]rev-index:: +--rev-index:: +--no-rev-index:: When this flag is provided, generate a reverse index (a `.rev` file) corresponding to the given pack. If `--verify` is given, ensure that the existing diff --git a/Documentation/git-log.adoc b/Documentation/git-log.adoc index b6f3d92c435f56..e304739c5e8011 100644 --- a/Documentation/git-log.adoc +++ b/Documentation/git-log.adoc @@ -73,8 +73,10 @@ used as decoration if they match `HEAD`, `refs/heads/`, `refs/remotes/`, Print out the ref name given on the command line by which each commit was reached. -`--[no-]mailmap`:: -`--[no-]use-mailmap`:: +`--mailmap`:: +`--no-mailmap`:: +`--use-mailmap`:: +`--no-use-mailmap`:: Use mailmap file to map author and committer names and email addresses to canonical real names and email addresses. See linkgit:git-shortlog[1]. diff --git a/Documentation/git-merge-tree.adoc b/Documentation/git-merge-tree.adoc index f824eea61f1e06..271ab220e8d75b 100644 --- a/Documentation/git-merge-tree.adoc +++ b/Documentation/git-merge-tree.adoc @@ -59,7 +59,8 @@ OPTIONS do not list filenames multiple times if they have multiple conflicting stages). ---[no-]messages:: +--messages:: +--no-messages:: Write any informational messages such as "Auto-merging " or CONFLICT notices to the end of stdout. If unspecified, the default is to include these messages if there are merge diff --git a/Documentation/git-multi-pack-index.adoc b/Documentation/git-multi-pack-index.adoc index b6cd0d7f855d5f..e8073bc272327c 100644 --- a/Documentation/git-multi-pack-index.adoc +++ b/Documentation/git-multi-pack-index.adoc @@ -25,7 +25,8 @@ OPTIONS + `` must be an alternate of the current repository. ---[no-]progress:: +--progress:: +--no-progress:: Turn progress on/off explicitly. If neither is specified, progress is shown if standard error is connected to a terminal. Supported by sub-commands `write`, `verify`, `expire`, and `repack. diff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc index eba014c40615eb..71b9682485c38b 100644 --- a/Documentation/git-pack-objects.adoc +++ b/Documentation/git-pack-objects.adoc @@ -243,7 +243,8 @@ depth is 4095. Add --no-reuse-object if you want to force a uniform compression level on all data no matter the source. ---[no-]sparse:: +--sparse:: +--no-sparse:: Toggle the "sparse" algorithm to determine which objects to include in the pack, when combined with the "--revs" option. This algorithm only walks trees that appear in paths that introduce new objects. diff --git a/Documentation/git-pull.adoc b/Documentation/git-pull.adoc index 3f4ecc47301ae3..48e924a10a40c0 100644 --- a/Documentation/git-pull.adoc +++ b/Documentation/git-pull.adoc @@ -87,7 +87,8 @@ OPTIONS --verbose:: Pass --verbose to git-fetch and git-merge. ---[no-]recurse-submodules[=(yes|on-demand|no)]:: +--recurse-submodules[=(yes|on-demand|no)]:: +--no-recurse-submodules:: This option controls if new commits of populated submodules should be fetched, and if the working trees of active submodules should be updated, too (see linkgit:git-fetch[1], linkgit:git-config[1] and diff --git a/Documentation/git-push.adoc b/Documentation/git-push.adoc index d1978650d60a7c..5f5408e2c01d26 100644 --- a/Documentation/git-push.adoc +++ b/Documentation/git-push.adoc @@ -197,7 +197,8 @@ already exists on the remote side. with configuration variable `push.followTags`. For more information, see `push.followTags` in linkgit:git-config[1]. ---[no-]signed:: +--signed:: +--no-signed:: --signed=(true|false|if-asked):: GPG-sign the push request to update refs on the receiving side, to allow it to be checked by the hooks and/or be @@ -208,7 +209,8 @@ already exists on the remote side. will also fail if the actual call to `gpg --sign` fails. See linkgit:git-receive-pack[1] for the details on the receiving end. ---[no-]atomic:: +--atomic:: +--no-atomic:: Use an atomic transaction on the remote side if available. Either all refs are updated, or on error, no refs are updated. If the server does not support atomic pushes the push will fail. @@ -232,7 +234,8 @@ already exists on the remote side. repository over ssh, and you do not have the program in a directory on the default $PATH. ---[no-]force-with-lease:: +--force-with-lease:: +--no-force-with-lease:: --force-with-lease=:: --force-with-lease=::: Usually, "git push" refuses to update a remote ref that is @@ -350,7 +353,8 @@ one branch, use a `+` in front of the refspec to push (e.g `git push origin +master` to force a push to the `master` branch). See the `...` section above for details. ---[no-]force-if-includes:: +--force-if-includes:: +--no-force-if-includes:: Force an update only if the tip of the remote-tracking ref has been integrated locally. + @@ -377,7 +381,8 @@ Specifying `--no-force-if-includes` disables this behavior. linkgit:git-pull[1] and other commands. For more information, see `branch..merge` in linkgit:git-config[1]. ---[no-]thin:: +--thin:: +--no-thin:: These options are passed to linkgit:git-send-pack[1]. A thin transfer significantly reduces the amount of sent data when the sender and receiver share many of the same objects in common. The default is @@ -419,7 +424,8 @@ When using 'on-demand' or 'only', if a submodule has a "push.recurseSubmodules={on-demand,only}" or "submodule.recurse" configuration, further recursion will occur. In this case, "only" is treated as "on-demand". ---[no-]verify:: +--verify:: +--no-verify:: Toggle the pre-push hook (see linkgit:githooks[5]). The default is --verify, giving the hook a chance to prevent the push. With --no-verify, the hook is bypassed completely. diff --git a/Documentation/git-range-diff.adoc b/Documentation/git-range-diff.adoc index db0e4279b52847..b5e85d37f1bee7 100644 --- a/Documentation/git-range-diff.adoc +++ b/Documentation/git-range-diff.adoc @@ -96,7 +96,8 @@ diff. --remerge-diff:: Convenience option, equivalent to `--diff-merges=remerge`. ---[no-]notes[=]:: +--notes[=]:: +--no-notes:: This flag is passed to the `git log` program (see linkgit:git-log[1]) that generates the patches. diff --git a/Documentation/git-read-tree.adoc b/Documentation/git-read-tree.adoc index 1c48c289963063..1c04bba2b7b843 100644 --- a/Documentation/git-read-tree.adoc +++ b/Documentation/git-read-tree.adoc @@ -100,7 +100,8 @@ OPTIONS directories the index file and index output file are located in. ---[no-]recurse-submodules:: +--recurse-submodules:: +--no-recurse-submodules:: Using --recurse-submodules will update the content of all active submodules according to the commit recorded in the superproject by calling read-tree recursively, also setting the submodules' HEAD to be diff --git a/Documentation/git-reset.adoc b/Documentation/git-reset.adoc index 50e8a0ba6f6612..3b9ba9aee95203 100644 --- a/Documentation/git-reset.adoc +++ b/Documentation/git-reset.adoc @@ -90,7 +90,8 @@ but carries forward unmerged index entries. If a file that is different between __ and `HEAD` has local changes, reset is aborted. -`--[no-]recurse-submodules`:: +`--recurse-submodules`:: +`--no-recurse-submodules`:: When the working tree is updated, using `--recurse-submodules` will also recursively reset the working tree of all active submodules according to the commit recorded in the superproject, also setting diff --git a/Documentation/git-send-email.adoc b/Documentation/git-send-email.adoc index 5335502d68fc7b..11b1ab1a070af8 100644 --- a/Documentation/git-send-email.adoc +++ b/Documentation/git-send-email.adoc @@ -115,7 +115,8 @@ illustration below where `[PATCH v2 0/3]` is in reply to `[PATCH 0/2]`: Only necessary if `--compose` is also set. If `--compose` is not set, this will be prompted for. ---[no-]outlook-id-fix:: +--outlook-id-fix:: +--no-outlook-id-fix:: Microsoft Outlook SMTP servers discard the Message-ID sent via email and assign a new random Message-ID, thus breaking threads. + @@ -350,7 +351,8 @@ Automating --no-header-cmd:: Disable any header command in use. ---[no-]chain-reply-to:: +--chain-reply-to:: +--no-chain-reply-to:: If this is set, each email will be sent as a reply to the previous email sent. If disabled with `--no-chain-reply-to`, all emails after the first will be sent as replies to the first email sent. When using @@ -364,19 +366,22 @@ Automating values in the `sendemail` section. The default identity is the value of `sendemail.identity`. ---[no-]signed-off-by-cc:: +--signed-off-by-cc:: +--no-signed-off-by-cc:: If this is set, add emails found in the `Signed-off-by` trailer or `Cc:` lines to the cc list. Default is the value of `sendemail.signedOffByCc` configuration value; if that is unspecified, default to `--signed-off-by-cc`. ---[no-]cc-cover:: +--cc-cover:: +--no-cc-cover:: If this is set, emails found in `Cc:` headers in the first patch of the series (typically the cover letter) are added to the cc list for each email set. Default is the value of `sendemail.ccCover` configuration value; if that is unspecified, default to `--no-cc-cover`. ---[no-]to-cover:: +--to-cover:: +--no-to-cover:: If this is set, emails found in `To:` headers in the first patch of the series (typically the cover letter) are added to the to list for each email set. Default is the value of `sendemail.toCover` @@ -407,12 +412,14 @@ Default is the value of `sendemail.suppressCc` configuration value; if that is unspecified, default to `self` if `--suppress-from` is specified, as well as `body` if `--no-signed-off-cc` is specified. ---[no-]suppress-from:: +--suppress-from:: +--no-suppress-from:: If this is set, do not add the `From:` address to the `Cc:` list. Default is the value of `sendemail.suppressFrom` configuration value; if that is unspecified, default to `--no-suppress-from`. ---[no-]thread:: +--thread:: +--no-thread:: If this is set, the `In-Reply-To` and `References` headers will be added to each email sent. Whether each mail refers to the previous email (`deep` threading per `git format-patch` @@ -430,7 +437,8 @@ exists when `git send-email` is asked to add it (especially note that Failure to do so may not produce the expected result in the recipient's MUA. ---[no-]mailmap:: +--mailmap:: +--no-mailmap:: Use the mailmap file (see linkgit:gitmailmap[5]) to map all addresses to their canonical real name and email address. Additional mailmap data specific to `git send-email` may be provided using the @@ -459,7 +467,8 @@ have been specified, in which case default to `compose`. --dry-run:: Do everything except actually send the emails. ---[no-]format-patch:: +--format-patch:: +--no-format-patch:: When an argument may be understood either as a reference or as a file name, choose to understand it as a format-patch argument (`--format-patch`) or as a file name (`--no-format-patch`). By default, when such a conflict @@ -469,7 +478,8 @@ have been specified, in which case default to `compose`. Make `git send-email` less verbose. One line per email should be all that is output. ---[no-]validate:: +--validate:: +--no-validate:: Perform sanity checks on patches. Currently, validation means the following: + diff --git a/Documentation/git-send-pack.adoc b/Documentation/git-send-pack.adoc index b9e73f2e77b1cc..811193f16c3306 100644 --- a/Documentation/git-send-pack.adoc +++ b/Documentation/git-send-pack.adoc @@ -71,7 +71,8 @@ be in a separate packet, and the list must end with a flush packet. fails to update then the entire push will fail without changing any refs. ---[no-]signed:: +--signed:: +--no-signed:: --signed=(true|false|if-asked):: GPG-sign the push request to update refs on the receiving side, to allow it to be checked by the hooks and/or be diff --git a/Documentation/git-submodule.adoc b/Documentation/git-submodule.adoc index 87d8e0f0c563b7..2d6ac92ea45000 100644 --- a/Documentation/git-submodule.adoc +++ b/Documentation/git-submodule.adoc @@ -435,7 +435,8 @@ options carefully. clone with a history truncated to the specified number of revisions. See linkgit:git-clone[1] ---[no-]recommend-shallow:: +--recommend-shallow:: +--no-recommend-shallow:: This option is only valid for the update command. The initial clone of a submodule will use the recommended `submodule..shallow` as provided by the `.gitmodules` file @@ -447,7 +448,8 @@ options carefully. Clone new submodules in parallel with as many jobs. Defaults to the `submodule.fetchJobs` option. ---[no-]single-branch:: +--single-branch:: +--no-single-branch:: This option is only valid for the update command. Clone only one branch during update: HEAD or one specified by --branch. diff --git a/Documentation/git-update-index.adoc b/Documentation/git-update-index.adoc index 7128aed540581f..9bea9fab9ad1fa 100644 --- a/Documentation/git-update-index.adoc +++ b/Documentation/git-update-index.adoc @@ -86,7 +86,8 @@ OPTIONS --chmod=(+|-)x:: Set the execute permissions on the updated files. ---[no-]assume-unchanged:: +--assume-unchanged:: +--no-assume-unchanged:: When this flag is specified, the object names recorded for the paths are not updated. Instead, this option sets/unsets the "assume unchanged" bit for the @@ -108,18 +109,21 @@ you will need to handle the situation manually. Like `--refresh`, but checks stat information unconditionally, without regard to the "assume unchanged" setting. ---[no-]skip-worktree:: +--skip-worktree:: +--no-skip-worktree:: When one of these flags is specified, the object names recorded for the paths are not updated. Instead, these options set and unset the "skip-worktree" bit for the paths. See section "Skip-worktree bit" below for more information. ---[no-]ignore-skip-worktree-entries:: +--ignore-skip-worktree-entries:: +--no-ignore-skip-worktree-entries:: Do not remove skip-worktree (AKA "index-only") entries even when the `--remove` option was specified. ---[no-]fsmonitor-valid:: +--fsmonitor-valid:: +--no-fsmonitor-valid:: When one of these flags is specified, the object names recorded for the paths are not updated. Instead, these options set and unset the "fsmonitor valid" bit for the paths. See diff --git a/Documentation/git-upload-pack.adoc b/Documentation/git-upload-pack.adoc index 516d1639d9d05c..9167a321d08e51 100644 --- a/Documentation/git-upload-pack.adoc +++ b/Documentation/git-upload-pack.adoc @@ -25,7 +25,8 @@ repository. For push operations, see 'git send-pack'. OPTIONS ------- ---[no-]strict:: +--strict:: +--no-strict:: Do not try /.git/ if is not a Git directory. --timeout=:: diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc index 8340b7f028e6c1..389e669ac044de 100644 --- a/Documentation/git-worktree.adoc +++ b/Documentation/git-worktree.adoc @@ -200,13 +200,15 @@ To remove a locked worktree, specify `--force` twice. With `add`, detach `HEAD` in the new worktree. See "DETACHED HEAD" in linkgit:git-checkout[1]. ---[no-]checkout:: +--checkout:: +--no-checkout:: By default, `add` checks out ``, however, `--no-checkout` can be used to suppress checkout in order to make customizations, such as configuring sparse-checkout. See "Sparse checkout" in linkgit:git-read-tree[1]. ---[no-]guess-remote:: +--guess-remote:: +--no-guess-remote:: With `worktree add `, without ``, instead of creating a new branch from `HEAD`, if there exists a tracking branch in exactly one remote matching the basename of ``, @@ -216,7 +218,8 @@ To remove a locked worktree, specify `--force` twice. This can also be set up as the default behaviour by using the `worktree.guessRemote` config option. ---[no-]relative-paths:: +--relative-paths:: +--no-relative-paths:: Link worktrees using relative paths or absolute paths (default). Overrides the `worktree.useRelativePaths` config option, see linkgit:git-config[1]. @@ -224,7 +227,8 @@ This can also be set up as the default behaviour by using the With `repair`, the linking files will be updated if there's an absolute/relative mismatch, even if the links are correct. ---[no-]track:: +--track:: +--no-track:: When creating a new branch, if `` is a branch, mark it as "upstream" from the new branch. This is the default if `` is a remote-tracking branch. See diff --git a/Documentation/lint-documentation-style.perl b/Documentation/lint-documentation-style.perl index 1f35a6a116da3c..11321a151bcaf6 100755 --- a/Documentation/lint-documentation-style.perl +++ b/Documentation/lint-documentation-style.perl @@ -18,6 +18,9 @@ sub report { report($line, "multiple parameters in a definition list item"); } + if ($line =~ /^`?--\[no-\][a-z0-9-]+.*(::|;;)$/) { + report($line, "definition list item with a `--[no-]` parameter"); + } } diff --git a/Documentation/merge-options.adoc b/Documentation/merge-options.adoc index 95ef491be109e0..9d433265b2984b 100644 --- a/Documentation/merge-options.adoc +++ b/Documentation/merge-options.adoc @@ -135,7 +135,8 @@ ifdef::git-pull[] Only useful when merging. endif::git-pull[] -`--[no-]verify`:: +`--verify`:: +`--no-verify`:: By default, the pre-merge and commit-msg hooks are run. When `--no-verify` is given, these are bypassed. See also linkgit:githooks[5]. diff --git a/Documentation/scalar.adoc b/Documentation/scalar.adoc index 4bd5b150e8e1d4..f81b2832f8dfeb 100644 --- a/Documentation/scalar.adoc +++ b/Documentation/scalar.adoc @@ -71,7 +71,8 @@ HEAD[:]`. Instead of checking out the branch pointed to by the cloned repository's HEAD, check out the `` branch instead. ---[no-]single-branch:: +--single-branch:: +--no-single-branch:: Clone only the history leading to the tip of a single branch, either specified by the `--branch` option or the primary branch remote's `HEAD` points at. @@ -81,23 +82,27 @@ remote-tracking branch for the branch this option was used for the initial cloning. If the HEAD at the remote did not point at any branch when `--single-branch` clone was made, no remote-tracking branch is created. ---[no-]src:: +--src:: +--no-src:: By default, `scalar clone` places the cloned repository within a `/src` directory. Use `--no-src` to place the cloned repository directly in the `` directory. ---[no-]tags:: +--tags:: +--no-tags:: By default, `scalar clone` will fetch the tag objects advertised by the remote and future `git fetch` commands will do the same. Use `--no-tags` to avoid fetching tags in `scalar clone` and to configure the repository to avoid fetching tags in the future. To fetch tags after cloning with `--no-tags`, run `git fetch --tags`. ---[no-]full-clone:: +--full-clone:: +--no-full-clone:: A sparse-checkout is initialized by default. This behavior can be turned off via `--full-clone`. ---[no-]maintenance:: +--maintenance:: +--no-maintenance:: By default, `scalar clone` configures the enlistment to use Git's background maintenance feature. Use the `--no-maintenance` to skip this configuration. @@ -122,7 +127,8 @@ Note: when this subcommand is called in a worktree that is called `src/`, its parent directory is considered to be the Scalar enlistment. If the worktree is _not_ called `src/`, it itself will be considered to be the Scalar enlistment. ---[no-]maintenance:: +--maintenance:: +--no-maintenance:: By default, `scalar register` configures the enlistment to use Git's background maintenance feature. Use the `--no-maintenance` to skip this configuration. This does not disable any maintenance that may From 93203872d721cfe98b89de108bfaea36f102a241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= Date: Mon, 11 Aug 2025 20:53:19 +0000 Subject: [PATCH 062/695] doc:git-for-each-ref: fix styling and typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes the synopsis syntax and changes the wording of a few descriptions to be more consistent with the rest of the documentation. It is a prepartion for the next commit that checks that synopsis style is applied consistently across a manual page. Signed-off-by: Jean-Noël Avila Signed-off-by: Junio C Hamano --- Documentation/git-for-each-ref.adoc | 264 ++++++++++++++-------------- 1 file changed, 132 insertions(+), 132 deletions(-) diff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc index 060940904da21c..b69080c4a0006b 100644 --- a/Documentation/git-for-each-ref.adoc +++ b/Documentation/git-for-each-ref.adoc @@ -14,101 +14,98 @@ git for-each-ref [--count=] [--shell|--perl|--python|--tcl] [--merged[=]] [--no-merged[=]] [--contains[=]] [--no-contains[=]] [(--exclude=)...] [--start-after=] - [ --stdin | ... ] + [ --stdin | (...)] DESCRIPTION ----------- -Iterate over all refs that match `` and show them -according to the given ``, after sorting them according -to the given set of ``. If `` is given, stop after -showing that many refs. The interpolated values in `` +Iterate over all refs that match __ and show them +according to the given __, after sorting them according +to the given set of __. If __ is given, stop after +showing that many refs. The interpolated values in __ can optionally be quoted as string literals in the specified 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 +`...`:: + If one or more __ parameters 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. +`--stdin`:: + 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. +`--count=`:: + Stop after showing __ refs. ---sort=:: - A field name to sort on. Prefix `-` to sort in +`--sort=`:: + Sort on the field name __. Prefix `-` to sort in descending order of the value. When unspecified, - `refname` is used. You may use the --sort= option + `refname` is used. You may use the `--sort=` option multiple times, in which case the last key becomes the primary key. ---format=:: +`--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) + `\0` (_NUL_), `%09` to `\t` (_TAB_), and `%0a` to `\n` (_LF_). + +When unspecified, __ defaults to `%(objectname) SPC %(objecttype) TAB %(refname)`. ---color[=]:: +`--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:: +`--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. + a scriptlet that can directly be "eval"ed. ---points-at=:: +`--points-at=`:: Only list refs which points at the given object. ---merged[=]:: +`--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). + specified commit (`HEAD` if not specified). ---contains[=]:: - Only list refs which contain the specified commit (HEAD if not +`--no-merged[=]`:: + Only list refs whose tips are not reachable from __(`HEAD` if not specified). ---no-contains[=]:: - Only list refs which don't contain the specified commit (HEAD +`--contains[=]`:: + Only list refs which contain __(`HEAD` if not specified). + +`--no-contains[=]`:: + Only list refs which don't contain __ (`HEAD` if not specified). ---ignore-case:: +`--ignore-case`:: Sorting and filtering refs are case insensitive. ---omit-empty:: +`--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. +`--exclude=`:: + If one or more `--exclude` options are given, only refs which do not + match any __ parameters are shown. Matching is done + using the same rules as __ above. ---include-root-refs:: - List root refs (HEAD and pseudorefs) apart from regular refs. +`--include-root-refs`:: + List root refs (`HEAD` and pseudorefs) apart from regular refs. ---start-after=:: +`--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 @@ -126,44 +123,44 @@ keys. For all objects, the following names can be used: -refname:: - The name of the ref (the part after $GIT_DIR/). +`refname`:: + The name of the ref (the part after `$GIT_DIR/`). For a non-ambiguous short name of the ref append `:short`. - The option core.warnAmbiguousRefs is used to select the strict - abbreviation mode. If `lstrip=` (`rstrip=`) is appended, strips `` + The option `core.warnAmbiguousRefs` is used to select the strict + abbreviation mode. If `lstrip=` (`rstrip=`) is appended, strip __ slash-separated path components from the front (back) of the refname (e.g. `%(refname:lstrip=2)` turns `refs/tags/foo` into `foo` and `%(refname:rstrip=2)` turns `refs/tags/foo` into `refs`). - If `` is a negative number, strip as many path components as - necessary from the specified end to leave `-` path components + If __ is a negative number, strip as many path components as + necessary from the specified end to leave `-` path components (e.g. `%(refname:lstrip=-2)` turns `refs/tags/foo` into `tags/foo` and `%(refname:rstrip=-1)` turns `refs/tags/foo` into `refs`). When the ref does not have enough components, the result becomes an empty string if - stripping with positive , or it becomes the full refname if - stripping with negative . Neither is an error. + stripping with positive __, or it becomes the full refname if + stripping with negative __. Neither is an error. + `strip` can be used as a synonym to `lstrip`. -objecttype:: +`objecttype`:: The type of the object (`blob`, `tree`, `commit`, `tag`). -objectsize:: +`objectsize`:: The size of the object (the same as 'git cat-file -s' reports). Append `:disk` to get the size, in bytes, that the object takes up on - disk. See the note about on-disk sizes in the `CAVEATS` section below. -objectname:: + disk. See the note about on-disk sizes in the 'CAVEATS' section below. +`objectname`:: The object name (aka SHA-1). For a non-ambiguous abbreviation of the object name append `:short`. For an abbreviation of the object name with desired length append - `:short=`, where the minimum length is MINIMUM_ABBREV. The + `:short=`, where the minimum length is `MINIMUM_ABBREV`. The length may be exceeded to ensure unique object names. -deltabase:: +`deltabase`:: This expands to the object name of the delta base for the given object, if it is stored as a delta. Otherwise it expands to the null object name (all zeroes). -upstream:: +`upstream`:: The name of a local ref which can be considered ``upstream'' from the displayed ref. Respects `:short`, `:lstrip` and `:rstrip` in the same way as `refname` above. Additionally @@ -185,100 +182,103 @@ Has no effect if the ref does not have tracking information associated with it. All the options apart from `nobracket` are mutually exclusive, but if used together the last option is selected. -push:: +`push`:: The name of a local ref which represents the `@{push}` location for the displayed ref. Respects `:short`, `:lstrip`, `:rstrip`, `:track`, `:trackshort`, `:remotename`, and `:remoteref` options as `upstream` does. Produces an empty string if no `@{push}` ref is configured. -HEAD:: - '*' if HEAD matches current ref (the checked out branch), ' ' +`HEAD`:: + `*` if `HEAD` matches current ref (the checked out branch), ' ' otherwise. -color:: +`color`:: Change output color. Followed by `:`, where color names are described under Values in the "CONFIGURATION FILE" section of linkgit:git-config[1]. For example, `%(color:bold red)`. -align:: +`align`:: Left-, middle-, or right-align the content between - %(align:...) and %(end). The "align:" is followed by + `%(align:...)` and `%(end)`. The "`align:`" is followed by `width=` and `position=` in any order - separated by a comma, where the `` is either left, - right or middle, default being left and `` is the total + separated by a comma, where the __ is either `left`, + `right` or `middle`, default being `left` and __ is the total length of the content with alignment. For brevity, the "width=" and/or "position=" prefixes may be omitted, and bare - and used instead. For instance, + __ and __ used instead. For instance, `%(align:,)`. If the contents length is more than the width then no alignment is performed. If used with - `--quote` everything in between %(align:...) and %(end) is + `--quote` everything in between `%(align:...)` and `%(end)` is quoted, but if nested then only the topmost level performs quoting. -if:: - Used as %(if)...%(then)...%(end) or - %(if)...%(then)...%(else)...%(end). If there is an atom with - value or string literal after the %(if) then everything after - the %(then) is printed, else if the %(else) atom is used, then +`if`:: + Used as `%(if)...%(then)...%(end)` or + `%(if)...%(then)...%(else)...%(end)`. If there is an atom with + value or string literal after the `%(if)` then everything after + the `%(then)` is printed, else if the `%(else)` atom is used, then everything after %(else) is printed. We ignore space when - evaluating the string before %(then), this is useful when we - use the %(HEAD) atom which prints either "*" or " " and we - want to apply the 'if' condition only on the 'HEAD' ref. - Append ":equals=" or ":notequals=" to compare - the value between the %(if:...) and %(then) atoms with the + evaluating the string before `%(then)`, this is useful when we + use the `%(HEAD)` atom which prints either "`*`" or " " and we + want to apply the 'if' condition only on the `HEAD` ref. + Append "`:equals=`" or "`:notequals=`" to compare + the value between the `%(if:...)` and `%(then)` atoms with the given string. -symref:: +`symref`:: The ref which the given symbolic ref refers to. If not a symbolic ref, nothing is printed. Respects the `:short`, `:lstrip` and `:rstrip` options in the same way as `refname` above. -signature:: +`signature`:: The GPG signature of a commit. -signature:grade:: - Show "G" for a good (valid) signature, "B" for a bad - signature, "U" for a good signature with unknown validity, "X" - for a good signature that has expired, "Y" for a good - signature made by an expired key, "R" for a good signature - made by a revoked key, "E" if the signature cannot be - checked (e.g. missing key) and "N" for no signature. - -signature:signer:: +`signature:grade`:: + Show +`G`;; for a good (valid) signature +`B`;; for a bad signature +`U`;; for a good signature with unknown validity +`X`;; for a good signature that has expired +`Y`;; for a good signature made by an expired key +`R`;; for a good signature made by a revoked key +`E`;; if the signature cannot be checked (e.g. missing key) +`N`;; for no signature. + +`signature:signer`:: The signer of the GPG signature of a commit. -signature:key:: +`signature:key`:: The key of the GPG signature of a commit. -signature:fingerprint:: +`signature:fingerprint`:: The fingerprint of the GPG signature of a commit. -signature:primarykeyfingerprint:: +`signature:primarykeyfingerprint`:: The primary key fingerprint of the GPG signature of a commit. -signature:trustlevel:: +`signature:trustlevel`:: The trust level of the GPG signature of a commit. Possible outputs are `ultimate`, `fully`, `marginal`, `never` and `undefined`. -worktreepath:: +`worktreepath`:: The absolute path to the worktree in which the ref is checked out, if it is checked out in any linked worktree. Empty string otherwise. -ahead-behind::: +`ahead-behind:`:: Two integers, separated by a space, demonstrating the number of commits ahead and behind, respectively, when comparing the output - ref to the `` specified in the format. + ref to the __ specified in the format. -is-base::: - In at most one row, `()` will appear to indicate the ref +`is-base:`:: + In at most one row, `()` will appear to indicate the ref that is most likely the ref used as a starting point for the branch - that produced ``. This choice is made using a heuristic: + that produced __. This choice is made using a heuristic: choose the ref that minimizes the number of commits in the - first-parent history of `` and not in the first-parent + first-parent history of __ and not in the first-parent history of the ref. + For example, consider the following figure of first-parent histories of @@ -312,29 +312,29 @@ common first-parent ancestor of `B` and `C` and ties are broken by the earliest ref in the sorted order. + Note that this token will not appear if the first-parent history of -`` does not intersect the first-parent histories of the +__ does not intersect the first-parent histories of the filtered refs. -describe[:options]:: +`describe[: