From 7b0c37953d2e9198309ca6b6faf10bb5deeb4837 Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 1 Oct 2025 16:02:50 +0200 Subject: [PATCH 01/18] SubmittingPatches: add section about AI As more and more developer tools use AI, we are facing two main risks related to AI generated content: - its situation regarding copyright and license is not clear, and: - more and more bad quality content could be submitted for review to the mailing list. To mitigate both risks, let's add an "Use of Artificial Intelligence" section to "Documentation/SubmittingPatches" with the goal of discouraging its blind use to generate content that is submitted to the project, while still allowing us to benefit from its help in some innovative, useful and less risky ways. Helped-by: Rick Sanders Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- Documentation/SubmittingPatches | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index 86ca7f6a78a9b6..04191e29458953 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -446,6 +446,34 @@ highlighted above. Only capitalize the very first letter of the trailer, i.e. favor "Signed-off-by" over "Signed-Off-By" and "Acked-by:" over "Acked-By". +[[ai]] +=== Use of Artificial Intelligence (AI) + +The Developer's Certificate of Origin requires contributors to certify +that they know the origin of their contributions to the project and +that they have the right to submit it under the project's license. +It's not yet clear that this can be legally satisfied when submitting +significant amount of content that has been generated by AI tools. + +Another issue with AI generated content is that AIs still often +hallucinate or just produce bad code, commit messages, documentation +or output, even when you point out their mistakes. + +To avoid these issues, we will reject anything that looks AI +generated, that sounds overly formal or bloated, that looks like AI +slop, that looks good on the surface but makes no sense, or that +senders don’t understand or cannot explain. + +We strongly recommend using AI tools carefully and responsibly. + +Contributors would often benefit more from AI by using it to guide and +help them step by step towards producing a solution by themselves +rather than by asking for a full solution that they would then mostly +copy-paste. They can also use AI to help with debugging, or with +checking for obvious mistakes, things that can be improved, things +that don’t match our style, guidelines or our feedback, before sending +it to us. + [[git-tools]] === Generate your patch using Git tools out of your commits. From a35952b493f24941ad47233ac01ac9fea305d07f Mon Sep 17 00:00:00 2001 From: Todd Zullinger Date: Wed, 3 Jul 2024 11:37:31 -0400 Subject: [PATCH 02/18] t/lib-gpg: add prepare_gnupghome() to create GNUPGHOME dir We create the $GNUPGHOME directory in both the GPG and GPGSSH prereqs. Replace the redundancy with a function. Use `mkdir -p` to ensure we do not fail if a test includes more than one of these prereqs. Signed-off-by: Todd Zullinger Signed-off-by: Junio C Hamano --- t/lib-gpg.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh index add11e88fc00d7..4e44f182bb6421 100644 --- a/t/lib-gpg.sh +++ b/t/lib-gpg.sh @@ -9,6 +9,11 @@ GNUPGHOME="$PWD/gpghome" export GNUPGHOME +prepare_gnupghome () { + mkdir -p "$GNUPGHOME" && + chmod 0700 "$GNUPGHOME" +} + test_lazy_prereq GPG ' gpg_version=$(gpg --version 2>&1) test $? != 127 || exit 1 @@ -38,8 +43,7 @@ test_lazy_prereq GPG ' # To export ownertrust: # gpg --homedir /tmp/gpghome --export-ownertrust \ # > lib-gpg/ownertrust - mkdir "$GNUPGHOME" && - chmod 0700 "$GNUPGHOME" && + prepare_gnupghome && (gpgconf --kill all || : ) && gpg --homedir "${GNUPGHOME}" --import \ "$TEST_DIRECTORY"/lib-gpg/keyring.gpg && @@ -132,8 +136,7 @@ test_lazy_prereq GPGSSH ' test $? = 0 || exit 1; # Setup some keys and an allowed signers file - mkdir -p "${GNUPGHOME}" && - chmod 0700 "${GNUPGHOME}" && + prepare_gnupghome && (setfacl -k "${GNUPGHOME}" 2>/dev/null || true) && ssh-keygen -t ed25519 -N "" -C "git ed25519 key" -f "${GPGSSH_KEY_PRIMARY}" >/dev/null && ssh-keygen -t rsa -b 2048 -N "" -C "git rsa2048 key" -f "${GPGSSH_KEY_SECONDARY}" >/dev/null && From 6cd8369ef394176978b962edd8676d74ecd1c400 Mon Sep 17 00:00:00 2001 From: Todd Zullinger Date: Wed, 3 Jul 2024 11:37:32 -0400 Subject: [PATCH 03/18] t/lib-gpg: call prepare_gnupghome() in GPG2 prereq The GPG2 prereq added in 2f36339fa8 (t/lib-gpg: introduce new prereq GPG2, 2023-06-04) does not create the $GNUPGHOME directory. Tests which use the GPG2 prereq without previously using the GPG prereq fail because of the missing directory. This currently affects t1016-compatObjectFormat. Ensure $GNUPGHOME is created in the GPG2 prereq. Signed-off-by: Todd Zullinger Signed-off-by: Junio C Hamano --- t/lib-gpg.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/t/lib-gpg.sh b/t/lib-gpg.sh index 4e44f182bb6421..21666b2ab066c2 100644 --- a/t/lib-gpg.sh +++ b/t/lib-gpg.sh @@ -66,6 +66,7 @@ test_lazy_prereq GPG2 ' exit 1 ;; *) + prepare_gnupghome && (gpgconf --kill all || : ) && gpg --homedir "${GNUPGHOME}" --import \ "$TEST_DIRECTORY"/lib-gpg/keyring.gpg && From 026ad6016070748a66ed9a977ad90efc08df2225 Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Tue, 21 Oct 2025 13:25:55 -0500 Subject: [PATCH 04/18] builtin/repo: rename repo_info() to cmd_repo_info() Subcommand functions are often prefixed with `cmd_` to denote that they are an entrypoint. Rename repo_info() to cmd_repo_info() accordingly. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- builtin/repo.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/repo.c b/builtin/repo.c index bbb0966f2d2284..eeeab8fbd277dd 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -136,8 +136,8 @@ static int parse_format_cb(const struct option *opt, return 0; } -static int repo_info(int argc, const char **argv, const char *prefix, - struct repository *repo) +static int cmd_repo_info(int argc, const char **argv, const char *prefix, + struct repository *repo) { enum output_format format = FORMAT_KEYVALUE; struct option options[] = { @@ -161,7 +161,7 @@ int cmd_repo(int argc, const char **argv, const char *prefix, { parse_opt_subcommand_fn *fn = NULL; struct option options[] = { - OPT_SUBCOMMAND("info", &fn, repo_info), + OPT_SUBCOMMAND("info", &fn, cmd_repo_info), OPT_END() }; From eafc03dbe316478acff5eef3b70c037de4758f08 Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Tue, 21 Oct 2025 13:25:56 -0500 Subject: [PATCH 05/18] ref-filter: allow NULL filter pattern When setting up `struct ref_filter` for filter_refs(), the `name_patterns` field must point to an array of pattern strings even if no patterns are required. To improve this interface, treat a NULL `name_patterns` field the same as when it points to an empty array. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- ref-filter.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ref-filter.c b/ref-filter.c index 520d2539c9e163..2cb5a166d61f09 100644 --- a/ref-filter.c +++ b/ref-filter.c @@ -2664,7 +2664,7 @@ static int match_name_as_path(const char **pattern, const char *refname, /* Return 1 if the refname matches one of the patterns, otherwise 0. */ static int filter_pattern_match(struct ref_filter *filter, const char *refname) { - if (!*filter->name_patterns) + if (!filter->name_patterns || !*filter->name_patterns) return 1; /* No pattern always matches */ if (filter->match_as_path) return match_name_as_path(filter->name_patterns, refname, @@ -2751,7 +2751,7 @@ static int for_each_fullref_in_pattern(struct ref_filter *filter, return for_each_fullref_with_seek(filter, cb, cb_data, 0); } - if (!filter->name_patterns[0]) { + if (!filter->name_patterns || !filter->name_patterns[0]) { /* no patterns; we have to look at everything */ return for_each_fullref_with_seek(filter, cb, cb_data, 0); } From 6d1997f6cbc10ac03bc450630de4410762f77b6f Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Tue, 21 Oct 2025 13:25:57 -0500 Subject: [PATCH 06/18] ref-filter: export ref_kind_from_refname() When filtering refs, `ref_kind_from_refname()` is used to determine the ref type. In a subsequent commit, this same logic is reused when counting refs by type. Export the function to prepare for this change. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- ref-filter.c | 2 +- ref-filter.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ref-filter.c b/ref-filter.c index 2cb5a166d61f09..30cc488d8ab659 100644 --- a/ref-filter.c +++ b/ref-filter.c @@ -2833,7 +2833,7 @@ struct ref_array_item *ref_array_push(struct ref_array *array, return ref; } -static int ref_kind_from_refname(const char *refname) +int ref_kind_from_refname(const char *refname) { unsigned int i; diff --git a/ref-filter.h b/ref-filter.h index f22ca94b49df7c..4ed1edf09a2560 100644 --- a/ref-filter.h +++ b/ref-filter.h @@ -135,6 +135,8 @@ struct ref_format { OPT_STRVEC(0, "exclude", &(var)->exclude, \ N_("pattern"), N_("exclude refs which match pattern")) +/* Get the reference kind from the provided reference name. */ +int ref_kind_from_refname(const char *refname); /* * API for filtering a set of refs. Based on the type of refs the user * has requested, we iterate through those refs and apply filters From bbb2b9334856ae0a2b18e65e5924a42c31a83c6b Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Tue, 21 Oct 2025 13:25:58 -0500 Subject: [PATCH 07/18] builtin/repo: introduce structure subcommand The structure of a repository's history can have huge impacts on the performance and health of the repository itself. Currently, Git lacks a means to surface repository metrics regarding its structure/shape via a single command. Acquiring this information requires users to be familiar with the relevant data points and the various Git commands required to surface them. To fill this gap, supplemental tools such as git-sizer(1) have been developed. To allow users to more readily identify repository structure related information, introduce the "structure" subcommand in git-repo(1). The goal of this subcommand is to eventually provide similar functionality to git-sizer(1), but natively in Git. The initial version of this command only iterates through all references in the repository and tracks the count of branches, tags, remote refs, and other reference types. The corresponding information is displayed in a human-friendly table formatted in a very similar manner to git-sizer(1). The width of each table column is adjusted automatically to satisfy the requirements of the widest row contained. Subsequent commits will surface additional relevant data points to output and also provide other more machine-friendly output formats. Based-on-patch-by: Derrick Stolee Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- Documentation/git-repo.adoc | 10 ++ builtin/repo.c | 200 ++++++++++++++++++++++++++++++++++++ t/meson.build | 1 + t/t1901-repo-structure.sh | 61 +++++++++++ 4 files changed, 272 insertions(+) create mode 100755 t/t1901-repo-structure.sh diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc index 209afd1b6152be..8193298dd532e6 100644 --- a/Documentation/git-repo.adoc +++ b/Documentation/git-repo.adoc @@ -9,6 +9,7 @@ SYNOPSIS -------- [synopsis] git repo info [--format=(keyvalue|nul)] [-z] [...] +git repo structure DESCRIPTION ----------- @@ -43,6 +44,15 @@ supported: + `-z` is an alias for `--format=nul`. +`structure`:: + Retrieve statistics about the current repository structure. The + following kinds of information are reported: ++ +* Reference counts categorized by type + ++ +The table output format may change and is not intended for machine parsing. + INFO KEYS --------- In order to obtain a set of values from `git repo info`, you should provide diff --git a/builtin/repo.c b/builtin/repo.c index eeeab8fbd277dd..e77e8db563d410 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -4,12 +4,16 @@ #include "environment.h" #include "parse-options.h" #include "quote.h" +#include "ref-filter.h" #include "refs.h" #include "strbuf.h" +#include "string-list.h" #include "shallow.h" +#include "utf8.h" static const char *const repo_usage[] = { "git repo info [--format=(keyvalue|nul)] [-z] [...]", + "git repo structure", NULL }; @@ -156,12 +160,208 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix, return print_fields(argc, argv, repo, format); } +struct ref_stats { + size_t branches; + size_t remotes; + size_t tags; + size_t others; +}; + +struct stats_table { + struct string_list rows; + + int name_col_width; + int value_col_width; +}; + +/* + * Holds column data that gets stored for each row. + */ +struct stats_table_entry { + char *value; +}; + +static void stats_table_vaddf(struct stats_table *table, + struct stats_table_entry *entry, + const char *format, va_list ap) +{ + struct strbuf buf = STRBUF_INIT; + struct string_list_item *item; + char *formatted_name; + int name_width; + + strbuf_vaddf(&buf, format, ap); + formatted_name = strbuf_detach(&buf, NULL); + name_width = utf8_strwidth(formatted_name); + + item = string_list_append_nodup(&table->rows, formatted_name); + item->util = entry; + + if (name_width > table->name_col_width) + table->name_col_width = name_width; + if (entry) { + int value_width = utf8_strwidth(entry->value); + if (value_width > table->value_col_width) + table->value_col_width = value_width; + } +} + +static void stats_table_addf(struct stats_table *table, const char *format, ...) +{ + va_list ap; + + va_start(ap, format); + stats_table_vaddf(table, NULL, format, ap); + va_end(ap); +} + +static void stats_table_count_addf(struct stats_table *table, size_t value, + const char *format, ...) +{ + struct stats_table_entry *entry; + va_list ap; + + CALLOC_ARRAY(entry, 1); + entry->value = xstrfmt("%" PRIuMAX, (uintmax_t)value); + + va_start(ap, format); + stats_table_vaddf(table, entry, format, ap); + va_end(ap); +} + +static inline size_t get_total_reference_count(struct ref_stats *stats) +{ + return stats->branches + stats->remotes + stats->tags + stats->others; +} + +static void stats_table_setup_structure(struct stats_table *table, + struct ref_stats *refs) +{ + size_t ref_total; + + ref_total = get_total_reference_count(refs); + stats_table_addf(table, "* %s", _("References")); + stats_table_count_addf(table, ref_total, " * %s", _("Count")); + stats_table_count_addf(table, refs->branches, " * %s", _("Branches")); + stats_table_count_addf(table, refs->tags, " * %s", _("Tags")); + stats_table_count_addf(table, refs->remotes, " * %s", _("Remotes")); + stats_table_count_addf(table, refs->others, " * %s", _("Others")); +} + +static void stats_table_print_structure(const struct stats_table *table) +{ + const char *name_col_title = _("Repository structure"); + const char *value_col_title = _("Value"); + int name_col_width = utf8_strwidth(name_col_title); + int value_col_width = utf8_strwidth(value_col_title); + struct string_list_item *item; + + if (table->name_col_width > name_col_width) + name_col_width = table->name_col_width; + if (table->value_col_width > value_col_width) + value_col_width = table->value_col_width; + + printf("| %-*s | %-*s |\n", name_col_width, name_col_title, + value_col_width, value_col_title); + printf("| "); + for (int i = 0; i < name_col_width; i++) + putchar('-'); + printf(" | "); + for (int i = 0; i < value_col_width; i++) + putchar('-'); + printf(" |\n"); + + for_each_string_list_item(item, &table->rows) { + struct stats_table_entry *entry = item->util; + const char *value = ""; + + if (entry) { + struct stats_table_entry *entry = item->util; + value = entry->value; + } + + printf("| %-*s | %*s |\n", name_col_width, item->string, + value_col_width, value); + } +} + +static void stats_table_clear(struct stats_table *table) +{ + struct stats_table_entry *entry; + struct string_list_item *item; + + for_each_string_list_item(item, &table->rows) { + entry = item->util; + if (entry) + free(entry->value); + } + + string_list_clear(&table->rows, 1); +} + +static int count_references(const char *refname, + const char *referent UNUSED, + const struct object_id *oid UNUSED, + int flags UNUSED, void *cb_data) +{ + struct ref_stats *stats = cb_data; + + switch (ref_kind_from_refname(refname)) { + case FILTER_REFS_BRANCHES: + stats->branches++; + break; + case FILTER_REFS_REMOTES: + stats->remotes++; + break; + case FILTER_REFS_TAGS: + stats->tags++; + break; + case FILTER_REFS_OTHERS: + stats->others++; + break; + default: + BUG("unexpected reference type"); + } + + return 0; +} + +static void structure_count_references(struct ref_stats *stats, + struct repository *repo) +{ + refs_for_each_ref(get_main_ref_store(repo), count_references, &stats); +} + +static int cmd_repo_structure(int argc, const char **argv, const char *prefix, + struct repository *repo) +{ + struct stats_table table = { + .rows = STRING_LIST_INIT_DUP, + }; + struct ref_stats stats = { 0 }; + struct option options[] = { 0 }; + + argc = parse_options(argc, argv, prefix, options, repo_usage, 0); + if (argc) + usage(_("too many arguments")); + + structure_count_references(&stats, repo); + + stats_table_setup_structure(&table, &stats); + stats_table_print_structure(&table); + + stats_table_clear(&table); + + return 0; +} + int cmd_repo(int argc, const char **argv, const char *prefix, struct repository *repo) { parse_opt_subcommand_fn *fn = NULL; struct option options[] = { OPT_SUBCOMMAND("info", &fn, cmd_repo_info), + OPT_SUBCOMMAND("structure", &fn, cmd_repo_structure), OPT_END() }; diff --git a/t/meson.build b/t/meson.build index 7974795fe48c3a..9e426f8edc87a0 100644 --- a/t/meson.build +++ b/t/meson.build @@ -236,6 +236,7 @@ integration_tests = [ 't1701-racy-split-index.sh', 't1800-hook.sh', 't1900-repo.sh', + 't1901-repo-structure.sh', 't2000-conflict-when-checking-files-out.sh', 't2002-checkout-cache-u.sh', 't2003-checkout-cache-mkdir.sh', diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh new file mode 100755 index 00000000000000..e592eea0eb3285 --- /dev/null +++ b/t/t1901-repo-structure.sh @@ -0,0 +1,61 @@ +#!/bin/sh + +test_description='test git repo structure' + +. ./test-lib.sh + +test_expect_success 'empty repository' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + cat >expect <<-\EOF && + | Repository structure | Value | + | -------------------- | ----- | + | * References | | + | * Count | 0 | + | * Branches | 0 | + | * Tags | 0 | + | * Remotes | 0 | + | * Others | 0 | + EOF + + git repo structure >out 2>err && + + test_cmp expect out && + test_line_count = 0 err + ) +' + +test_expect_success 'repository with references' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + git commit --allow-empty -m init && + git tag -a foo -m bar && + + oid="$(git rev-parse HEAD)" && + git update-ref refs/remotes/origin/foo "$oid" && + + git notes add -m foo && + + cat >expect <<-\EOF && + | Repository structure | Value | + | -------------------- | ----- | + | * References | | + | * Count | 4 | + | * Branches | 1 | + | * Tags | 1 | + | * Remotes | 1 | + | * Others | 1 | + EOF + + git repo structure >out 2>err && + + test_cmp expect out && + test_line_count = 0 err + ) +' + +test_done From eb5cf58ffcd4bb117c870d448b0df0193df52c82 Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Tue, 21 Oct 2025 13:25:59 -0500 Subject: [PATCH 08/18] builtin/repo: add object counts in structure output The amount of objects in a repository can provide insight regarding its shape. To surface this information, use the path-walk API to count the number of reachable objects in the repository by object type. All regular references are used to determine the reachable set of objects. The object counts are appended to the same table containing the reference information. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- Documentation/git-repo.adoc | 1 + builtin/repo.c | 105 +++++++++++++++++++++++++++++++++--- t/t1901-repo-structure.sh | 19 ++++++- 3 files changed, 117 insertions(+), 8 deletions(-) diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc index 8193298dd532e6..ae62d2415fd92a 100644 --- a/Documentation/git-repo.adoc +++ b/Documentation/git-repo.adoc @@ -49,6 +49,7 @@ supported: following kinds of information are reported: + * Reference counts categorized by type +* Reachable object counts categorized by type + The table output format may change and is not intended for machine parsing. diff --git a/builtin/repo.c b/builtin/repo.c index e77e8db563d410..f39f06ee8cf04f 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -3,9 +3,11 @@ #include "builtin.h" #include "environment.h" #include "parse-options.h" +#include "path-walk.h" #include "quote.h" #include "ref-filter.h" #include "refs.h" +#include "revision.h" #include "strbuf.h" #include "string-list.h" #include "shallow.h" @@ -167,6 +169,18 @@ struct ref_stats { size_t others; }; +struct object_stats { + size_t tags; + size_t commits; + size_t trees; + size_t blobs; +}; + +struct repo_structure { + struct ref_stats refs; + struct object_stats objects; +}; + struct stats_table { struct string_list rows; @@ -234,9 +248,17 @@ static inline size_t get_total_reference_count(struct ref_stats *stats) return stats->branches + stats->remotes + stats->tags + stats->others; } +static inline size_t get_total_object_count(struct object_stats *stats) +{ + return stats->tags + stats->commits + stats->trees + stats->blobs; +} + static void stats_table_setup_structure(struct stats_table *table, - struct ref_stats *refs) + struct repo_structure *stats) { + struct object_stats *objects = &stats->objects; + struct ref_stats *refs = &stats->refs; + size_t object_total; size_t ref_total; ref_total = get_total_reference_count(refs); @@ -246,6 +268,15 @@ static void stats_table_setup_structure(struct stats_table *table, stats_table_count_addf(table, refs->tags, " * %s", _("Tags")); stats_table_count_addf(table, refs->remotes, " * %s", _("Remotes")); stats_table_count_addf(table, refs->others, " * %s", _("Others")); + + object_total = get_total_object_count(objects); + stats_table_addf(table, ""); + stats_table_addf(table, "* %s", _("Reachable objects")); + stats_table_count_addf(table, object_total, " * %s", _("Count")); + stats_table_count_addf(table, objects->commits, " * %s", _("Commits")); + stats_table_count_addf(table, objects->trees, " * %s", _("Trees")); + stats_table_count_addf(table, objects->blobs, " * %s", _("Blobs")); + stats_table_count_addf(table, objects->tags, " * %s", _("Tags")); } static void stats_table_print_structure(const struct stats_table *table) @@ -299,12 +330,18 @@ static void stats_table_clear(struct stats_table *table) string_list_clear(&table->rows, 1); } +struct count_references_data { + struct ref_stats *stats; + struct rev_info *revs; +}; + static int count_references(const char *refname, const char *referent UNUSED, - const struct object_id *oid UNUSED, + const struct object_id *oid, int flags UNUSED, void *cb_data) { - struct ref_stats *stats = cb_data; + struct count_references_data *data = cb_data; + struct ref_stats *stats = data->stats; switch (ref_kind_from_refname(refname)) { case FILTER_REFS_BRANCHES: @@ -323,13 +360,64 @@ static int count_references(const char *refname, BUG("unexpected reference type"); } + /* + * While iterating through references for counting, also add OIDs in + * preparation for the path walk. + */ + add_pending_oid(data->revs, NULL, oid, 0); + return 0; } static void structure_count_references(struct ref_stats *stats, + struct rev_info *revs, struct repository *repo) { - refs_for_each_ref(get_main_ref_store(repo), count_references, &stats); + struct count_references_data data = { + .stats = stats, + .revs = revs, + }; + + refs_for_each_ref(get_main_ref_store(repo), count_references, &data); +} + + +static int count_objects(const char *path UNUSED, struct oid_array *oids, + enum object_type type, void *cb_data) +{ + struct object_stats *stats = cb_data; + + switch (type) { + case OBJ_TAG: + stats->tags += oids->nr; + break; + case OBJ_COMMIT: + stats->commits += oids->nr; + break; + case OBJ_TREE: + stats->trees += oids->nr; + break; + case OBJ_BLOB: + stats->blobs += oids->nr; + break; + default: + BUG("invalid object type"); + } + + return 0; +} + +static void structure_count_objects(struct object_stats *stats, + struct rev_info *revs) +{ + struct path_walk_info info = PATH_WALK_INFO_INIT; + + info.revs = revs; + info.path_fn = count_objects; + info.path_fn_data = stats; + + walk_objects_by_path(&info); + path_walk_info_clear(&info); } static int cmd_repo_structure(int argc, const char **argv, const char *prefix, @@ -338,19 +426,24 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix, struct stats_table table = { .rows = STRING_LIST_INIT_DUP, }; - struct ref_stats stats = { 0 }; + struct repo_structure stats = { 0 }; + struct rev_info revs; struct option options[] = { 0 }; argc = parse_options(argc, argv, prefix, options, repo_usage, 0); if (argc) usage(_("too many arguments")); - structure_count_references(&stats, repo); + repo_init_revisions(repo, &revs, prefix); + + structure_count_references(&stats.refs, &revs, repo); + structure_count_objects(&stats.objects, &revs); stats_table_setup_structure(&table, &stats); stats_table_print_structure(&table); stats_table_clear(&table); + release_revisions(&revs); return 0; } diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh index e592eea0eb3285..c32cf4e239627f 100755 --- a/t/t1901-repo-structure.sh +++ b/t/t1901-repo-structure.sh @@ -18,6 +18,13 @@ test_expect_success 'empty repository' ' | * Tags | 0 | | * Remotes | 0 | | * Others | 0 | + | | | + | * Reachable objects | | + | * Count | 0 | + | * Commits | 0 | + | * Trees | 0 | + | * Blobs | 0 | + | * Tags | 0 | EOF git repo structure >out 2>err && @@ -27,17 +34,18 @@ test_expect_success 'empty repository' ' ) ' -test_expect_success 'repository with references' ' +test_expect_success 'repository with references and objects' ' test_when_finished "rm -rf repo" && git init repo && ( cd repo && - git commit --allow-empty -m init && + test_commit_bulk 42 && git tag -a foo -m bar && oid="$(git rev-parse HEAD)" && git update-ref refs/remotes/origin/foo "$oid" && + # Also creates a commit, tree, and blob. git notes add -m foo && cat >expect <<-\EOF && @@ -49,6 +57,13 @@ test_expect_success 'repository with references' ' | * Tags | 1 | | * Remotes | 1 | | * Others | 1 | + | | | + | * Reachable objects | | + | * Count | 130 | + | * Commits | 43 | + | * Trees | 43 | + | * Blobs | 43 | + | * Tags | 1 | EOF git repo structure >out 2>err && From 17215675b5a2c2eab54b295a7e92d953af2e8779 Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Tue, 21 Oct 2025 13:26:00 -0500 Subject: [PATCH 09/18] builtin/repo: add keyvalue and nul format for structure stats All repository structure stats are outputted in a human-friendly table form. This format is not suitable for machine parsing. Add a --format option that supports three output modes: `table`, `keyvalue`, and `nul`. The `table` mode is the default format and prints the same table output as before. With the `keyvalue` mode, each line of output contains a key-value pair of a repository stat. The '=' character is used to delimit between keys and values. The `nul` mode is similar to `keyvalue`, but key-values are delimited by a NUL character instead of a newline. Also, instead of a '=' character to delimit between keys and values, a newline character is used. This allows stat values to support special characters without having to cquote them. These two new modes provides output that is more machine-friendly. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- Documentation/git-repo.adoc | 25 +++++++++++++++-- builtin/repo.c | 55 ++++++++++++++++++++++++++++++++++--- t/t1901-repo-structure.sh | 33 ++++++++++++++++++++++ 3 files changed, 106 insertions(+), 7 deletions(-) diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc index ae62d2415fd92a..ce43cb19c8b03c 100644 --- a/Documentation/git-repo.adoc +++ b/Documentation/git-repo.adoc @@ -9,7 +9,7 @@ SYNOPSIS -------- [synopsis] git repo info [--format=(keyvalue|nul)] [-z] [...] -git repo structure +git repo structure [--format=(table|keyvalue|nul)] DESCRIPTION ----------- @@ -44,7 +44,7 @@ supported: + `-z` is an alias for `--format=nul`. -`structure`:: +`structure [--format=(table|keyvalue|nul)]`:: Retrieve statistics about the current repository structure. The following kinds of information are reported: + @@ -52,7 +52,26 @@ supported: * Reachable object counts categorized by type + -The table output format may change and is not intended for machine parsing. +The output format can be chosen through the flag `--format`. Three formats are +supported: ++ +`table`::: + Outputs repository stats in a human-friendly table. This format may + change and is not intended for machine parsing. This is the default + format. + +`keyvalue`::: + Each line of output contains a key-value pair for a repository stat. + The '=' character is used to delimit between the key and the value. + Values containing "unusual" characters are quoted as explained for the + configuration variable `core.quotePath` (see linkgit:git-config[1]). + +`nul`::: + Similar to `keyvalue`, but uses a NUL character to delimit between + key-value pairs instead of a newline. Also uses a newline character as + the delimiter between the key and value instead of '='. Unlike the + `keyvalue` format, values containing "unusual" characters are never + quoted. INFO KEYS --------- diff --git a/builtin/repo.c b/builtin/repo.c index f39f06ee8cf04f..1754cc7e5de3d7 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -15,13 +15,14 @@ static const char *const repo_usage[] = { "git repo info [--format=(keyvalue|nul)] [-z] [...]", - "git repo structure", + "git repo structure [--format=(table|keyvalue|nul)]", NULL }; typedef int get_value_fn(struct repository *repo, struct strbuf *buf); enum output_format { + FORMAT_TABLE, FORMAT_KEYVALUE, FORMAT_NUL_TERMINATED, }; @@ -136,6 +137,8 @@ static int parse_format_cb(const struct option *opt, *format = FORMAT_NUL_TERMINATED; else if (!strcmp(arg, "keyvalue")) *format = FORMAT_KEYVALUE; + else if (!strcmp(arg, "table")) + *format = FORMAT_TABLE; else die(_("invalid format '%s'"), arg); @@ -158,6 +161,8 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix, }; argc = parse_options(argc, argv, prefix, options, repo_usage, 0); + if (format != FORMAT_KEYVALUE && format != FORMAT_NUL_TERMINATED) + die(_("unsupported output format")); return print_fields(argc, argv, repo, format); } @@ -330,6 +335,30 @@ static void stats_table_clear(struct stats_table *table) string_list_clear(&table->rows, 1); } +static void structure_keyvalue_print(struct repo_structure *stats, + char key_delim, char value_delim) +{ + printf("references.branches.count%c%" PRIuMAX "%c", key_delim, + (uintmax_t)stats->refs.branches, value_delim); + printf("references.tags.count%c%" PRIuMAX "%c", key_delim, + (uintmax_t)stats->refs.tags, value_delim); + printf("references.remotes.count%c%" PRIuMAX "%c", key_delim, + (uintmax_t)stats->refs.remotes, value_delim); + printf("references.others.count%c%" PRIuMAX "%c", key_delim, + (uintmax_t)stats->refs.others, value_delim); + + printf("objects.commits.count%c%" PRIuMAX "%c", key_delim, + (uintmax_t)stats->objects.commits, value_delim); + printf("objects.trees.count%c%" PRIuMAX "%c", key_delim, + (uintmax_t)stats->objects.trees, value_delim); + printf("objects.blobs.count%c%" PRIuMAX "%c", key_delim, + (uintmax_t)stats->objects.blobs, value_delim); + printf("objects.tags.count%c%" PRIuMAX "%c", key_delim, + (uintmax_t)stats->objects.tags, value_delim); + + fflush(stdout); +} + struct count_references_data { struct ref_stats *stats; struct rev_info *revs; @@ -426,9 +455,15 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix, struct stats_table table = { .rows = STRING_LIST_INIT_DUP, }; + enum output_format format = FORMAT_TABLE; struct repo_structure stats = { 0 }; struct rev_info revs; - struct option options[] = { 0 }; + struct option options[] = { + OPT_CALLBACK_F(0, "format", &format, N_("format"), + N_("output format"), + PARSE_OPT_NONEG, parse_format_cb), + OPT_END() + }; argc = parse_options(argc, argv, prefix, options, repo_usage, 0); if (argc) @@ -439,8 +474,20 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix, structure_count_references(&stats.refs, &revs, repo); structure_count_objects(&stats.objects, &revs); - stats_table_setup_structure(&table, &stats); - stats_table_print_structure(&table); + switch (format) { + case FORMAT_TABLE: + stats_table_setup_structure(&table, &stats); + stats_table_print_structure(&table); + break; + case FORMAT_KEYVALUE: + structure_keyvalue_print(&stats, '=', '\n'); + break; + case FORMAT_NUL_TERMINATED: + structure_keyvalue_print(&stats, '\n', '\0'); + break; + default: + BUG("invalid output format"); + } stats_table_clear(&table); release_revisions(&revs); diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh index c32cf4e239627f..14bd8aede5e66a 100755 --- a/t/t1901-repo-structure.sh +++ b/t/t1901-repo-structure.sh @@ -73,4 +73,37 @@ test_expect_success 'repository with references and objects' ' ) ' +test_expect_success 'keyvalue and nul format' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit_bulk 42 && + git tag -a foo -m bar && + + cat >expect <<-\EOF && + references.branches.count=1 + references.tags.count=1 + references.remotes.count=0 + references.others.count=0 + objects.commits.count=42 + objects.trees.count=42 + objects.blobs.count=42 + objects.tags.count=1 + EOF + + git repo structure --format=keyvalue >out 2>err && + + test_cmp expect out && + test_line_count = 0 err && + + # Replace key and value delimiters for nul format. + tr "\n=" "\0\n" expect_nul && + git repo structure --format=nul >out 2>err && + + test_cmp expect_nul out && + test_line_count = 0 err + ) +' + test_done From 16a93c03c7824a40b034a6ee1cb1c68c8ef48682 Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Tue, 21 Oct 2025 13:26:01 -0500 Subject: [PATCH 10/18] builtin/repo: add progress meter for structure stats When using the structure subcommand for git-repo(1), evaluating a repository may take some time depending on its shape. Add a progress meter to provide feedback to the user about what is happening. The progress meter is enabled by default when the command is executed from a tty. It can also be explicitly enabled/disabled via the --[no-]progress option. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- builtin/repo.c | 46 ++++++++++++++++++++++++++++++++++----- t/t1901-repo-structure.sh | 20 +++++++++++++++++ 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/builtin/repo.c b/builtin/repo.c index 1754cc7e5de3d7..9d4749f79befa8 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -4,6 +4,7 @@ #include "environment.h" #include "parse-options.h" #include "path-walk.h" +#include "progress.h" #include "quote.h" #include "ref-filter.h" #include "refs.h" @@ -362,6 +363,7 @@ static void structure_keyvalue_print(struct repo_structure *stats, struct count_references_data { struct ref_stats *stats; struct rev_info *revs; + struct progress *progress; }; static int count_references(const char *refname, @@ -371,6 +373,7 @@ static int count_references(const char *refname, { struct count_references_data *data = cb_data; struct ref_stats *stats = data->stats; + size_t ref_count; switch (ref_kind_from_refname(refname)) { case FILTER_REFS_BRANCHES: @@ -395,26 +398,41 @@ static int count_references(const char *refname, */ add_pending_oid(data->revs, NULL, oid, 0); + ref_count = get_total_reference_count(stats); + display_progress(data->progress, ref_count); + return 0; } static void structure_count_references(struct ref_stats *stats, struct rev_info *revs, - struct repository *repo) + struct repository *repo, + int show_progress) { struct count_references_data data = { .stats = stats, .revs = revs, }; + if (show_progress) + data.progress = start_delayed_progress(repo, + _("Counting references"), 0); + refs_for_each_ref(get_main_ref_store(repo), count_references, &data); + stop_progress(&data.progress); } +struct count_objects_data { + struct object_stats *stats; + struct progress *progress; +}; static int count_objects(const char *path UNUSED, struct oid_array *oids, enum object_type type, void *cb_data) { - struct object_stats *stats = cb_data; + struct count_objects_data *data = cb_data; + struct object_stats *stats = data->stats; + size_t object_count; switch (type) { case OBJ_TAG: @@ -433,20 +451,31 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids, BUG("invalid object type"); } + object_count = get_total_object_count(stats); + display_progress(data->progress, object_count); + return 0; } static void structure_count_objects(struct object_stats *stats, - struct rev_info *revs) + struct rev_info *revs, + struct repository *repo, int show_progress) { struct path_walk_info info = PATH_WALK_INFO_INIT; + struct count_objects_data data = { + .stats = stats, + }; info.revs = revs; info.path_fn = count_objects; - info.path_fn_data = stats; + info.path_fn_data = &data; + + if (show_progress) + data.progress = start_delayed_progress(repo, _("Counting objects"), 0); walk_objects_by_path(&info); path_walk_info_clear(&info); + stop_progress(&data.progress); } static int cmd_repo_structure(int argc, const char **argv, const char *prefix, @@ -458,10 +487,12 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix, enum output_format format = FORMAT_TABLE; struct repo_structure stats = { 0 }; struct rev_info revs; + int show_progress = -1; struct option options[] = { OPT_CALLBACK_F(0, "format", &format, N_("format"), N_("output format"), PARSE_OPT_NONEG, parse_format_cb), + OPT_BOOL(0, "progress", &show_progress, N_("show progress")), OPT_END() }; @@ -471,8 +502,11 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix, repo_init_revisions(repo, &revs, prefix); - structure_count_references(&stats.refs, &revs, repo); - structure_count_objects(&stats.objects, &revs); + if (show_progress < 0) + show_progress = isatty(2); + + structure_count_references(&stats.refs, &revs, repo, show_progress); + structure_count_objects(&stats.objects, &revs, repo, show_progress); switch (format) { case FORMAT_TABLE: diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh index 14bd8aede5e66a..36a71a144e3f74 100755 --- a/t/t1901-repo-structure.sh +++ b/t/t1901-repo-structure.sh @@ -106,4 +106,24 @@ test_expect_success 'keyvalue and nul format' ' ) ' +test_expect_success 'progress meter option' ' + test_when_finished "rm -rf repo" && + git init repo && + ( + cd repo && + test_commit foo && + + GIT_PROGRESS_DELAY=0 git repo structure --progress >out 2>err && + + test_file_not_empty out && + test_grep "Counting references: 2, done." err && + test_grep "Counting objects: 3, done." err && + + GIT_PROGRESS_DELAY=0 git repo structure --no-progress >out 2>err && + + test_file_not_empty out && + test_line_count = 0 err + ) +' + test_done From 595be20d22401538534197430cdee6b26f67533e Mon Sep 17 00:00:00 2001 From: Thomas Uhle Date: Sat, 25 Oct 2025 22:30:07 +0200 Subject: [PATCH 11/18] contrib/credential: add install target Add an install target rule to the Makefiles in contrib/credential in the same manner as in other Makefiles in contrib such as for contacts or subtree. Signed-off-by: Thomas Uhle Signed-off-by: Junio C Hamano --- contrib/credential/libsecret/Makefile | 7 ++++++- contrib/credential/osxkeychain/Makefile | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/contrib/credential/libsecret/Makefile b/contrib/credential/libsecret/Makefile index 7cacc576818338..9309cfb78c7f6c 100644 --- a/contrib/credential/libsecret/Makefile +++ b/contrib/credential/libsecret/Makefile @@ -10,6 +10,7 @@ gitexecdir ?= $(prefix)/libexec/git-core CC ?= gcc CFLAGS ?= -g -O2 -Wall PKG_CONFIG ?= pkg-config +INSTALL ?= install RM ?= rm -f INCS:=$(shell $(PKG_CONFIG) --cflags libsecret-1 glib-2.0) @@ -21,7 +22,11 @@ LIBS:=$(shell $(PKG_CONFIG) --libs libsecret-1 glib-2.0) git-credential-libsecret: git-credential-libsecret.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) $(LIBS) +install: git-credential-libsecret + $(INSTALL) -d -m 755 $(DESTDIR)$(gitexecdir) + $(INSTALL) -m 755 $< $(DESTDIR)$(gitexecdir) + clean: $(RM) git-credential-libsecret git-credential-libsecret.o -.PHONY: all clean +.PHONY: all install clean diff --git a/contrib/credential/osxkeychain/Makefile b/contrib/credential/osxkeychain/Makefile index c7d9121022b0e5..9680717abe44c6 100644 --- a/contrib/credential/osxkeychain/Makefile +++ b/contrib/credential/osxkeychain/Makefile @@ -9,6 +9,7 @@ gitexecdir ?= $(prefix)/libexec/git-core CC ?= gcc CFLAGS ?= -g -O2 -Wall +INSTALL ?= install RM ?= rm -f %.o: %.c @@ -18,7 +19,11 @@ git-credential-osxkeychain: git-credential-osxkeychain.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) \ -framework Security -framework CoreFoundation +install: git-credential-osxkeychain + $(INSTALL) -d -m 755 $(DESTDIR)$(gitexecdir) + $(INSTALL) -m 755 $< $(DESTDIR)$(gitexecdir) + clean: $(RM) git-credential-osxkeychain git-credential-osxkeychain.o -.PHONY: all clean +.PHONY: all install clean From 6661cde2bef0cdb1649be1f1b5f95af7c08a6059 Mon Sep 17 00:00:00 2001 From: Xinyu Ruan Date: Fri, 24 Oct 2025 08:38:14 +0000 Subject: [PATCH 12/18] refs: add missing remove_on_disk implementation for debug backend The debug ref backend (refs_be_debug) was missing the remove_on_disk function pointer, which caused a segmentation fault when running 'GIT_TRACE_REFS=1 git refs migrate --ref-format=reftable' commands. Signed-off-by: Xinyu Ruan Acked-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- refs/debug.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/refs/debug.c b/refs/debug.c index 697adbd0dc3f65..c59c1728a3d31c 100644 --- a/refs/debug.c +++ b/refs/debug.c @@ -47,6 +47,14 @@ static int debug_create_on_disk(struct ref_store *refs, int flags, struct strbuf return res; } +static int debug_remove_on_disk(struct ref_store *refs, struct strbuf *err) +{ + struct debug_ref_store *drefs = (struct debug_ref_store *)refs; + int res = drefs->refs->be->remove_on_disk(drefs->refs, err); + trace_printf_key(&trace_refs, "remove_on_disk: %d\n", res); + return res; +} + static int debug_transaction_prepare(struct ref_store *refs, struct ref_transaction *transaction, struct strbuf *err) @@ -432,6 +440,7 @@ struct ref_storage_be refs_be_debug = { .init = NULL, .release = debug_release, .create_on_disk = debug_create_on_disk, + .remove_on_disk = debug_remove_on_disk, /* * None of these should be NULL. If the "files" backend (in From 29181abeadb20421ffa39c1b6a51c0d59598b9d6 Mon Sep 17 00:00:00 2001 From: Queen Ediri Jessa Date: Mon, 27 Oct 2025 12:16:00 +0100 Subject: [PATCH 13/18] MyFirstContribution: add note on confirming patches Add a note after the `git send-email` section explaining how contributors can confirm that their patches reached the mailing list by checking https://lore.kernel.org/git/. This helps contributors verify that their emails were successfully delivered. Signed-off-by: Queen Ediri Jessa Signed-off-by: Junio C Hamano --- Documentation/MyFirstContribution.adoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Documentation/MyFirstContribution.adoc b/Documentation/MyFirstContribution.adoc index 02ba8ba5f6514e..f186dfbc898fd4 100644 --- a/Documentation/MyFirstContribution.adoc +++ b/Documentation/MyFirstContribution.adoc @@ -1153,6 +1153,11 @@ NOTE: When you are sending a real patch, it will go to git@vger.kernel.org - but please don't send your patchset from the tutorial to the real mailing list! For now, you can send it to yourself, to make sure you understand how it will look. +NOTE: After sending your patches, you can confirm that they reached the mailing +list by visiting https://lore.kernel.org/git/. Use the search bar to find your +name or the subject of your patch. If it appears, your email was successfully +delivered. + After you run the command above, you will be presented with an interactive prompt for each patch that's about to go out. This gives you one last chance to edit or quit sending something (but again, don't edit code this way). Once you From d24220b9e81e75c14a9e67d71a60dd81a1ba5467 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Tue, 28 Oct 2025 19:19:19 +0100 Subject: [PATCH 14/18] doc: git-checkout: fix placeholder markup The placeholder markup is underscore (_), not backtick (`) as well. The inline-verbatim markup (backticks) handle interior formatting. This means in this case that it applies HTML `` to the underscores and `` to the placeholder. That is the effect, anyway; we can see from the rest of 042d6f34 (doc: git-checkout: clarify `-b` and `-B`, 2025-09-10) that this was probably an unintended mix-up. Acked-by: Julia Evans Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/git-checkout.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/git-checkout.adoc b/Documentation/git-checkout.adoc index 431185ca0bad3d..6f281b298efac1 100644 --- a/Documentation/git-checkout.adoc +++ b/Documentation/git-checkout.adoc @@ -61,7 +61,7 @@ uncommitted changes. `git checkout -B []`:: The same as `-b`, except that if the branch already exists it - resets `__` to the start point instead of failing. + resets __ to the start point instead of failing. `git checkout --detach []`:: `git checkout [--detach] `:: @@ -155,7 +155,7 @@ of it"). `-B `:: The same as `-b`, except that if the branch already exists it - resets `__` to the start point instead of failing. + resets __ to the start point instead of failing. `-t`:: `--track[=(direct|inherit)]`:: From f711f37b05b7ff11038a707ec1f0f72aca98581c Mon Sep 17 00:00:00 2001 From: "Eric W. Biederman" Date: Tue, 28 Oct 2025 11:01:45 -0500 Subject: [PATCH 15/18] t1016-compatObjectFormat: really freeze time for reproduciblity The strategy in t1016-compatObjectFormat is to build two trees with identical commits, one tree encoded in sha1 the other tree encoded in sha256 and to use the compatibility code to test and see if the two trees are identical. GPG signatures include the current time as part of the signature. To make gpg deterministic I forced the use of gpg --faked-system-time. Unfortunately I did not look closely enough. By default gpg still allows time to move forward with --faked-system-time. So in those rare instances when the system is heavily loaded and gpg runs slower than other times, signatures over the exact same data differ due to timestamps with a minuscule difference. Reading through the gpg documentation with a close eye, time can be frozen by including an exclamation point at the end of the argument to --faked-system-time. Add the exclamation point so gpg really runs with a fixed notion of time, resulting in the exact same data having identical gpg signatures. That is enough that I can run "t1016-compatObjectFormat.sh --stress" and I don't see any failures. It is possible a future change to gpg will make replay protection more robust and not provide a way to allow two separate runs of gpg to produce exactly the same signature for exactly the same data. If that happens a deeper comparison of the two repositories will need to be performed. A comparison that simply verifies the signatures and compares the data for equality. For now that is a lot of work for no gain so I am just documenting the possibility. Signed-off-by: Eric W. Biederman Signed-off-by: Junio C Hamano --- t/t1016-compatObjectFormat.sh | 6 ++++++ t/t1016/gpg | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/t/t1016-compatObjectFormat.sh b/t/t1016-compatObjectFormat.sh index e88362fbe4286b..c1beac2db177b8 100755 --- a/t/t1016-compatObjectFormat.sh +++ b/t/t1016-compatObjectFormat.sh @@ -21,6 +21,12 @@ test_description='Test how well compatObjectFormat works' # different hash functions result in the same content in the commits. # This means that when the commit is translated between hash functions # the commit is identical to the commit in the other repository. +# +# Similarly this test relies on: +# gpg --faked-system-time '20230918T154812! +# freezing the system time from gpg perspective so that two different +# runs of gpg applied to the same data result in identical signatures. +# compat_hash () { case "$1" in diff --git a/t/t1016/gpg b/t/t1016/gpg index 2601cb18a5b3f4..34d6e055fc9e59 100755 --- a/t/t1016/gpg +++ b/t/t1016/gpg @@ -1,2 +1,2 @@ #!/bin/sh -exec gpg --faked-system-time "20230918T154812" "$@" +exec gpg --faked-system-time '20230918T154812!' "$@" From 8a6d158a1d69996542ec0d28b63e83eaf46c5945 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Wed, 29 Oct 2025 11:32:37 -0400 Subject: [PATCH 16/18] doc: document backslash in gitignore patterns Because gitignore patterns are passed to fnmatch, the handling of backslashes is the same as it is there: it can be used to escape metacharacters. We do reference fnmatch(3) for more details, but it may be friendlier to point out this implication explicitly (especially for people who want to know about backslash handling and search the documentation for that word). There are also two cases that I've seen some other backslash-escaping systems handle differently, so let's describe those: 1. A backslash before any character treats that character literally, even if it's not otherwise a meta-character. As opposed to including the backslash itself (like "foo\bar" in shell expands to "foo\bar") or forbidding it ("foo\zar" is required to produce a diagnostic in C). 2. A backslash at the end of the string is an invalid pattern (and not a literal backslash). This second one in particular was a point of confusion between our implementation and the one in JGit. Our wildmatch behavior matches what POSIX specifies for fnmatch, so the code and documentation are in line. But let's add a test to cover this case. Note that the behavior here differs between wildmatch itself (which is what gitignore will use) and pathspec matching (which will only turn to wildmatch if a literal match fails). So we match "foo\" to "foo\" in pathspecs, but not via gitignore. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- Documentation/gitignore.adoc | 5 +++++ t/t3070-wildmatch.sh | 2 ++ 2 files changed, 7 insertions(+) diff --git a/Documentation/gitignore.adoc b/Documentation/gitignore.adoc index 5e0964ef4191d9..9fccab4ae8df23 100644 --- a/Documentation/gitignore.adoc +++ b/Documentation/gitignore.adoc @@ -111,6 +111,11 @@ PATTERN FORMAT one of the characters in a range. See fnmatch(3) and the FNM_PATHNAME flag for a more detailed description. + - A backslash ("`\`") can be used to escape any character. E.g., "`\*`" + matches a literal asterisk (and "`\a`" matches "`a`", even though + there is no need for escaping there). As with fnmatch(3), a backslash + at the end of a pattern is an invalid pattern that never matches. + Two consecutive asterisks ("`**`") in patterns matched against full pathname may have special meaning: diff --git a/t/t3070-wildmatch.sh b/t/t3070-wildmatch.sh index 3da824117c61fd..655bb1a0f21031 100755 --- a/t/t3070-wildmatch.sh +++ b/t/t3070-wildmatch.sh @@ -235,6 +235,8 @@ match 1 1 1 1 aaaaaaabababab '*ab' match 1 1 1 1 'foo*' 'foo\*' match 0 0 0 0 foobar 'foo\*bar' match 1 1 1 1 'f\oo' 'f\\oo' +match 0 0 0 0 \ + 1 1 1 1 'foo\' 'foo\' match 1 1 1 1 ball '*[al]?' match 0 0 0 0 ten '[ten]' match 1 1 1 1 ten '**[!te]' From 85333aa1af6ebd609bf564a0ecae0b17c6388546 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Wed, 29 Oct 2025 15:10:31 -0400 Subject: [PATCH 17/18] test-tool: fix leak in delete-gpgsig command We read the input into a strbuf, so we must free it. Without this, t1016 complains in SANITIZE=leak mode. The bug was introduced in 7673ecd2dc (t1016-compatObjectFormat: add tests to verify the conversion between objects, 2023-10-01). But nobody seems to have noticed, probably because CI did not run these tests until the fix in 6cd8369ef3 (t/lib-gpg: call prepare_gnupghome() in GPG2 prereq, 2024-07-03). Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- t/helper/test-delete-gpgsig.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/t/helper/test-delete-gpgsig.c b/t/helper/test-delete-gpgsig.c index e36831af03f6a3..658c7a37f7decb 100644 --- a/t/helper/test-delete-gpgsig.c +++ b/t/helper/test-delete-gpgsig.c @@ -23,8 +23,7 @@ int cmd__delete_gpgsig(int argc, const char **argv) if (!strcmp(pattern, "trailer")) { size_t payload_size = parse_signed_buffer(buf.buf, buf.len); fwrite(buf.buf, 1, payload_size, stdout); - fflush(stdout); - return 0; + goto out; } bufptr = buf.buf; @@ -56,7 +55,9 @@ int cmd__delete_gpgsig(int argc, const char **argv) fwrite(bufptr, 1, (eol - bufptr) + 1, stdout); bufptr = eol + 1; } - fflush(stdout); +out: + fflush(stdout); + strbuf_release(&buf); return 0; } From 4cf919bd7b946477798af5414a371b23fd68bf93 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Tue, 4 Nov 2025 07:47:51 -0800 Subject: [PATCH 18/18] A bit more before rc1 Signed-off-by: Junio C Hamano --- Documentation/RelNotes/2.52.0.adoc | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Documentation/RelNotes/2.52.0.adoc b/Documentation/RelNotes/2.52.0.adoc index ba213c0d6c7df3..9759a37871404c 100644 --- a/Documentation/RelNotes/2.52.0.adoc +++ b/Documentation/RelNotes/2.52.0.adoc @@ -74,6 +74,8 @@ UI, Workflows & Features avoids doing maintenance tasks that rebuilds everything from scratch. + * "git repo structure", a new command. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -165,6 +167,18 @@ Performance, Internal Implementation, Development Support etc. * The code to walk revision graph to compute merge base has been optimized. + * AI guidelines has been added to our documentation set. + + * Contributed credential helpers (obviously in contrib/) now have "cd + $there && make install" target. + + * The "MyFirstContribution" tutorial tells the reader how to send out + their patches; the section gained a hint to verify the message + reached the mailing list. + + * The "debug" ref-backend was missing a method implementation, which + has been corrected. + Fixes since v2.51 ----------------- @@ -397,6 +411,15 @@ including security updates, are included in this release. "foo**/bar" match with "foobar", which has been corrected. (merge 1940a02dc1 jk/match-pathname-fix later to maint). + * Tests did not set up GNUPGHOME correctly, which is fixed but some + flaky tests are exposed in t1016, which needs to be addressed + before this topic can move forward. + (merge 6cd8369ef3 tz/test-prepare-gnupghome later to maint). + + * The patterns used in the .gitignore files use backslash in the way + documented for fnmatch(3); document as such to reduce confusion. + (merge 8a6d158a1d jk/doc-backslash-in-exclude later to maint). + * Other code cleanup, docfix, build fix, etc. (merge 529a60a885 ua/t1517-short-help-tests later to maint). (merge 22d421fed9 ac/deglobal-fmt-merge-log-config later to maint). @@ -409,3 +432,5 @@ including security updates, are included in this release. (merge 15b8abde07 js/mingw-includes-cleanup later to maint). (merge 2cebca0582 tb/cat-file-objectmode-update later to maint). (merge 8f487db07a kh/doc-patch-id-1 later to maint). + (merge f711f37b05 eb/t1016-hash-transition-fix later to maint). + (merge 85333aa1af jk/test-delete-gpgsig-leakfix later to maint).