From 665d19ec7bcc2d578e2fa2701f7399a6a965b086 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 10 Dec 2025 13:52:18 +0100 Subject: [PATCH 01/30] midx: fix `BUG()` when getting preferred pack without a reverse index The function `midx_preferred_pack()` returns the preferred pack for a given multi-pack index. To compute the preferred pack we: 1. Take the first position indexed by the MIDX in pseudo-pack order. 2. Convert this pseudo-pack position into the MIDX position. 3. We then look up the pack that corresponds to this MIDX position. This reliably returns the preferred pack given that all of its contained objects will be up front in pseudo-pack order. The second step that turns the pseudo-pack order into MIDX order requires the reverse index though, which may not exist for example when the MIDX does not have a bitmap. And in that case one may easily hit a bug: BUG: ../pack-revindex.c:491: pack_pos_to_midx: reverse index not yet loaded In theory, `midx_preferred_pack()` already knows to handle the case where no reverse index exists, as it calls `load_midx_revindex()` before calling into `midx_preferred_pack()`. But we only check for negative return values there, even though the function returns a positive error code in case the reverse index does not exist. Fix the issue by testing for a non-zero return value instead, same as all the other callers of this function already do. While at it, document the return value of `load_midx_revindex()`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- midx.c | 2 +- pack-revindex.h | 3 ++- t/t5319-multi-pack-index.sh | 13 +++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/midx.c b/midx.c index 1d6269f957e781..79c890cf1b948e 100644 --- a/midx.c +++ b/midx.c @@ -688,7 +688,7 @@ int midx_preferred_pack(struct multi_pack_index *m, uint32_t *pack_int_id) { if (m->preferred_pack_idx == -1) { uint32_t midx_pos; - if (load_midx_revindex(m) < 0) { + if (load_midx_revindex(m)) { m->preferred_pack_idx = -2; return -1; } diff --git a/pack-revindex.h b/pack-revindex.h index 422c2487ae32d8..004289209191d0 100644 --- a/pack-revindex.h +++ b/pack-revindex.h @@ -72,7 +72,8 @@ int verify_pack_revindex(struct packed_git *p); * multi-pack index by mmap-ing it and assigning pointers in the * multi_pack_index to point at it. * - * A negative number is returned on error. + * A negative number is returned on error. A positive number is returned in + * case the multi-pack-index does not have a reverse index. */ int load_midx_revindex(struct multi_pack_index *m); diff --git a/t/t5319-multi-pack-index.sh b/t/t5319-multi-pack-index.sh index 93f319a4b29fbb..9492a9737b5f8e 100755 --- a/t/t5319-multi-pack-index.sh +++ b/t/t5319-multi-pack-index.sh @@ -350,7 +350,20 @@ test_expect_success 'preferred pack from existing MIDX without bitmaps' ' # the new MIDX git multi-pack-index write --preferred-pack=pack-$pack.pack ) +' +test_expect_success 'preferred pack cannot be determined without bitmap' ' + test_when_finished "rm -fr preferred-can-be-queried" && + git init preferred-can-be-queried && + ( + cd preferred-can-be-queried && + test_commit initial && + git repack -Adl --write-midx --no-write-bitmap-index && + test_must_fail test-tool read-midx --preferred-pack .git/objects 2>err && + test_grep "could not determine MIDX preferred pack" err && + git repack -Adl --write-midx --write-bitmap-index && + test-tool read-midx --preferred-pack .git/objects + ) ' test_expect_success 'verify multi-pack-index success' ' From b3bab9d2729fde1f52c407447711c34a75c5c377 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 10 Dec 2025 13:52:19 +0100 Subject: [PATCH 02/30] midx-write: extract function to test whether MIDX needs updating In `write_midx_internal()` we know to skip writing the new multi-pack index in case it would be the same as the existing one. This logic does not handle the `--stdin-packs` option yet though, so we end up always rewriting the MIDX if that option is passed to us. Extract the logic to decide whether or not to rewrite the MIDX into a separate function. This will allow us to extend that feature in the next commit to address the above issue. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- midx-write.c | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/midx-write.c b/midx-write.c index c73010df6d3a4f..c1eed04691f0a7 100644 --- a/midx-write.c +++ b/midx-write.c @@ -1015,6 +1015,41 @@ static void clear_midx_files(struct odb_source *source, strbuf_release(&buf); } +static bool midx_needs_update(struct write_midx_context *ctx) +{ + struct multi_pack_index *midx = ctx->m; + bool needed = true; + + /* + * Ignore incremental updates for now. The assumption is that any + * incremental update would be either empty (in which case we will bail + * out later) or it would actually cover at least one new pack. + */ + if (ctx->incremental) + goto out; + + /* + * If there is no MIDX then either it doesn't exist, or we're doing a + * geometric repack. We cannot (yet) determine whether we need to + * update the multi-pack index in the second case. + */ + if (!midx) + goto out; + + /* + * Otherwise, we need to verify that the packs covered by the existing + * MIDX match the packs that we already have. This test is somewhat + * lenient and will be fixed. + */ + if (ctx->nr != midx->num_packs + midx->num_packs_in_base) + goto out; + + needed = false; + +out: + return needed; +} + static int write_midx_internal(struct odb_source *source, struct string_list *packs_to_include, struct string_list *packs_to_drop, @@ -1112,9 +1147,7 @@ static int write_midx_internal(struct odb_source *source, 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) && - !ctx.incremental && - !(packs_to_include || packs_to_drop)) { + if (!packs_to_include && !packs_to_drop && !midx_needs_update(&ctx)) { struct bitmap_index *bitmap_git; int bitmap_exists; int want_bitmap = flags & MIDX_WRITE_BITMAP; From 6ce9d558ced275a707393d044e5b0035412f8360 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Wed, 10 Dec 2025 13:52:20 +0100 Subject: [PATCH 03/30] midx-write: skip rewriting MIDX with `--stdin-packs` unless needed In `write_midx_internal()` we know to skip rewriting the multi-pack index in case the existing one already covers all packs. This logic does not know to handle `git multi-pack-index write --stdin-packs` though, so we end up always rewriting the MIDX in this case even if the MIDX would not change. With our default maintenance strategy this isn't really much of a problem, as git-gc(1) does not use the "--stdin-packs" option. But that is changing with geometric repacking, where "--stdin-packs" is used to explicitly select the packfiles part of the geometric sequence. This issue can be demonstrated trivially with a benchmark in the Git repository: executing `git repack --geometric=2 --write-midx -d` in the Git repository takes more than 3 seconds only to end up with the same multi-pack index as we already had before. The logic that decides if we need to rewrite the MIDX only checks whether the number of packfiles covered will change. That check is of course too lenient for "--stdin-packs", as it could happen that we want to cover a different-but-same-size set of packfiles. But there is no inherent reason why we cannot handle "--stdin-packs". Improve the logic to not only check for the number of packs, but to also verify that we are asked to generate a MIDX for the _same_ packs. This allows us to also skip no-op rewrites for "--stdin-packs". Helped-by: Taylor Blau Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- midx-write.c | 100 +++++++++++++++++++++++++----------- t/t5319-multi-pack-index.sh | 51 ++++++++++++++++++ t/t7703-repack-geometric.sh | 35 +++++++++++++ 3 files changed, 156 insertions(+), 30 deletions(-) diff --git a/midx-write.c b/midx-write.c index c1eed04691f0a7..40abe3868c4d6f 100644 --- a/midx-write.c +++ b/midx-write.c @@ -1015,9 +1015,10 @@ static void clear_midx_files(struct odb_source *source, strbuf_release(&buf); } -static bool midx_needs_update(struct write_midx_context *ctx) +static bool midx_needs_update(struct multi_pack_index *midx, struct write_midx_context *ctx) { - struct multi_pack_index *midx = ctx->m; + struct strset packs = STRSET_INIT; + struct strbuf buf = STRBUF_INIT; bool needed = true; /* @@ -1028,25 +1029,48 @@ static bool midx_needs_update(struct write_midx_context *ctx) if (ctx->incremental) goto out; - /* - * If there is no MIDX then either it doesn't exist, or we're doing a - * geometric repack. We cannot (yet) determine whether we need to - * update the multi-pack index in the second case. - */ - if (!midx) - goto out; - /* * Otherwise, we need to verify that the packs covered by the existing - * MIDX match the packs that we already have. This test is somewhat - * lenient and will be fixed. + * MIDX match the packs that we already have. The logic to do so is way + * more complicated than it has any right to be. This is because: + * + * - We cannot assume any ordering. + * + * - The MIDX packs may not be loaded at all, and loading them would + * be wasteful. So we need to use the pack names tracked by the + * MIDX itself. + * + * - The MIDX pack names are tracking the ".idx" files, whereas the + * packs themselves are tracking the ".pack" files. So we need to + * strip suffixes. */ if (ctx->nr != midx->num_packs + midx->num_packs_in_base) goto out; + for (uint32_t i = 0; i < ctx->nr; i++) { + strbuf_reset(&buf); + strbuf_addstr(&buf, pack_basename(ctx->info[i].p)); + strbuf_strip_suffix(&buf, ".pack"); + + if (!strset_add(&packs, buf.buf)) + BUG("same pack added twice?"); + } + + for (uint32_t i = 0; i < ctx->nr; i++) { + strbuf_reset(&buf); + strbuf_addstr(&buf, midx->pack_names[i]); + strbuf_strip_suffix(&buf, ".idx"); + + if (!strset_contains(&packs, buf.buf)) + goto out; + strset_remove(&packs, buf.buf); + } + needed = false; out: + strbuf_release(&buf); + strset_clear(&packs); return needed; } @@ -1067,6 +1091,7 @@ static int write_midx_internal(struct odb_source *source, struct write_midx_context ctx = { .preferred_pack_idx = NO_PREFERRED_PACK, }; + struct multi_pack_index *midx_to_free = NULL; int bitmapped_packs_concat_len = 0; int pack_name_concat_len = 0; int dropped_packs = 0; @@ -1147,25 +1172,39 @@ static int write_midx_internal(struct odb_source *source, for_each_file_in_pack_dir(source->path, add_pack_to_midx, &ctx); stop_progress(&ctx.progress); - if (!packs_to_include && !packs_to_drop && !midx_needs_update(&ctx)) { - struct bitmap_index *bitmap_git; - int bitmap_exists; - int want_bitmap = flags & MIDX_WRITE_BITMAP; - - bitmap_git = prepare_midx_bitmap_git(ctx.m); - bitmap_exists = bitmap_git && bitmap_is_midx(bitmap_git); - free_bitmap_index(bitmap_git); - - if (bitmap_exists || !want_bitmap) { - /* - * The correct MIDX already exists, and so does a - * corresponding bitmap (or one wasn't requested). - */ - if (!want_bitmap) - clear_midx_files_ext(source, "bitmap", NULL); - result = 0; - goto cleanup; + if (!packs_to_drop) { + /* + * If there is no MIDX then either it doesn't exist, or we're + * doing a geometric repack. Try to load it from the source to + * tell these two cases apart. + */ + struct multi_pack_index *midx = ctx.m; + if (!midx) + midx = midx_to_free = load_multi_pack_index(ctx.source); + + if (midx && !midx_needs_update(midx, &ctx)) { + struct bitmap_index *bitmap_git; + int bitmap_exists; + int want_bitmap = flags & MIDX_WRITE_BITMAP; + + bitmap_git = prepare_midx_bitmap_git(midx); + bitmap_exists = bitmap_git && bitmap_is_midx(bitmap_git); + free_bitmap_index(bitmap_git); + + if (bitmap_exists || !want_bitmap) { + /* + * The correct MIDX already exists, and so does a + * corresponding bitmap (or one wasn't requested). + */ + if (!want_bitmap) + clear_midx_files_ext(source, "bitmap", NULL); + result = 0; + goto cleanup; + } } + + close_midx(midx_to_free); + midx_to_free = NULL; } if (ctx.incremental && !ctx.nr) { @@ -1521,6 +1560,7 @@ static int write_midx_internal(struct odb_source *source, free(keep_hashes); } strbuf_release(&midx_name); + close_midx(midx_to_free); trace2_region_leave("midx", "write_midx_internal", r); diff --git a/t/t5319-multi-pack-index.sh b/t/t5319-multi-pack-index.sh index 9492a9737b5f8e..794f8b5ab4e136 100755 --- a/t/t5319-multi-pack-index.sh +++ b/t/t5319-multi-pack-index.sh @@ -366,6 +366,57 @@ test_expect_success 'preferred pack cannot be determined without bitmap' ' ) ' +test_midx_is_retained () { + test-tool chmtime =0 .git/objects/pack/multi-pack-index && + ls -l .git/objects/pack/multi-pack-index >expect && + git multi-pack-index write "$@" && + ls -l .git/objects/pack/multi-pack-index >actual && + test_cmp expect actual +} + +test_midx_is_rewritten () { + test-tool chmtime =0 .git/objects/pack/multi-pack-index && + ls -l .git/objects/pack/multi-pack-index >expect && + git multi-pack-index write "$@" && + ls -l .git/objects/pack/multi-pack-index >actual && + ! test_cmp expect actual +} + +test_expect_success 'up-to-date multi-pack-index is retained' ' + test_when_finished "rm -fr midx-up-to-date" && + git init midx-up-to-date && + ( + cd midx-up-to-date && + + # Write the initial pack that contains the most objects. + test_commit first && + test_commit second && + git repack -Ad --write-midx && + test_midx_is_retained && + + # Writing a new bitmap index should cause us to regenerate the MIDX. + test_midx_is_rewritten --bitmap && + test_midx_is_retained --bitmap && + + # Ensure that writing a new packfile causes us to rewrite the index. + test_commit incremental && + git repack -d && + test_midx_is_rewritten && + test_midx_is_retained && + + for pack in .git/objects/pack/*.idx + do + basename "$pack" || exit 1 + done >stdin && + test_line_count = 2 stdin && + test_midx_is_retained --stdin-packs stdin.trimmed && + test_midx_is_rewritten --stdin-packs expect && + git repack --geometric=2 --write-midx --no-write-bitmap-index && + ls -l .git/objects/pack/ >actual && + test_cmp expect actual + ) +' + +test_expect_success '--geometric --write-midx retains up-to-date MIDX with bitmap index' ' + test_when_finished "rm -fr repo" && + git init repo && + test_commit -C repo initial && + + test_path_is_missing repo/.git/objects/pack/multi-pack-index && + git -C repo repack --geometric=2 --write-midx --write-bitmap-index && + test_path_is_file repo/.git/objects/pack/multi-pack-index && + test-tool chmtime =0 repo/.git/objects/pack/multi-pack-index && + + ls -l repo/.git/objects/pack/ >expect && + git -C repo repack --geometric=2 --write-midx --write-bitmap-index && + ls -l repo/.git/objects/pack/ >actual && + test_cmp expect actual +' + test_expect_success '--geometric --write-midx with packfiles in main and alternate ODB' ' test_when_finished "rm -fr shared member" && From 6d8dc99478adeefc1a74f3b4db9336decadddc48 Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Mon, 15 Dec 2025 14:05:12 -0600 Subject: [PATCH 04/30] docs: clarify git-rev-list(1) --filter behavior When using the --filter option for git-rev-list(1), objects that are explicitly provided ignore filters and are always printed unless the --filter-provided-objects option is also specified. Clarify this behavior in the documentation. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- Documentation/rev-list-options.adoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc index d9665d82c8dfbe..453ec590571ffc 100644 --- a/Documentation/rev-list-options.adoc +++ b/Documentation/rev-list-options.adoc @@ -983,7 +983,9 @@ to name units in KiB, MiB, or GiB. For example, `blob:limit=1k` is the same as 'blob:limit=1024'. + The form `--filter=object:type=(tag|commit|tree|blob)` omits all objects -which are not of the requested type. +which are not of the requested type. Note that explicitly provided objects +ignore filters and are always printed unless `--filter-provided-objects` is +also specified. + The form `--filter=sparse:oid=` uses a sparse-checkout specification contained in the blob (or blob-expression) __ From 4ec7ac101b737cd2add8369d0e04eaec1a9f0735 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 17 Dec 2025 14:18:37 +0000 Subject: [PATCH 05/30] t9700: accommodate for Windows paths Ever since fe53bbc9beb (Git.pm: Always set Repository to absolute path if autodetecting, 2009-05-07), the t9700 test _must_ fail on Windows because of that age-old Unix paths vs Windows paths problem. The underlying root cause is that Git cannot run with a regular Win32 variant of Perl, the assumption that every path is a Unix path is just too strong in Git's Perl code. As a consequence, Git for Windows is basically stuck with using the MSYS2 variant of Perl which uses a POSIX emulation layer (which is a friendly fork of Cygwin) _and_ a best-effort Unix <-> Windows paths conversion whenever crossing the boundary between MSYS2 and regular Win32 processes. It is best effort only, though, using heuristics to automagically convert correctly in most cases, but not in all cases. In the context of this here patch, this means that asking `git.exe` for the absolute path of the `.git/` directory will return a Win32 path because `git.exe` is a regular Win32 executable that has no idea about Unix-ish paths. But above-mentioned commit introduced a test that wants to verify that this path is identical to the one that the Git Perl module reports (which refuses to use Win32 paths and uses Unix-ish paths instead). Obviously, this must fail because no heuristics can kick in at that layer. This test failure has not even been caught when Git introduced Windows support in its CI definition in 2e90484eb4a (ci: add a Windows job to the Azure Pipelines definition, 2019-01-29), as all tests relying on Perl had to be disabled even from the start (because the CI runs would otherwise have resulted in prohibitively long runtimes, not because Windows is super slow per se, but because Git's test suite keeps insisting on using technology that requires a POSIX emulation layer, which _is_ super slow on Windows). To work around this failure, let's use the `cygpath` utility to convert the absolute `gitdir` path into the form that the Perl code expects. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t9700/test.pl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/t/t9700/test.pl b/t/t9700/test.pl index 58a9b328d558f3..570b0c5680fc73 100755 --- a/t/t9700/test.pl +++ b/t/t9700/test.pl @@ -117,7 +117,12 @@ sub adjust_dirsep { unlink $tmpfile; # paths -is($r->repo_path, $abs_repo_dir . "/.git", "repo_path"); +my $abs_git_dir = $abs_repo_dir . "/.git"; +if ($^O eq 'msys' or $^O eq 'cygwin') { + $abs_git_dir = `cygpath -am "$abs_repo_dir/.git"`; + $abs_git_dir =~ s/\r?\n?$//; +} +is($r->repo_path, $abs_git_dir, "repo_path"); is($r->wc_path, $abs_repo_dir . "/", "wc_path"); is($r->wc_subdir, "", "wc_subdir initial"); $r->wc_chdir("directory1"); @@ -127,7 +132,7 @@ sub adjust_dirsep { # Object generation in sub directory chdir("directory2"); my $r2 = Git->repository(); -is($r2->repo_path, $abs_repo_dir . "/.git", "repo_path (2)"); +is($r2->repo_path, $abs_git_dir, "repo_path (2)"); is($r2->wc_path, $abs_repo_dir . "/", "wc_path (2)"); is($r2->wc_subdir, "directory2/", "wc_subdir initial (2)"); From b90a926371bbb45b2abd27241a8ef682f1450b99 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 17 Dec 2025 14:18:38 +0000 Subject: [PATCH 06/30] apply: symbolic links lack a "trustable executable bit" When 0482c32c334b (apply: ignore working tree filemode when !core.filemode, 2023-12-26) fixed `git apply` to stop warning about executable files, it inadvertently changed the code flow also for symbolic links and directories. Let's narrow the scope of the special `!trust_executable_git` code path to apply only to regular files. This is needed to let t4115.5(symlink escape when creating new files) pass on Windows when symbolic link support is enabled in the MSYS2 runtime. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- apply.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apply.c b/apply.c index a2ceb3fb40d3b5..de5750354ad2f8 100644 --- a/apply.c +++ b/apply.c @@ -3779,7 +3779,7 @@ static int check_preimage(struct apply_state *state, if (*ce && !(*ce)->ce_mode) BUG("ce_mode == 0 for path '%s'", old_name); - if (trust_executable_bit) + if (trust_executable_bit || !S_ISREG(st->st_mode)) st_mode = ce_mode_from_stat(*ce, st->st_mode); else if (*ce) st_mode = (*ce)->ce_mode; From 6fa50cc4a1979fb8a2f77a026e307d6336a09172 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 17 Dec 2025 14:18:39 +0000 Subject: [PATCH 07/30] mingw: special-case `open(symlink, O_CREAT | O_EXCL)` The `_wopen()` function would gladly follow a symbolic link to a non-existent file and create it when given above-mentioned flags. Git expects the `open()` call to fail, though. So let's add yet another work-around to pretend that Windows behaves according to POSIX, see: https://pubs.opengroup.org/onlinepubs/007904875/functions/open.html#:~:text=If%20O_CREAT%20and%20O_EXCL%20are,set%2C%20the%20result%20is%20undefined. This is required to let t4115.8(--reject removes .rej symlink if it exists) pass on Windows when enabling the MSYS2 runtime's symbolic link support. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- compat/mingw.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/compat/mingw.c b/compat/mingw.c index 90ba5cea9d3ace..ba1b7b6dd1e6a7 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -629,6 +629,7 @@ int mingw_open (const char *filename, int oflags, ...) int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL); wchar_t wfilename[MAX_PATH]; open_fn_t open_fn; + WIN32_FILE_ATTRIBUTE_DATA fdata; DECLARE_PROC_ADDR(ntdll.dll, NTSTATUS, NTAPI, RtlGetLastNtStatus, void); @@ -653,6 +654,19 @@ int mingw_open (const char *filename, int oflags, ...) else if (xutftowcs_path(wfilename, filename) < 0) return -1; + /* + * When `symlink` exists and is a symbolic link pointing to a + * non-existing file, `_wopen(symlink, O_CREAT | O_EXCL)` would + * create that file. Not what we want: Linux would say `EEXIST` + * in that instance, which is therefore what Git expects. + */ + if (create && + GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata) && + (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) { + errno = EEXIST; + return -1; + } + fd = open_fn(wfilename, oflags, mode); /* From 5e8e7e47e0029335bb8b51333d56077d72b862a9 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 17 Dec 2025 14:18:40 +0000 Subject: [PATCH 08/30] t0001: handle `diff --no-index` gracefully The test case 're-init to move gitdir symlink' wants to compare the contents of `newdir/.git`, which is a symbolic link pointing to a file. However, `git diff --no-index`, which is used by `test_cmp` on Windows, does not resolve symlinks; It shows the symlink _target_ instead (with a file mode of 120000). That is totally unexpected by the test case, which as a consequence fails, meaning that it's a bug in the test case itself. Co-authored-by: Junio C Hamano Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t0001-init.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/t/t0001-init.sh b/t/t0001-init.sh index 618da080dc9ea9..e4d32bb4d259f6 100755 --- a/t/t0001-init.sh +++ b/t/t0001-init.sh @@ -425,7 +425,11 @@ test_expect_success SYMLINKS 're-init to move gitdir symlink' ' git init --separate-git-dir ../realgitdir ) && echo "gitdir: $(pwd)/realgitdir" >expected && - test_cmp expected newdir/.git && + case "$GIT_TEST_CMP" in + # `git diff --no-index` does not resolve symlinks + *--no-index*) cmp expected newdir/.git;; + *) test_cmp expected newdir/.git;; + esac && test_cmp expected newdir/here && test_path_is_dir realgitdir/refs ' From 492cc31b57b2f06626c302f3470471bfe355de9b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 17 Dec 2025 14:18:41 +0000 Subject: [PATCH 09/30] t0301: another fix for Windows compatibility Just like 0fdcfa2f9f5 (t0301: fixes for windows compatibility, 2021-09-14) explained, we should not call `mkdir -m` in the test suite because that would fail on Windows. There was one forgotten instance of this which was hidden by a `SYMLINK` prerequisite. Currently, this prevents this test case from being executed on Windows, but with the upcoming support for symbolic links, it would become a problem. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t0301-credential-cache.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/t/t0301-credential-cache.sh b/t/t0301-credential-cache.sh index dc30289f7539ee..6f7cfd9e33f633 100755 --- a/t/t0301-credential-cache.sh +++ b/t/t0301-credential-cache.sh @@ -123,7 +123,8 @@ test_expect_success SYMLINKS 'use user socket if user directory is a symlink to rmdir \"\$HOME/dir/\" && rm \"\$HOME/.git-credential-cache\" " && - mkdir -p -m 700 "$HOME/dir/" && + mkdir -p "$HOME/dir/" && + chmod 700 "$HOME/dir/" && ln -s "$HOME/dir" "$HOME/.git-credential-cache" && check approve cache <<-\EOF && protocol=https From bd6457cfa3f5216700da0ef6ee2ea6614c533a30 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 17 Dec 2025 14:18:42 +0000 Subject: [PATCH 10/30] t0600: fix incomplete prerequisite for a test case The 'symref transaction supports symlinks' test case is guarded by the `SYMLINK` prerequisite because `core.prefersymlinkrefs = true` requires symbolic links to be supported. However, the `preferSymlinkRefs` feature is not supported on Windows, therefore this test case needs the `MINGW` prerequisite, too. There's a couple more cases where we set this config key: - In a subsequent test in t0600, but there we explicitly set it to "false". So this would naturally be supported by Windows. - In t7201 we set the value to `yes`, but we never verify that the written reference is a symbolic link in the first place. I guess that we could rather remove setting the configuration value here, as we are about to deprecate support for symrefs via symbolic links in the first place. But that's certainly outside of the scope of this patch. - In t9903 we do the same, but likewise, we don't check whether the written file is a symbolic link. Therefore this seems to be the only instance where the tests actually need to be adapted. Helped-by: Patrick Steinhardt Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t0600-reffiles-backend.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/t0600-reffiles-backend.sh b/t/t0600-reffiles-backend.sh index b11126ed478129..74bfa2e9ba060d 100755 --- a/t/t0600-reffiles-backend.sh +++ b/t/t0600-reffiles-backend.sh @@ -467,7 +467,7 @@ test_expect_success POSIXPERM 'git reflog expire honors core.sharedRepository' ' esac ' -test_expect_success SYMLINKS 'symref transaction supports symlinks' ' +test_expect_success SYMLINKS,!MINGW 'symref transaction supports symlinks' ' test_when_finished "git symbolic-ref -d TEST_SYMREF_HEAD" && git update-ref refs/heads/new @ && test_config core.prefersymlinkrefs true && From dd479069232d5afcceb1134c501e24cf11ddd9ed Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 17 Dec 2025 14:18:43 +0000 Subject: [PATCH 11/30] t1006: accommodate for symlink support in MSYS2 The MSYS2 runtime (which inherits this trait from the Cygwin runtime, and which is used by Git for Windows' Bash to emulate POSIX functionality on Windows, the same Bash that is also used to run Git's test suite on Windows) has a mode where it can create native symbolic links on Windows. Naturally, this is a bit of a strange feature, given that Cygwin goes out of its way to support Unix-like paths even if no Win32 program understands those, and the symbolic links have to use Win32 paths instead (which Win32 programs understand very well). As a consequence, the symbolic link targets get normalized before the links are created. This results in certain quirks that Git's test suite is ill equipped to accommodate (because Git's test suite expects to be able to use Unix-like paths even on Windows). The test script t1006-cat-file.sh contains two prime examples, two test cases that need to skip a couple assertions because they are simply wrong in the context of Git for Windows. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t1006-cat-file.sh | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/t/t1006-cat-file.sh b/t/t1006-cat-file.sh index 1f61b666a7d382..0eee3bb8781b30 100755 --- a/t/t1006-cat-file.sh +++ b/t/t1006-cat-file.sh @@ -1048,18 +1048,28 @@ test_expect_success 'git cat-file --batch-check --follow-symlinks works for out- echo .. >>expect && echo HEAD:dir/subdir/out-of-repo-link-dir | git cat-file --batch-check --follow-symlinks >actual && test_cmp expect actual && - echo symlink 3 >expect && - echo ../ >>expect && + if test_have_prereq MINGW,SYMLINKS + then + test_write_lines "symlink 2" .. + else + test_write_lines "symlink 3" ../ + fi >expect && echo HEAD:dir/subdir/out-of-repo-link-dir-trailing | git cat-file --batch-check --follow-symlinks >actual && test_cmp expect actual ' test_expect_success 'git cat-file --batch-check --follow-symlinks works for symlinks with internal ..' ' - echo HEAD: | git cat-file --batch-check >expect && - echo HEAD:up-down | git cat-file --batch-check --follow-symlinks >actual && - test_cmp expect actual && - echo HEAD:up-down-trailing | git cat-file --batch-check --follow-symlinks >actual && - test_cmp expect actual && + if test_have_prereq !MINGW + then + # The `up-down` and `up-down-trailing` symlinks are normalized + # in MSYS in `winsymlinks` mode and are therefore in a + # different shape than Git expects them. + echo HEAD: | git cat-file --batch-check >expect && + echo HEAD:up-down | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual && + echo HEAD:up-down-trailing | git cat-file --batch-check --follow-symlinks >actual && + test_cmp expect actual + fi && echo HEAD:up-down-file | git cat-file --batch-check --follow-symlinks >actual && test_cmp found actual && echo symlink 7 >expect && From be6ac3510708da0d662f97783ccaca0794a34593 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 17 Dec 2025 14:18:44 +0000 Subject: [PATCH 12/30] t1305: skip symlink tests that do not apply to Windows In Git for Windows, the gitdir is canonicalized so that even when the gitdir is specified via a symbolic link, the `gitdir:` conditional include will only match the real directory path. Unfortunately, t1305 codifies a different behavior in two test cases, which are hereby skipped on Windows. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t1305-config-include.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t/t1305-config-include.sh b/t/t1305-config-include.sh index 8ff2b0c232b485..6e51f892f320bb 100755 --- a/t/t1305-config-include.sh +++ b/t/t1305-config-include.sh @@ -286,7 +286,7 @@ test_expect_success SYMLINKS 'conditional include, relative path with symlinks' ) ' -test_expect_success SYMLINKS 'conditional include, gitdir matching symlink' ' +test_expect_success SYMLINKS,!MINGW 'conditional include, gitdir matching symlink' ' ln -s foo bar && ( cd bar && @@ -298,7 +298,7 @@ test_expect_success SYMLINKS 'conditional include, gitdir matching symlink' ' ) ' -test_expect_success SYMLINKS 'conditional include, gitdir matching symlink, icase' ' +test_expect_success SYMLINKS,!MINGW 'conditional include, gitdir matching symlink, icase' ' ( cd bar && echo "[includeIf \"gitdir/i:BAR/\"]path=bar8" >>.git/config && From eae7c16c3db2e746dd720c4e9ad7c1724d372b07 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 17 Dec 2025 14:18:45 +0000 Subject: [PATCH 13/30] t6423: introduce Windows-specific handling for symlinking to /dev/null The device `/dev/null` does not exist on Windows, it's called `NUL` there. Calling `ln -s /dev/null my-symlink` in a symlink-enabled MSYS2 Bash will therefore literally link to a file or directory called `null` that is supposed to be in the current drive's top-level `dev` directory. Which typically does not exist. The test, however, really wants the created symbolic link to point to the NUL device. Let's instead use the `mklink` utility on Windows to perform that job, and keep using `ln -s /dev/null ` on non-Windows platforms. While at it, add the missing `SYMLINKS` prereq because this test _still_ would not pass on Windows before support for symbolic links is upstreamed from Git for Windows. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t6423-merge-rename-directories.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/t/t6423-merge-rename-directories.sh b/t/t6423-merge-rename-directories.sh index 533ac85dc83409..53535a8ebfc393 100755 --- a/t/t6423-merge-rename-directories.sh +++ b/t/t6423-merge-rename-directories.sh @@ -5158,13 +5158,18 @@ test_setup_12m () { git switch B && git rm dir/subdir/file && mkdir dir && - ln -s /dev/null dir/subdir && + if test_have_prereq MINGW + then + cmd //c 'mklink dir\subdir NUL' + else + ln -s /dev/null dir/subdir + fi && git add . && git commit -m "B" ) } -test_expect_success '12m: Change parent of renamed-dir to symlink on other side' ' +test_expect_success SYMLINKS '12m: Change parent of renamed-dir to symlink on other side' ' test_setup_12m && ( cd 12m && From ef6dd000ad813fc34a05c4b9055578df13a2eaa6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 17 Dec 2025 14:18:46 +0000 Subject: [PATCH 14/30] t7800: work around the MSYS path conversion on Windows Git's test suite's relies on Unix shell scripting, which is understandable, of course, given Git's firm roots (and indeed, ongoing focus) on Linux. This fact, combined with Unix shell scripting's natural habitat -- which is, naturally... *drumroll*... Unix -- often has unintended side effects, where developers expect the test suite to run in a Unix environment, which is an incorrect assumption. One instance of this problem can be observed in the 'difftool --dir-diff handles modified symlinks' test case in `t7800-difftool.sh`, which assumes that all absolute paths start with a forward slash. That assumption is incorrect in general, e.g. on Windows, where absolute paths have many shapes and forms, none of which starts with a forward slash. The only saving grace is that this test case is currently not run on Windows because of the `SYMLINK` prerequisite. However, I am currently working towards upstreaming symbolic link support from Git for Windows to upstream Git, which will put a crack into that saving grace. Let's change that test case so that it does not rely on absolute paths (which are passed to the "external command" `ls` as parameters and are therefore part of its output, and which the test case wants to filter out before verifying that the output is as expected) starting with a forward slash. Let's instead rely on the much more reliable fact that `ls` will output the path in a line that ends in a colon, and simply filter out those lines by matching said colon instead. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t7800-difftool.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/t/t7800-difftool.sh b/t/t7800-difftool.sh index 9b74db55634b16..bf0f67378dbb23 100755 --- a/t/t7800-difftool.sh +++ b/t/t7800-difftool.sh @@ -752,11 +752,11 @@ test_expect_success SYMLINKS 'difftool --dir-diff handles modified symlinks' ' c EOF git difftool --symlinks --dir-diff --extcmd ls >output && - grep -v ^/ output >actual && + grep -v ":\$" output >actual && test_cmp expect actual && git difftool --no-symlinks --dir-diff --extcmd ls >output && - grep -v ^/ output >actual && + grep -v ":\$" output >actual && test_cmp expect actual && # The left side contains symlink "c" that points to "b" @@ -786,11 +786,11 @@ test_expect_success SYMLINKS 'difftool --dir-diff handles modified symlinks' ' EOF git difftool --symlinks --dir-diff --extcmd ls >output && - grep -v ^/ output >actual && + grep -v ":\$" output >actual && test_cmp expect actual && git difftool --no-symlinks --dir-diff --extcmd ls >output && - grep -v ^/ output >actual && + grep -v ":\$" output >actual && test_cmp expect actual ' From 9faaf254ba061e9fc7065f4c940c9dfcc51e6bbe Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Wed, 17 Dec 2025 11:53:58 -0600 Subject: [PATCH 15/30] builtin/repo: group per-type object values into struct The `object_stats` structure stores object counts by type. In a subsequent commit, additional per-type object measurements will also be stored. Group per-type object values into a new struct to allow better reuse. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- builtin/repo.c | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/builtin/repo.c b/builtin/repo.c index 2a653bd3eacf20..a69699857a5e03 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -202,13 +202,17 @@ struct ref_stats { size_t others; }; -struct object_stats { +struct object_values { size_t tags; size_t commits; size_t trees; size_t blobs; }; +struct object_stats { + struct object_values type_counts; +}; + struct repo_structure { struct ref_stats refs; struct object_stats objects; @@ -281,9 +285,9 @@ 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) +static inline size_t get_total_object_values(struct object_values *values) { - return stats->tags + stats->commits + stats->trees + stats->blobs; + return values->tags + values->commits + values->trees + values->blobs; } static void stats_table_setup_structure(struct stats_table *table, @@ -302,14 +306,18 @@ static void stats_table_setup_structure(struct stats_table *table, 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); + object_total = get_total_object_values(&objects->type_counts); 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")); + stats_table_count_addf(table, objects->type_counts.commits, + " * %s", _("Commits")); + stats_table_count_addf(table, objects->type_counts.trees, + " * %s", _("Trees")); + stats_table_count_addf(table, objects->type_counts.blobs, + " * %s", _("Blobs")); + stats_table_count_addf(table, objects->type_counts.tags, + " * %s", _("Tags")); } static void stats_table_print_structure(const struct stats_table *table) @@ -389,13 +397,13 @@ static void structure_keyvalue_print(struct repo_structure *stats, (uintmax_t)stats->refs.others, value_delim); printf("objects.commits.count%c%" PRIuMAX "%c", key_delim, - (uintmax_t)stats->objects.commits, value_delim); + (uintmax_t)stats->objects.type_counts.commits, value_delim); printf("objects.trees.count%c%" PRIuMAX "%c", key_delim, - (uintmax_t)stats->objects.trees, value_delim); + (uintmax_t)stats->objects.type_counts.trees, value_delim); printf("objects.blobs.count%c%" PRIuMAX "%c", key_delim, - (uintmax_t)stats->objects.blobs, value_delim); + (uintmax_t)stats->objects.type_counts.blobs, value_delim); printf("objects.tags.count%c%" PRIuMAX "%c", key_delim, - (uintmax_t)stats->objects.tags, value_delim); + (uintmax_t)stats->objects.type_counts.tags, value_delim); fflush(stdout); } @@ -473,22 +481,22 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids, switch (type) { case OBJ_TAG: - stats->tags += oids->nr; + stats->type_counts.tags += oids->nr; break; case OBJ_COMMIT: - stats->commits += oids->nr; + stats->type_counts.commits += oids->nr; break; case OBJ_TREE: - stats->trees += oids->nr; + stats->type_counts.trees += oids->nr; break; case OBJ_BLOB: - stats->blobs += oids->nr; + stats->type_counts.blobs += oids->nr; break; default: BUG("invalid object type"); } - object_count = get_total_object_count(stats); + object_count = get_total_object_values(&stats->type_counts); display_progress(data->progress, object_count); return 0; From ce849b1851102d974653701564573798034492d5 Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Wed, 17 Dec 2025 11:53:59 -0600 Subject: [PATCH 16/30] strbuf: split out logic to humanise byte values In a subsequent commit, byte size values displayed in table output for the git-repo(1) "structure" subcommand will be shown in a more human-readable format with the appropriate unit prefixes. For this usecase, the downscaled values and unit strings must be handled separately to ensure proper column alignment. Split out logic from strbuf_humanise() to downscale byte values and determine the corresponding unit prefix into a separate humanise_bytes() function that provides seperate value and unit strings. Note that the "byte" string in "t/helper/test-simple-ipc.c" is unmarked for translation here so that it doesn't conflict with the newly defined plural "byte/bytes" translation and instead uses it. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- strbuf.c | 74 ++++++++++++++++++++------------------ strbuf.h | 14 ++++++++ t/helper/test-simple-ipc.c | 7 +++- 3 files changed, 60 insertions(+), 35 deletions(-) diff --git a/strbuf.c b/strbuf.c index 6c3851a7f84d72..349ee9727a1920 100644 --- a/strbuf.c +++ b/strbuf.c @@ -836,47 +836,53 @@ void strbuf_addstr_urlencode(struct strbuf *sb, const char *s, strbuf_add_urlencode(sb, s, strlen(s), allow_unencoded_fn); } -static void strbuf_humanise(struct strbuf *buf, off_t bytes, - int humanise_rate) +void humanise_bytes(off_t bytes, char **value, const char **unit, + unsigned flags) { + int humanise_rate = flags & HUMANISE_RATE; + if (bytes > 1 << 30) { - strbuf_addf(buf, - humanise_rate == 0 ? - /* TRANSLATORS: IEC 80000-13:2008 gibibyte */ - _("%u.%2.2u GiB") : - /* TRANSLATORS: IEC 80000-13:2008 gibibyte/second */ - _("%u.%2.2u GiB/s"), - (unsigned)(bytes >> 30), - (unsigned)(bytes & ((1 << 30) - 1)) / 10737419); + *value = xstrfmt(_("%u.%2.2u"), (unsigned)(bytes >> 30), + (unsigned)(bytes & ((1 << 30) - 1)) / 10737419); + /* TRANSLATORS: IEC 80000-13:2008 gibibyte/second and gibibyte */ + *unit = humanise_rate ? _("GiB/s") : _("GiB"); } else if (bytes > 1 << 20) { - unsigned x = bytes + 5243; /* for rounding */ - strbuf_addf(buf, - humanise_rate == 0 ? - /* TRANSLATORS: IEC 80000-13:2008 mebibyte */ - _("%u.%2.2u MiB") : - /* TRANSLATORS: IEC 80000-13:2008 mebibyte/second */ - _("%u.%2.2u MiB/s"), - x >> 20, ((x & ((1 << 20) - 1)) * 100) >> 20); + unsigned x = bytes + 5243; /* for rounding */ + *value = xstrfmt(_("%u.%2.2u"), x >> 20, + ((x & ((1 << 20) - 1)) * 100) >> 20); + /* TRANSLATORS: IEC 80000-13:2008 mebibyte/second and mebibyte */ + *unit = humanise_rate ? _("MiB/s") : _("MiB"); } else if (bytes > 1 << 10) { - unsigned x = bytes + 5; /* for rounding */ - strbuf_addf(buf, - humanise_rate == 0 ? - /* TRANSLATORS: IEC 80000-13:2008 kibibyte */ - _("%u.%2.2u KiB") : - /* TRANSLATORS: IEC 80000-13:2008 kibibyte/second */ - _("%u.%2.2u KiB/s"), - x >> 10, ((x & ((1 << 10) - 1)) * 100) >> 10); + unsigned x = bytes + 5; /* for rounding */ + *value = xstrfmt(_("%u.%2.2u"), x >> 10, + ((x & ((1 << 10) - 1)) * 100) >> 10); + /* TRANSLATORS: IEC 80000-13:2008 kibibyte/second and kibibyte */ + *unit = humanise_rate ? _("KiB/s") : _("KiB"); } else { - strbuf_addf(buf, - humanise_rate == 0 ? - /* TRANSLATORS: IEC 80000-13:2008 byte */ - Q_("%u byte", "%u bytes", bytes) : - /* TRANSLATORS: IEC 80000-13:2008 byte/second */ - Q_("%u byte/s", "%u bytes/s", bytes), - (unsigned)bytes); + *value = xstrfmt("%u", (unsigned)bytes); + *unit = humanise_rate ? + /* TRANSLATORS: IEC 80000-13:2008 byte/second */ + Q_("byte/s", "bytes/s", bytes) : + /* TRANSLATORS: IEC 80000-13:2008 byte */ + Q_("byte", "bytes", bytes); } } +static void strbuf_humanise(struct strbuf *buf, off_t bytes, unsigned flags) +{ + char *value; + const char *unit; + + humanise_bytes(bytes, &value, &unit, flags); + + /* + * TRANSLATORS: The first argument is the number string. The second + * argument is the unit string (i.e. "12.34 MiB/s"). + */ + strbuf_addf(buf, _("%s %s"), value, unit); + free(value); +} + void strbuf_humanise_bytes(struct strbuf *buf, off_t bytes) { strbuf_humanise(buf, bytes, 0); @@ -884,7 +890,7 @@ void strbuf_humanise_bytes(struct strbuf *buf, off_t bytes) void strbuf_humanise_rate(struct strbuf *buf, off_t bytes) { - strbuf_humanise(buf, bytes, 1); + strbuf_humanise(buf, bytes, HUMANISE_RATE); } int printf_ln(const char *fmt, ...) diff --git a/strbuf.h b/strbuf.h index a580ac6084b7f1..698b3cc4a51367 100644 --- a/strbuf.h +++ b/strbuf.h @@ -367,6 +367,20 @@ void strbuf_addbuf_percentquote(struct strbuf *dst, const struct strbuf *src); */ void strbuf_add_percentencode(struct strbuf *dst, const char *src, int flags); +enum humanise_flags { + /* + * Use rate based units for humanised values. + */ + HUMANISE_RATE = (1 << 0), +}; + +/** + * Converts the given byte size into a downscaled human-readable value and + * corresponding unit as two separate strings. + */ +void humanise_bytes(off_t bytes, char **value, const char **unit, + unsigned flags); + /** * Append the given byte size as a human-readable string (i.e. 12.23 KiB, * 3.50 MiB). diff --git a/t/helper/test-simple-ipc.c b/t/helper/test-simple-ipc.c index 03cc5eea2c2944..442ad6b16f18d8 100644 --- a/t/helper/test-simple-ipc.c +++ b/t/helper/test-simple-ipc.c @@ -603,7 +603,12 @@ int cmd__simple_ipc(int argc, const char **argv) OPT_INTEGER(0, "bytecount", &cl_args.bytecount, N_("number of bytes")), OPT_INTEGER(0, "batchsize", &cl_args.batchsize, N_("number of requests per thread")), - OPT_STRING(0, "byte", &bytevalue, N_("byte"), N_("ballast character")), + /* + * The "byte" string here is not marked for translation and + * instead relies on translation in strbuf.c:humanise_bytes() to + * avoid conflict with the plural form. + */ + OPT_STRING(0, "byte", &bytevalue, "byte", N_("ballast character")), OPT_STRING(0, "token", &cl_args.token, N_("token"), N_("command token to send to the server")), OPT_END() From 54731320cc3db337f9a3e3920f707e9de3596c60 Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Wed, 17 Dec 2025 11:54:00 -0600 Subject: [PATCH 17/30] builtin/repo: humanise count values in structure output The table output format for the git-repo(1) structure subcommand is used by default and intended to provide output to users in a human-friendly manner. When the reference/object count values in a repository are large, it becomes more cumbersome for users to read the values. For larger values, update the table output format to instead produce more human-friendly count values that are scaled down with the appropriate unit prefix. Output for the keyvalue and nul formats remains unchanged. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- builtin/repo.c | 38 +++++++++++++++++------- strbuf.c | 26 ++++++++++++++++ strbuf.h | 6 ++++ t/t1901-repo-structure.sh | 62 +++++++++++++++++++-------------------- 4 files changed, 91 insertions(+), 41 deletions(-) diff --git a/builtin/repo.c b/builtin/repo.c index a69699857a5e03..9c61bc3e173a94 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -223,6 +223,7 @@ struct stats_table { int name_col_width; int value_col_width; + int unit_col_width; }; /* @@ -230,6 +231,7 @@ struct stats_table { */ struct stats_table_entry { char *value; + const char *unit; }; static void stats_table_vaddf(struct stats_table *table, @@ -250,11 +252,18 @@ static void stats_table_vaddf(struct stats_table *table, if (name_width > table->name_col_width) table->name_col_width = name_width; - if (entry) { + if (!entry) + return; + if (entry->value) { int value_width = utf8_strwidth(entry->value); if (value_width > table->value_col_width) table->value_col_width = value_width; } + if (entry->unit) { + int unit_width = utf8_strwidth(entry->unit); + if (unit_width > table->unit_col_width) + table->unit_col_width = unit_width; + } } static void stats_table_addf(struct stats_table *table, const char *format, ...) @@ -273,7 +282,7 @@ static void stats_table_count_addf(struct stats_table *table, size_t value, va_list ap; CALLOC_ARRAY(entry, 1); - entry->value = xstrfmt("%" PRIuMAX, (uintmax_t)value); + humanise_count(value, &entry->value, &entry->unit); va_start(ap, format); stats_table_vaddf(table, entry, format, ap); @@ -324,20 +333,24 @@ 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); + int title_name_width = utf8_strwidth(name_col_title); + int title_value_width = utf8_strwidth(value_col_title); + int name_col_width = table->name_col_width; + int value_col_width = table->value_col_width; + int unit_col_width = table->unit_col_width; struct string_list_item *item; struct strbuf buf = STRBUF_INIT; - 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; + if (title_name_width > name_col_width) + name_col_width = title_name_width; + if (title_value_width > value_col_width + unit_col_width + 1) + value_col_width = title_value_width - unit_col_width; strbuf_addstr(&buf, "| "); strbuf_utf8_align(&buf, ALIGN_LEFT, name_col_width, name_col_title); strbuf_addstr(&buf, " | "); - strbuf_utf8_align(&buf, ALIGN_LEFT, value_col_width, value_col_title); + strbuf_utf8_align(&buf, ALIGN_LEFT, + value_col_width + unit_col_width + 1, value_col_title); strbuf_addstr(&buf, " |"); printf("%s\n", buf.buf); @@ -345,17 +358,20 @@ static void stats_table_print_structure(const struct stats_table *table) for (int i = 0; i < name_col_width; i++) putchar('-'); printf(" | "); - for (int i = 0; i < value_col_width; i++) + for (int i = 0; i < value_col_width + unit_col_width + 1; i++) putchar('-'); printf(" |\n"); for_each_string_list_item(item, &table->rows) { struct stats_table_entry *entry = item->util; const char *value = ""; + const char *unit = ""; if (entry) { struct stats_table_entry *entry = item->util; value = entry->value; + if (entry->unit) + unit = entry->unit; } strbuf_reset(&buf); @@ -363,6 +379,8 @@ static void stats_table_print_structure(const struct stats_table *table) strbuf_utf8_align(&buf, ALIGN_LEFT, name_col_width, item->string); strbuf_addstr(&buf, " | "); strbuf_utf8_align(&buf, ALIGN_RIGHT, value_col_width, value); + strbuf_addch(&buf, ' '); + strbuf_utf8_align(&buf, ALIGN_LEFT, unit_col_width, unit); strbuf_addstr(&buf, " |"); printf("%s\n", buf.buf); } diff --git a/strbuf.c b/strbuf.c index 349ee9727a1920..995ff15169f59e 100644 --- a/strbuf.c +++ b/strbuf.c @@ -836,6 +836,32 @@ void strbuf_addstr_urlencode(struct strbuf *sb, const char *s, strbuf_add_urlencode(sb, s, strlen(s), allow_unencoded_fn); } +void humanise_count(size_t count, char **value, const char **unit) +{ + if (count >= 1000000000) { + size_t x = count + 5000000; /* for rounding */ + *value = xstrfmt(_("%u.%2.2u"), (unsigned)(x / 1000000000), + (unsigned)(x % 1000000000 / 10000000)); + /* TRANSLATORS: SI decimal prefix symbol for 10^9 */ + *unit = _("G"); + } else if (count >= 1000000) { + size_t x = count + 5000; /* for rounding */ + *value = xstrfmt(_("%u.%2.2u"), (unsigned)(x / 1000000), + (unsigned)(x % 1000000 / 10000)); + /* TRANSLATORS: SI decimal prefix symbol for 10^6 */ + *unit = _("M"); + } else if (count >= 1000) { + size_t x = count + 5; /* for rounding */ + *value = xstrfmt(_("%u.%2.2u"), (unsigned)(x / 1000), + (unsigned)(x % 1000 / 10)); + /* TRANSLATORS: SI decimal prefix symbol for 10^3 */ + *unit = _("k"); + } else { + *value = xstrfmt("%u", (unsigned)count); + *unit = NULL; + } +} + void humanise_bytes(off_t bytes, char **value, const char **unit, unsigned flags) { diff --git a/strbuf.h b/strbuf.h index 698b3cc4a51367..52feef4c1bb0fd 100644 --- a/strbuf.h +++ b/strbuf.h @@ -381,6 +381,12 @@ enum humanise_flags { void humanise_bytes(off_t bytes, char **value, const char **unit, unsigned flags); +/** + * Converts the given count into a downscaled human-readable value and + * corresponding unit as two separate strings. + */ +void humanise_count(size_t count, char **value, const char **unit); + /** * Append the given byte size as a human-readable string (i.e. 12.23 KiB, * 3.50 MiB). diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh index 36a71a144e3f74..55fd13ad1b9c4c 100755 --- a/t/t1901-repo-structure.sh +++ b/t/t1901-repo-structure.sh @@ -10,21 +10,21 @@ test_expect_success 'empty repository' ' ( cd repo && cat >expect <<-\EOF && - | Repository structure | Value | - | -------------------- | ----- | - | * References | | - | * Count | 0 | - | * Branches | 0 | - | * Tags | 0 | - | * Remotes | 0 | - | * Others | 0 | - | | | - | * Reachable objects | | - | * Count | 0 | - | * Commits | 0 | - | * Trees | 0 | - | * Blobs | 0 | - | * Tags | 0 | + | Repository structure | Value | + | -------------------- | ------ | + | * References | | + | * Count | 0 | + | * Branches | 0 | + | * 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 && @@ -39,7 +39,7 @@ test_expect_success 'repository with references and objects' ' git init repo && ( cd repo && - test_commit_bulk 42 && + test_commit_bulk 1005 && git tag -a foo -m bar && oid="$(git rev-parse HEAD)" && @@ -49,21 +49,21 @@ test_expect_success 'repository with references and objects' ' git notes add -m foo && cat >expect <<-\EOF && - | Repository structure | Value | - | -------------------- | ----- | - | * References | | - | * Count | 4 | - | * Branches | 1 | - | * Tags | 1 | - | * Remotes | 1 | - | * Others | 1 | - | | | - | * Reachable objects | | - | * Count | 130 | - | * Commits | 43 | - | * Trees | 43 | - | * Blobs | 43 | - | * Tags | 1 | + | Repository structure | Value | + | -------------------- | ------ | + | * References | | + | * Count | 4 | + | * Branches | 1 | + | * Tags | 1 | + | * Remotes | 1 | + | * Others | 1 | + | | | + | * Reachable objects | | + | * Count | 3.02 k | + | * Commits | 1.01 k | + | * Trees | 1.01 k | + | * Blobs | 1.01 k | + | * Tags | 1 | EOF git repo structure >out 2>err && From 3e114496e48e665d2bb9e0c0917e6051d60392ea Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Wed, 17 Dec 2025 11:54:01 -0600 Subject: [PATCH 18/30] builtin/repo: add inflated object info to keyvalue structure output The structure subcommand for git-repo(1) outputs basic count information for objects and references. Extend this output to also provide information regarding total size of inflated objects by object type. For now, object size by object type info is only added to the keyvalue and nul output formats. In a subsequent commit, this info is also added to the table format. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- Documentation/git-repo.adoc | 1 + builtin/repo.c | 33 +++++++++++++++++++++++++++++++++ t/t1901-repo-structure.sh | 6 +++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc index 70f0a6d2e47291..287eee4b93de5a 100644 --- a/Documentation/git-repo.adoc +++ b/Documentation/git-repo.adoc @@ -50,6 +50,7 @@ supported: + * Reference counts categorized by type * Reachable object counts categorized by type +* Total inflated size of reachable objects by type + The output format can be chosen through the flag `--format`. Three formats are diff --git a/builtin/repo.c b/builtin/repo.c index 9c61bc3e173a94..8da321a3866077 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -2,6 +2,8 @@ #include "builtin.h" #include "environment.h" +#include "hex.h" +#include "odb.h" #include "parse-options.h" #include "path-walk.h" #include "progress.h" @@ -211,6 +213,7 @@ struct object_values { struct object_stats { struct object_values type_counts; + struct object_values inflated_sizes; }; struct repo_structure { @@ -423,6 +426,15 @@ static void structure_keyvalue_print(struct repo_structure *stats, printf("objects.tags.count%c%" PRIuMAX "%c", key_delim, (uintmax_t)stats->objects.type_counts.tags, value_delim); + printf("objects.commits.inflated_size%c%" PRIuMAX "%c", key_delim, + (uintmax_t)stats->objects.inflated_sizes.commits, value_delim); + printf("objects.trees.inflated_size%c%" PRIuMAX "%c", key_delim, + (uintmax_t)stats->objects.inflated_sizes.trees, value_delim); + printf("objects.blobs.inflated_size%c%" PRIuMAX "%c", key_delim, + (uintmax_t)stats->objects.inflated_sizes.blobs, value_delim); + printf("objects.tags.inflated_size%c%" PRIuMAX "%c", key_delim, + (uintmax_t)stats->objects.inflated_sizes.tags, value_delim); + fflush(stdout); } @@ -486,6 +498,7 @@ static void structure_count_references(struct ref_stats *stats, } struct count_objects_data { + struct object_database *odb; struct object_stats *stats; struct progress *progress; }; @@ -495,20 +508,39 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids, { struct count_objects_data *data = cb_data; struct object_stats *stats = data->stats; + size_t inflated_total = 0; size_t object_count; + for (size_t i = 0; i < oids->nr; i++) { + struct object_info oi = OBJECT_INFO_INIT; + unsigned long inflated; + + oi.sizep = &inflated; + + if (odb_read_object_info_extended(data->odb, &oids->oid[i], &oi, + OBJECT_INFO_SKIP_FETCH_OBJECT | + OBJECT_INFO_QUICK) < 0) + continue; + + inflated_total += inflated; + } + switch (type) { case OBJ_TAG: stats->type_counts.tags += oids->nr; + stats->inflated_sizes.tags += inflated_total; break; case OBJ_COMMIT: stats->type_counts.commits += oids->nr; + stats->inflated_sizes.commits += inflated_total; break; case OBJ_TREE: stats->type_counts.trees += oids->nr; + stats->inflated_sizes.trees += inflated_total; break; case OBJ_BLOB: stats->type_counts.blobs += oids->nr; + stats->inflated_sizes.blobs += inflated_total; break; default: BUG("invalid object type"); @@ -526,6 +558,7 @@ static void structure_count_objects(struct object_stats *stats, { struct path_walk_info info = PATH_WALK_INFO_INIT; struct count_objects_data data = { + .odb = repo->objects, .stats = stats, }; diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh index 55fd13ad1b9c4c..33237822fd551e 100755 --- a/t/t1901-repo-structure.sh +++ b/t/t1901-repo-structure.sh @@ -73,7 +73,7 @@ test_expect_success 'repository with references and objects' ' ) ' -test_expect_success 'keyvalue and nul format' ' +test_expect_success SHA1 'keyvalue and nul format' ' test_when_finished "rm -rf repo" && git init repo && ( @@ -90,6 +90,10 @@ test_expect_success 'keyvalue and nul format' ' objects.trees.count=42 objects.blobs.count=42 objects.tags.count=1 + objects.commits.inflated_size=9225 + objects.trees.inflated_size=28554 + objects.blobs.inflated_size=453 + objects.tags.inflated_size=132 EOF git repo structure --format=keyvalue >out 2>err && From 4d279ae36b1d0f68c8a7ba9b986ff9690ddc1af9 Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Wed, 17 Dec 2025 11:54:02 -0600 Subject: [PATCH 19/30] builtin/repo: add inflated object info to structure table Update the table output format for the git-repo(1) structure command to begin printing the total inflated object size info by object type. To be more human-friendly, larger values are scaled down and displayed with the appropriate unit prefix. Output for the keyvalue and nul formats remains unchanged. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- builtin/repo.c | 33 +++++++++++++++++++-- strbuf.c | 14 +++++---- strbuf.h | 5 ++++ t/t1901-repo-structure.sh | 62 +++++++++++++++++++++++---------------- 4 files changed, 80 insertions(+), 34 deletions(-) diff --git a/builtin/repo.c b/builtin/repo.c index 8da321a3866077..67d7548b8864d3 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -292,6 +292,20 @@ static void stats_table_count_addf(struct stats_table *table, size_t value, va_end(ap); } +static void stats_table_size_addf(struct stats_table *table, size_t value, + const char *format, ...) +{ + struct stats_table_entry *entry; + va_list ap; + + CALLOC_ARRAY(entry, 1); + humanise_bytes(value, &entry->value, &entry->unit, HUMANISE_COMPACT); + + 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; @@ -307,7 +321,8 @@ static void stats_table_setup_structure(struct stats_table *table, { struct object_stats *objects = &stats->objects; struct ref_stats *refs = &stats->refs; - size_t object_total; + size_t inflated_object_total; + size_t object_count_total; size_t ref_total; ref_total = get_total_reference_count(refs); @@ -318,10 +333,10 @@ static void stats_table_setup_structure(struct stats_table *table, stats_table_count_addf(table, refs->remotes, " * %s", _("Remotes")); stats_table_count_addf(table, refs->others, " * %s", _("Others")); - object_total = get_total_object_values(&objects->type_counts); + object_count_total = get_total_object_values(&objects->type_counts); 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, object_count_total, " * %s", _("Count")); stats_table_count_addf(table, objects->type_counts.commits, " * %s", _("Commits")); stats_table_count_addf(table, objects->type_counts.trees, @@ -330,6 +345,18 @@ static void stats_table_setup_structure(struct stats_table *table, " * %s", _("Blobs")); stats_table_count_addf(table, objects->type_counts.tags, " * %s", _("Tags")); + + inflated_object_total = get_total_object_values(&objects->inflated_sizes); + stats_table_size_addf(table, inflated_object_total, + " * %s", _("Inflated size")); + stats_table_size_addf(table, objects->inflated_sizes.commits, + " * %s", _("Commits")); + stats_table_size_addf(table, objects->inflated_sizes.trees, + " * %s", _("Trees")); + stats_table_size_addf(table, objects->inflated_sizes.blobs, + " * %s", _("Blobs")); + stats_table_size_addf(table, objects->inflated_sizes.tags, + " * %s", _("Tags")); } static void stats_table_print_structure(const struct stats_table *table) diff --git a/strbuf.c b/strbuf.c index 995ff15169f59e..7fb7d12ac0cb9e 100644 --- a/strbuf.c +++ b/strbuf.c @@ -886,11 +886,15 @@ void humanise_bytes(off_t bytes, char **value, const char **unit, *unit = humanise_rate ? _("KiB/s") : _("KiB"); } else { *value = xstrfmt("%u", (unsigned)bytes); - *unit = humanise_rate ? - /* TRANSLATORS: IEC 80000-13:2008 byte/second */ - Q_("byte/s", "bytes/s", bytes) : - /* TRANSLATORS: IEC 80000-13:2008 byte */ - Q_("byte", "bytes", bytes); + if (flags & HUMANISE_COMPACT) + /* TRANSLATORS: IEC 80000-13:2008 byte/second and byte */ + *unit = humanise_rate ? _("B/s") : _("B"); + else + *unit = humanise_rate ? + /* TRANSLATORS: IEC 80000-13:2008 byte/second */ + Q_("byte/s", "bytes/s", bytes) : + /* TRANSLATORS: IEC 80000-13:2008 byte */ + Q_("byte", "bytes", bytes); } } diff --git a/strbuf.h b/strbuf.h index 52feef4c1bb0fd..06e284f9cca445 100644 --- a/strbuf.h +++ b/strbuf.h @@ -372,6 +372,11 @@ enum humanise_flags { * Use rate based units for humanised values. */ HUMANISE_RATE = (1 << 0), + /* + * Use compact "B" unit symbol instead of "byte/bytes" for humanised + * values. + */ + HUMANISE_COMPACT = (1 << 1), }; /** diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh index 33237822fd551e..b18213c660ea26 100755 --- a/t/t1901-repo-structure.sh +++ b/t/t1901-repo-structure.sh @@ -13,18 +13,23 @@ test_expect_success 'empty repository' ' | Repository structure | Value | | -------------------- | ------ | | * References | | - | * Count | 0 | - | * Branches | 0 | - | * Tags | 0 | - | * Remotes | 0 | - | * Others | 0 | + | * Count | 0 | + | * Branches | 0 | + | * Tags | 0 | + | * Remotes | 0 | + | * Others | 0 | | | | | * Reachable objects | | - | * Count | 0 | - | * Commits | 0 | - | * Trees | 0 | - | * Blobs | 0 | - | * Tags | 0 | + | * Count | 0 | + | * Commits | 0 | + | * Trees | 0 | + | * Blobs | 0 | + | * Tags | 0 | + | * Inflated size | 0 B | + | * Commits | 0 B | + | * Trees | 0 B | + | * Blobs | 0 B | + | * Tags | 0 B | EOF git repo structure >out 2>err && @@ -34,7 +39,7 @@ test_expect_success 'empty repository' ' ) ' -test_expect_success 'repository with references and objects' ' +test_expect_success SHA1 'repository with references and objects' ' test_when_finished "rm -rf repo" && git init repo && ( @@ -49,21 +54,26 @@ test_expect_success 'repository with references and objects' ' git notes add -m foo && cat >expect <<-\EOF && - | Repository structure | Value | - | -------------------- | ------ | - | * References | | - | * Count | 4 | - | * Branches | 1 | - | * Tags | 1 | - | * Remotes | 1 | - | * Others | 1 | - | | | - | * Reachable objects | | - | * Count | 3.02 k | - | * Commits | 1.01 k | - | * Trees | 1.01 k | - | * Blobs | 1.01 k | - | * Tags | 1 | + | Repository structure | Value | + | -------------------- | ---------- | + | * References | | + | * Count | 4 | + | * Branches | 1 | + | * Tags | 1 | + | * Remotes | 1 | + | * Others | 1 | + | | | + | * Reachable objects | | + | * Count | 3.02 k | + | * Commits | 1.01 k | + | * Trees | 1.01 k | + | * Blobs | 1.01 k | + | * Tags | 1 | + | * Inflated size | 16.03 MiB | + | * Commits | 217.92 KiB | + | * Trees | 15.81 MiB | + | * Blobs | 11.68 KiB | + | * Tags | 132 B | EOF git repo structure >out 2>err && From 67cecc693f511321b9d96eead24fd42e6a5c0cdc Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Wed, 17 Dec 2025 11:54:03 -0600 Subject: [PATCH 20/30] builtin/repo: add disk size info to keyvalue stucture output Similar to a prior commit, extend the keyvalue and nul output formats of the git-repo(1) structure command to additionally provide info regarding total object disk sizes by object type. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- Documentation/git-repo.adoc | 1 + builtin/repo.c | 18 ++++++++++++++++++ t/t1901-repo-structure.sh | 11 ++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc index 287eee4b93de5a..861073f641e0a3 100644 --- a/Documentation/git-repo.adoc +++ b/Documentation/git-repo.adoc @@ -51,6 +51,7 @@ supported: * Reference counts categorized by type * Reachable object counts categorized by type * Total inflated size of reachable objects by type +* Total disk size of reachable objects by type + The output format can be chosen through the flag `--format`. Three formats are diff --git a/builtin/repo.c b/builtin/repo.c index 67d7548b8864d3..7ea051f3aff643 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -214,6 +214,7 @@ struct object_values { struct object_stats { struct object_values type_counts; struct object_values inflated_sizes; + struct object_values disk_sizes; }; struct repo_structure { @@ -462,6 +463,15 @@ static void structure_keyvalue_print(struct repo_structure *stats, printf("objects.tags.inflated_size%c%" PRIuMAX "%c", key_delim, (uintmax_t)stats->objects.inflated_sizes.tags, value_delim); + printf("objects.commits.disk_size%c%" PRIuMAX "%c", key_delim, + (uintmax_t)stats->objects.disk_sizes.commits, value_delim); + printf("objects.trees.disk_size%c%" PRIuMAX "%c", key_delim, + (uintmax_t)stats->objects.disk_sizes.trees, value_delim); + printf("objects.blobs.disk_size%c%" PRIuMAX "%c", key_delim, + (uintmax_t)stats->objects.disk_sizes.blobs, value_delim); + printf("objects.tags.disk_size%c%" PRIuMAX "%c", key_delim, + (uintmax_t)stats->objects.disk_sizes.tags, value_delim); + fflush(stdout); } @@ -536,13 +546,16 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids, struct count_objects_data *data = cb_data; struct object_stats *stats = data->stats; size_t inflated_total = 0; + size_t disk_total = 0; size_t object_count; for (size_t i = 0; i < oids->nr; i++) { struct object_info oi = OBJECT_INFO_INIT; unsigned long inflated; + off_t disk; oi.sizep = &inflated; + oi.disk_sizep = &disk; if (odb_read_object_info_extended(data->odb, &oids->oid[i], &oi, OBJECT_INFO_SKIP_FETCH_OBJECT | @@ -550,24 +563,29 @@ static int count_objects(const char *path UNUSED, struct oid_array *oids, continue; inflated_total += inflated; + disk_total += disk; } switch (type) { case OBJ_TAG: stats->type_counts.tags += oids->nr; stats->inflated_sizes.tags += inflated_total; + stats->disk_sizes.tags += disk_total; break; case OBJ_COMMIT: stats->type_counts.commits += oids->nr; stats->inflated_sizes.commits += inflated_total; + stats->disk_sizes.commits += disk_total; break; case OBJ_TREE: stats->type_counts.trees += oids->nr; stats->inflated_sizes.trees += inflated_total; + stats->disk_sizes.trees += disk_total; break; case OBJ_BLOB: stats->type_counts.blobs += oids->nr; stats->inflated_sizes.blobs += inflated_total; + stats->disk_sizes.blobs += disk_total; break; default: BUG("invalid object type"); diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh index b18213c660ea26..dd17caad05da67 100755 --- a/t/t1901-repo-structure.sh +++ b/t/t1901-repo-structure.sh @@ -4,6 +4,11 @@ test_description='test git repo structure' . ./test-lib.sh +object_type_disk_usage() { + git rev-list --all --objects --disk-usage --filter=object:type=$1 \ + --filter-provided-objects +} + test_expect_success 'empty repository' ' test_when_finished "rm -rf repo" && git init repo && @@ -91,7 +96,7 @@ test_expect_success SHA1 'keyvalue and nul format' ' test_commit_bulk 42 && git tag -a foo -m bar && - cat >expect <<-\EOF && + cat >expect <<-EOF && references.branches.count=1 references.tags.count=1 references.remotes.count=0 @@ -104,6 +109,10 @@ test_expect_success SHA1 'keyvalue and nul format' ' objects.trees.inflated_size=28554 objects.blobs.inflated_size=453 objects.tags.inflated_size=132 + objects.commits.disk_size=$(object_type_disk_usage commit) + objects.trees.disk_size=$(object_type_disk_usage tree) + objects.blobs.disk_size=$(object_type_disk_usage blob) + objects.tags.disk_size=$(object_type_disk_usage tag) EOF git repo structure --format=keyvalue >out 2>err && From df1b071fedfddc322fa2a5e0f71d23cb05949d6f Mon Sep 17 00:00:00 2001 From: Justin Tobler Date: Wed, 17 Dec 2025 11:54:04 -0600 Subject: [PATCH 21/30] builtin/repo: add object disk size info to structure table Similar to a prior commit, update the table output format for the git-repo(1) structure command to display the total object disk usage by object type. Signed-off-by: Justin Tobler Signed-off-by: Junio C Hamano --- builtin/repo.c | 13 +++++++++++++ t/t1901-repo-structure.sh | 31 ++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/builtin/repo.c b/builtin/repo.c index 7ea051f3aff643..09bc8fccfd15b5 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -324,6 +324,7 @@ static void stats_table_setup_structure(struct stats_table *table, struct ref_stats *refs = &stats->refs; size_t inflated_object_total; size_t object_count_total; + size_t disk_object_total; size_t ref_total; ref_total = get_total_reference_count(refs); @@ -358,6 +359,18 @@ static void stats_table_setup_structure(struct stats_table *table, " * %s", _("Blobs")); stats_table_size_addf(table, objects->inflated_sizes.tags, " * %s", _("Tags")); + + disk_object_total = get_total_object_values(&objects->disk_sizes); + stats_table_size_addf(table, disk_object_total, + " * %s", _("Disk size")); + stats_table_size_addf(table, objects->disk_sizes.commits, + " * %s", _("Commits")); + stats_table_size_addf(table, objects->disk_sizes.trees, + " * %s", _("Trees")); + stats_table_size_addf(table, objects->disk_sizes.blobs, + " * %s", _("Blobs")); + stats_table_size_addf(table, objects->disk_sizes.tags, + " * %s", _("Tags")); } static void stats_table_print_structure(const struct stats_table *table) diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh index dd17caad05da67..435fd979fa93b9 100755 --- a/t/t1901-repo-structure.sh +++ b/t/t1901-repo-structure.sh @@ -5,8 +5,20 @@ test_description='test git repo structure' . ./test-lib.sh object_type_disk_usage() { - git rev-list --all --objects --disk-usage --filter=object:type=$1 \ - --filter-provided-objects + disk_usage_opt="--disk-usage" + + if test "$2" = "true" + then + disk_usage_opt="--disk-usage=human" + fi + + if test "$1" = "all" + then + git rev-list --all --objects $disk_usage_opt + else + git rev-list --all --objects $disk_usage_opt \ + --filter=object:type=$1 --filter-provided-objects + fi } test_expect_success 'empty repository' ' @@ -35,6 +47,11 @@ test_expect_success 'empty repository' ' | * Trees | 0 B | | * Blobs | 0 B | | * Tags | 0 B | + | * Disk size | 0 B | + | * Commits | 0 B | + | * Trees | 0 B | + | * Blobs | 0 B | + | * Tags | 0 B | EOF git repo structure >out 2>err && @@ -58,7 +75,10 @@ test_expect_success SHA1 'repository with references and objects' ' # Also creates a commit, tree, and blob. git notes add -m foo && - cat >expect <<-\EOF && + # The tags disk size is handled specially due to the + # git-rev-list(1) --disk-usage=human option printing the full + # "byte/bytes" unit string instead of just "B". + cat >expect <<-EOF && | Repository structure | Value | | -------------------- | ---------- | | * References | | @@ -79,6 +99,11 @@ test_expect_success SHA1 'repository with references and objects' ' | * Trees | 15.81 MiB | | * Blobs | 11.68 KiB | | * Tags | 132 B | + | * Disk size | $(object_type_disk_usage all true) | + | * Commits | $(object_type_disk_usage commit true) | + | * Trees | $(object_type_disk_usage tree true) | + | * Blobs | $(object_type_disk_usage blob true) | + | * Tags | $(object_type_disk_usage tag) B | EOF git repo structure >out 2>err && From 1722c2244bc0f5663c53f5dc8fc9ff5b8bf0e523 Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Wed, 17 Dec 2025 19:59:55 +0000 Subject: [PATCH 22/30] docs: note the type of core.attributesfile The previous wording: > Path expansions are made the same way as for `core.excludesFile`. required one to check the docs for 'core.excludesFile' and from there the definition of the pathname variable type to understand the path expansion behaviour of this variable. Instead, just link directly to the pathname type. This change is basically the same rewording as was done to 'core.excludesFile' in dca83abd (config: describe 'pathname' value type, 2016-04-29). Signed-off-by: Matthew Hughes Signed-off-by: Junio C Hamano --- Documentation/config/core.adoc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc index 11efad189e8d72..62b4f5b654dccd 100644 --- a/Documentation/config/core.adoc +++ b/Documentation/config/core.adoc @@ -492,10 +492,9 @@ core.askPass:: command-line argument and write the password on its STDOUT. core.attributesFile:: - In addition to `.gitattributes` (per-directory) and - `.git/info/attributes`, Git looks into this file for attributes - (see linkgit:gitattributes[5]). Path expansions are made the same - way as for `core.excludesFile`. Its default value is + Specifies the pathname to the file that contains attributes (see + linkgit:gitattributes[5]), in addition to `.gitattributes` (per-directory) + and `.git/info/attributes`. Its default value is `$XDG_CONFIG_HOME/git/attributes`. If `$XDG_CONFIG_HOME` is either not set or empty, `$HOME/.config/git/attributes` is used instead. From a650ad996db85b64643970dd7dc5920f989260a0 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 18 Dec 2025 12:35:40 +0900 Subject: [PATCH 23/30] odb: do not use "blank" substitute for NULL When various *object_info() functions are given an extended object info structure as NULL by a caller that does not want any details, the code uses a file-scope static blank_oi and passes it down to the helper functions they use, to avoid handling NULL specifically. The ps/object-read-stream topic graduated to 'master' recently however had a bug that assumed that two identically named file-scope static variables in two functions are the same, which of course is not the case. This made "git commit" take 0.38 seconds to 1508 seconds in some case, as reported by Aaron Plattner here: https://lore.kernel.org/git/f4ba7e89-4717-4b36-921f-56537131fd69@nvidia.com/ We _could_ move the blank_oi variable to the global scope in common section to fix this regression, but explicitly handling the NULL is a much safer fix. It would also reduce the chance of errors that somebody accidentally writes into blank_oi, making its contents dirty, which potentially will make subsequent calls into the function misbehave. By explicitly handling NULL input, we no longer have to worry about it. Reported-by: Aaron Plattner Signed-off-by: Junio C Hamano --- object-file.c | 8 ++++---- odb.c | 29 +++++++++++++---------------- packfile.c | 3 +-- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/object-file.c b/object-file.c index 12177a7dd707a8..e0cce3a62a827a 100644 --- a/object-file.c +++ b/object-file.c @@ -426,7 +426,7 @@ int odb_source_loose_read_object_info(struct odb_source *source, unsigned long size_scratch; enum object_type type_scratch; - if (oi->delta_base_oid) + if (oi && oi->delta_base_oid) oidclr(oi->delta_base_oid, source->odb->repo->hash_algo); /* @@ -437,13 +437,13 @@ int odb_source_loose_read_object_info(struct odb_source *source, * return value implicitly indicates whether the * object even exists. */ - if (!oi->typep && !oi->sizep && !oi->contentp) { + if (!oi || (!oi->typep && !oi->sizep && !oi->contentp)) { struct stat st; - if (!oi->disk_sizep && (flags & OBJECT_INFO_QUICK)) + if ((!oi || !oi->disk_sizep) && (flags & OBJECT_INFO_QUICK)) return quick_has_loose(source->loose, oid) ? 0 : -1; if (stat_loose_object(source->loose, oid, &st, &path) < 0) return -1; - if (oi->disk_sizep) + if (oi && oi->disk_sizep) *oi->disk_sizep = st.st_size; return 0; } diff --git a/odb.c b/odb.c index f4cbee4b042d83..85dc21b104d183 100644 --- a/odb.c +++ b/odb.c @@ -664,34 +664,31 @@ static int do_oid_object_info_extended(struct object_database *odb, const struct object_id *oid, struct object_info *oi, unsigned flags) { - static struct object_info blank_oi = OBJECT_INFO_INIT; const struct cached_object *co; const struct object_id *real = oid; int already_retried = 0; - if (flags & OBJECT_INFO_LOOKUP_REPLACE) real = lookup_replace_object(odb->repo, oid); if (is_null_oid(real)) return -1; - if (!oi) - oi = &blank_oi; - co = find_cached_object(odb, real); if (co) { - if (oi->typep) - *(oi->typep) = co->type; - if (oi->sizep) - *(oi->sizep) = co->size; - if (oi->disk_sizep) - *(oi->disk_sizep) = 0; - if (oi->delta_base_oid) - oidclr(oi->delta_base_oid, odb->repo->hash_algo); - if (oi->contentp) - *oi->contentp = xmemdupz(co->buf, co->size); - oi->whence = OI_CACHED; + if (oi) { + if (oi->typep) + *(oi->typep) = co->type; + if (oi->sizep) + *(oi->sizep) = co->size; + if (oi->disk_sizep) + *(oi->disk_sizep) = 0; + if (oi->delta_base_oid) + oidclr(oi->delta_base_oid, odb->repo->hash_algo); + if (oi->contentp) + *oi->contentp = xmemdupz(co->buf, co->size); + oi->whence = OI_CACHED; + } return 0; } diff --git a/packfile.c b/packfile.c index 7a16aaa90d0a2f..2aa6135c3a1fe4 100644 --- a/packfile.c +++ b/packfile.c @@ -2095,7 +2095,6 @@ int packfile_store_read_object_info(struct packfile_store *store, struct object_info *oi, unsigned flags UNUSED) { - static struct object_info blank_oi = OBJECT_INFO_INIT; struct pack_entry e; int rtype; @@ -2106,7 +2105,7 @@ int packfile_store_read_object_info(struct packfile_store *store, * We know that the caller doesn't actually need the * information below, so return early. */ - if (oi == &blank_oi) + if (!oi) return 0; rtype = packed_object_info(store->odb->repo, e.p, e.offset, oi); From 2c6fc31e04b32d5a8523cfe69e4495f188e86ec3 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 18 Dec 2025 07:13:47 -0500 Subject: [PATCH 24/30] t5551: handle trailing slashes in expected cookies output We check in t5551 that curl updates the expected list of cookies after making a request. We do this by telling it to read and write cookies from a particular text file, and then checking that after curl runs, the file has the expected content. However, in the upcoming curl 8.18.0, the output file has changed slightly: curl will canonicalize the paths it writes, due to commit a093c93994 (cookie: only keep and use the canonical cleaned up path, 2025-12-07). In particular, it strips trailing slashes from the paths we see in the cookies.txt file. This doesn't matter to Git, as the cookie handling is all internal to curl. But our test is overly brittle and breaks as a result. We can fix it by matching either format. We'll expect the new format (without trailing slashes) and strip the slashes from curl's output before comparing. That lets us pass with both old and new versions (I tested against curl's 8_17_0 and rc-8_18_0-2 tags, which are respectively before and after the curl change). In theory it might be nice to try to future-proof this test more by looking only for the bits we care about, rather than a byte-wise comparison of the whole file. But after removing comments and blank lines (which we already do), we care about most of what's there. So it's not clear to me what a more liberal test would look like. Given that the format doesn't change all that often, it's probably OK to stop here and see if it ever breaks again. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- t/t5551-http-fetch-smart.sh | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh index b0d4ea78015a25..73cf5315800fa4 100755 --- a/t/t5551-http-fetch-smart.sh +++ b/t/t5551-http-fetch-smart.sh @@ -333,12 +333,12 @@ test_expect_success 'dumb clone via http-backend respects namespace' ' test_expect_success 'cookies stored in http.cookiefile when http.savecookies set' ' cat >cookies.txt <<-\EOF && - 127.0.0.1 FALSE /smart_cookies/ FALSE 0 othername othervalue + 127.0.0.1 FALSE /smart_cookies FALSE 0 othername othervalue EOF sort >expect_cookies.txt <<-\EOF && - 127.0.0.1 FALSE /smart_cookies/ FALSE 0 othername othervalue - 127.0.0.1 FALSE /smart_cookies/repo.git/ FALSE 0 name value - 127.0.0.1 FALSE /smart_cookies/repo.git/info/ FALSE 0 name value + 127.0.0.1 FALSE /smart_cookies FALSE 0 othername othervalue + 127.0.0.1 FALSE /smart_cookies/repo.git FALSE 0 name value + 127.0.0.1 FALSE /smart_cookies/repo.git/info FALSE 0 name value EOF git config http.cookiefile cookies.txt && git config http.savecookies true && @@ -351,8 +351,11 @@ test_expect_success 'cookies stored in http.cookiefile when http.savecookies set tag -m "foo" cookie-tag && git fetch $HTTPD_URL/smart_cookies/repo.git cookie-tag && - grep "^[^#]" cookies.txt | sort >cookies_stripped.txt && - test_cmp expect_cookies.txt cookies_stripped.txt + # Strip trailing slashes from cookie paths to handle output from both + # old curl ("/smart_cookies/") and new ("/smart_cookies"). + HT=" " && + grep "^[^#]" cookies.txt | sed "s,/$HT,$HT," | sort >cookies_clean.txt && + test_cmp expect_cookies.txt cookies_clean.txt ' test_expect_success 'transfer.hiderefs works over smart-http' ' From 17f4b01da7a4d67d6c22d37904bdbbbddd81b9ac Mon Sep 17 00:00:00 2001 From: Jeff King Date: Thu, 18 Dec 2025 07:18:19 -0500 Subject: [PATCH 25/30] t5563: add missing end-of-line in HTTP header In t5563, we test how various oddly-formatted WWW-Authenticate headers are passed through curl to git's credential subsystem (and ultimately out to credential helpers). One test, "access using basic auth with wwwauth header mixed line-endings" does something odd. It does not mix line endings at all (which must be CRLF according to the RFC anyway), but omits the line ending entirely for the final header! This means that the server produces an incomplete response. We send our final header, and then the newline which is meant to mark the end of headers (and the start of the body) becomes the line ending for that header. And there is no header/body separator in the output at all. Looking at strace, this is what the client reads: recvfrom(9, "WWW-Authenticate: FooBar param1=\"value1\"\r\n \r\n\tparam2=\"value2\"\r\nWWW-Authenticate: Basic realm=\"example.com\"", 16384, 0, NULL, NULL) = 106 recvfrom(9, "\n", 16384, 0, NULL, NULL) = 1 recvfrom(9, "", 16384, 0, NULL, NULL) = 0 The headers themselves are produced from the custom-auth.challenge file we write in the test (which is missing the final CRLF), and then the header/body separator comes from our lib-httpd/nph-custom-auth.sh CGI. (Ignore for a moment that it is producing a bare newline, which I think is a bug; it should be a CRLF but curl is happy with either). Older versions of curl seemed to be OK with the truncated output, but the upcoming 8.18.0 release seems to get confused. Specifically, since 67ae101666 (http: unfold response headers earlier, 2025-12-12) our request to the server fails with insufficient credentials. I traced far enough to see that curl does relay the header back to us, which we then pass to a credential helper, which gives us the correct username/password combination. But on our followup request, curl refuses to send the Authorization header (and so gets an HTTP 401 again). The change in curl's behavior is a bit unexpected, but since we are sending it garbage, it is hard to complain too much. Let's add the missing CRLF to the header. I _think_ this was just an oversight and not the intent of the test. And that the "mixed line-endings" really meant "mixed continuations", since we differ from the previous test in continuing with both space and tab. So I've likewise updated the test title to match that assumption. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- t/t5563-simple-http-auth.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh index 317f33af5a7e60..c1febbae9d778b 100755 --- a/t/t5563-simple-http-auth.sh +++ b/t/t5563-simple-http-auth.sh @@ -469,7 +469,7 @@ test_expect_success 'access using basic auth with wwwauth header empty continuat EOF ' -test_expect_success 'access using basic auth with wwwauth header mixed line-endings' ' +test_expect_success 'access using basic auth with wwwauth header mixed continuations' ' test_when_finished "per_test_cleanup" && set_credential_reply get <<-EOF && @@ -490,7 +490,7 @@ test_expect_success 'access using basic auth with wwwauth header mixed line-endi printf "id=default response=WWW-Authenticate: FooBar param1=\"value1\"\r\n" >>"$CHALLENGE" && printf "id=default response= \r\n" >>"$CHALLENGE" && printf "id=default response=\tparam2=\"value2\"\r\n" >>"$CHALLENGE" && - printf "id=default response=WWW-Authenticate: Basic realm=\"example.com\"" >>"$CHALLENGE" && + printf "id=default response=WWW-Authenticate: Basic realm=\"example.com\"\r\n" >>"$CHALLENGE" && test_config_global credential.helper test-helper && git ls-remote "$HTTPD_URL/custom_auth/repo.git" && From 949df6ed6b02f0d52b3509b68b8c9fe27e56cd97 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 18 Dec 2025 15:20:59 +0000 Subject: [PATCH 26/30] test_detect_ref_format: fix comment When 58aaf59133b (t: introduce GIT_TEST_DEFAULT_REF_FORMAT envvar, 2023-12-29) copy-edited the `test_detect_hash` function, the code comment was accidentally left unchanged. Let's adjust it. Noticed-by: Matthew John Cheetham Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/test-lib-functions.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh index 78e054ab503a65..10bbcea0667db4 100644 --- a/t/test-lib-functions.sh +++ b/t/test-lib-functions.sh @@ -1702,7 +1702,7 @@ test_detect_hash () { esac } -# Detect the hash algorithm in use. +# Detect the ref format in use. test_detect_ref_format () { echo "${GIT_TEST_DEFAULT_REF_FORMAT:-files}" } From 12f0be085701472f634a71ffe1416334c4869267 Mon Sep 17 00:00:00 2001 From: Greg Funni Date: Thu, 18 Dec 2025 15:49:12 +0000 Subject: [PATCH 27/30] repository: remove duplicate free of cache->squash_msg Thankfully, it is set to NULL, so no security consequences. However, this is still a mistake that must be rectified. Signed-off-by: Greg Funni Signed-off-by: Junio C Hamano --- repository.c | 1 - 1 file changed, 1 deletion(-) diff --git a/repository.c b/repository.c index 1a6a62bbd03a5d..faa3fc23932df0 100644 --- a/repository.c +++ b/repository.c @@ -352,7 +352,6 @@ int repo_submodule_init(struct repository *subrepo, static void repo_clear_path_cache(struct repo_path_cache *cache) { - FREE_AND_NULL(cache->squash_msg); FREE_AND_NULL(cache->squash_msg); FREE_AND_NULL(cache->merge_msg); FREE_AND_NULL(cache->merge_rr); From 46d0ee2d6996779bf33acb83e36240443e27c79e Mon Sep 17 00:00:00 2001 From: Greg Funni Date: Thu, 18 Dec 2025 16:10:49 +0000 Subject: [PATCH 28/30] refs: dereference the value of the required pointer Currently, this always prints yes because required is non-null. This is the wrong behavior. The boolean must be dereferenced. Signed-off-by: Greg Funni Signed-off-by: Junio C Hamano --- refs/debug.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/refs/debug.c b/refs/debug.c index 36f8c58b6c781f..f3d1079a2c805c 100644 --- a/refs/debug.c +++ b/refs/debug.c @@ -131,7 +131,7 @@ static int debug_optimize_required(struct ref_store *ref_store, struct debug_ref_store *drefs = (struct debug_ref_store *)ref_store; int res = drefs->refs->be->optimize_required(drefs->refs, opts, required); trace_printf_key(&trace_refs, "optimize_required: %s, res: %d\n", - required ? "yes" : "no", res); + *required ? "yes" : "no", res); return res; } From c469ca26c588918cfad439636a26fbefa2049b1d Mon Sep 17 00:00:00 2001 From: "D. Ben Knoble" Date: Thu, 18 Dec 2025 18:25:44 -0500 Subject: [PATCH 29/30] rust: build correctly without GNU sed From e509b5b8be (rust: support for Windows, 2025-10-15), we check cargo's information to decide which library to build. However, that check mistakenly used "sed -s" ("consider files as separate rather than as a single, continuous long stream"), which is a GNU extension. The build thus fails on macOS with "meson -Drust=enabled", which comes with BSD-derived sed. Instead, use the intended "sed -n" and print the matching section of the output. This failure mode likely went unnoticed on systems with GNU sed (common for developer machines and CI) because, in those instances, the output being matched by case is the full cargo output (which either contains the string "-windows-" or doesn't). Helped-by: Eric Sunshine Helped-by: Patrick Steinhardt Signed-off-by: D. Ben Knoble Signed-off-by: Junio C Hamano --- src/cargo-meson.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cargo-meson.sh b/src/cargo-meson.sh index 3998db04354864..38728a371137f9 100755 --- a/src/cargo-meson.sh +++ b/src/cargo-meson.sh @@ -26,7 +26,7 @@ then exit $RET fi -case "$(cargo -vV | sed -s 's/^host: \(.*\)$/\1/')" in +case "$(cargo -vV | sed -n 's/^host: \(.*\)$/\1/p')" in *-windows-*) LIBNAME=gitcore.lib;; *) From 68cb7f9e92a5d8e9824f5b52ac3d0a9d8f653dbe Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 29 Dec 2025 17:43:28 +0900 Subject: [PATCH 30/30] The 14th batch Signed-off-by: Junio C Hamano --- Documentation/RelNotes/2.53.0.adoc | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Documentation/RelNotes/2.53.0.adoc b/Documentation/RelNotes/2.53.0.adoc index d71948829c9ee2..91cfb7adfaab8f 100644 --- a/Documentation/RelNotes/2.53.0.adoc +++ b/Documentation/RelNotes/2.53.0.adoc @@ -31,6 +31,9 @@ UI, Workflows & Features * "git repo struct" learned to take "-z" as a synonym to "--format=nul". + * More object database related information are shown in "git repo + structure" output. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -83,6 +86,9 @@ Performance, Internal Implementation, Development Support etc. * The code path that enumerates promisor objects have been optimized to skip pointlessly parsing blob objects. + * Prepare test suite for Git for Windows that supports symbolic + links. + Fixes since v2.52 ----------------- @@ -204,6 +210,17 @@ Fixes since v2.52 has been corrected. (merge b7b17ec8a6 kn/fix-fetch-backfill-tag-with-batched-ref-updates later to maint). + * Document "rev-list --filter-provided-objects" better. + (merge 6d8dc99478 jt/doc-rev-list-filter-provided-objects later to maint). + + * Even when there is no changes in the packfile and no need to + recompute bitmaps, "git repack" recomputed and updated the MIDX + file, which has been corrected. + (merge 6ce9d558ce ps/repack-avoid-noop-midx-rewrite later to maint). + + * Update HTTP tests to adjust for changes in curl 8.18.0 + (merge 17f4b01da7 jk/test-curl-updates later to maint). + * Other code cleanup, docfix, build fix, etc. (merge 46207a54cc qj/doc-http-bad-want-response later to maint). (merge df90eccd93 kh/doc-commit-extra-references later to maint). @@ -221,3 +238,7 @@ Fixes since v2.52 (merge 4ce170c522 ds/doc-scalar-config later to maint). (merge a0c813951a jc/doc-commit-signoff-config later to maint). (merge 8ee262985a ja/doc-misc-fixes later to maint). + (merge 1722c2244b mh/doc-core-attributesfile later to maint). + (merge c469ca26c5 dk/ci-rust-fix later to maint). + (merge 12f0be0857 gf/clear-path-cache-cleanup later to maint). + (merge 949df6ed6b js/test-func-comment-fix later to maint).