From a039ee6757c19b7f341e9796564ea7c93faca6b5 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 12 Aug 2026 09:25:49 -0700 Subject: [PATCH 01/16] completion: no-op refactoring of diff completion The "git diff" completion function punts very early when it sees "--" on the command line, since it is a sign that options or revisions can appear and the current completion does not need to do anything "git diff" specific. By returning, it lets Bash default action that completes the names of the files in $PWD to kick in. In preparation for the next step to change what happens when we "punt", arrange the code flow to avoid this early return. The behaviour at this step is unchanged, but the control flow just falls straight to the end. Signed-off-by: Junio C Hamano --- contrib/completion/git-completion.bash | 63 ++++++++++++++------------ 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index e8757877104eb9..a61b6ed59a2a9d 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1947,35 +1947,40 @@ __git_diff_difftool_options="--cached --staged _git_diff () { - __git_has_doubledash && return - - case "$cur" in - --diff-algorithm=*) - __gitcomp "$__git_diff_algorithms" "" "${cur##--diff-algorithm=}" - return - ;; - --submodule=*) - __gitcomp "$__git_diff_submodule_formats" "" "${cur##--submodule=}" - return - ;; - --color-moved=*) - __gitcomp "$__git_color_moved_opts" "" "${cur##--color-moved=}" - return - ;; - --color-moved-ws=*) - __gitcomp "$__git_color_moved_ws_opts" "" "${cur##--color-moved-ws=}" - return - ;; - --ws-error-highlight=*) - __gitcomp "$__git_ws_error_highlight_opts" "" "${cur##--ws-error-highlight=}" - return - ;; - --*) - __gitcomp "$__git_diff_difftool_options" - return - ;; - esac - __git_complete_revlist_file + if ! __git_has_doubledash; then + case "$cur" in + --diff-algorithm=*) + __gitcomp "$__git_diff_algorithms" \ + "" "${cur##--diff-algorithm=}" + return + ;; + --submodule=*) + __gitcomp "$__git_diff_submodule_formats" \ + "" "${cur##--submodule=}" + return + ;; + --color-moved=*) + __gitcomp "$__git_color_moved_opts" \ + "" "${cur##--color-moved=}" + return + ;; + --color-moved-ws=*) + __gitcomp "$__git_color_moved_ws_opts" \ + "" "${cur##--color-moved-ws=}" + return + ;; + --ws-error-highlight=*) + __gitcomp "$__git_ws_error_highlight_opts" \ + "" "${cur##--ws-error-highlight=}" + return + ;; + --*) + __gitcomp "$__git_diff_difftool_options" + return + ;; + esac + __git_complete_revlist_file + fi } __git_mergetools_common="diffuse diffmerge ecmerge emerge kdiff3 meld opendiff From 4b1b7a4e95af51c87e023fd96d9da0d4719e33bd Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 12 Aug 2026 09:25:50 -0700 Subject: [PATCH 02/16] completion: complete tracked paths for 'git diff' When completing arguments for 'git diff', _git_diff() delegates to __git_complete_revlist_file(), which only completes revision references. This is good [*], as mixing both revisions and paths in a single list for the user to pick from is simply too confusing. If no reference matches, or if '--' is given, however, _git_diff() leaves COMPREPLY empty. Bash then falls back to default filename completion in $PWD. This fails when 'git -C ' is used because $PWD is not the target repository. Update _git_diff() to use __git_complete_index_file() when '--' is present, or when revision reference completion yields no matching candidates, so that tracked paths are offered as candidates. This changes behavior even in the case where '-C ' is not used. The new behavior omits untracked paths from suggestions when no revs match the prefix but matching tracked paths exist, which is more useful in the context of 'git diff'. When run outside the working tree of a repository, or when nothing matches from revisions or tracked paths, Bash still falls back to default filename completion in $PWD, so such a use case would be just like completing paths for any 'diff' command, rather than for 'git diff'. [Footnote] * In https://lore.kernel.org/git/al%2Fw2qgBfhe9qMg6@szeder.dev/ SZEDER made the same argument for "git send-email 0". Signed-off-by: Junio C Hamano --- contrib/completion/git-completion.bash | 4 +++ t/t9902-completion.sh | 40 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index a61b6ed59a2a9d..76181e8714aa89 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1981,6 +1981,10 @@ _git_diff () esac __git_complete_revlist_file fi + + if [ ${#COMPREPLY[@]} -eq 0 ]; then + __git_complete_index_file "" + fi } __git_mergetools_common="diffuse diffmerge ecmerge emerge kdiff3 meld opendiff diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh index 55dc9eabfc42fe..32e5d484c714b1 100755 --- a/t/t9902-completion.sh +++ b/t/t9902-completion.sh @@ -2663,6 +2663,7 @@ test_expect_success 'setup for integration tests' ' echo content >file1 && echo more >file2 && git add file1 file2 && + echo untracked >file3 && git commit -m one && git branch mybranch && git tag mytag @@ -2712,6 +2713,45 @@ test_expect_success 'git -C checkout uses the right repo' ' EOF ' +test_expect_success 'git diff completes tracked paths when no refs match' ' + # file1 and file2 are tracked but file3 is not + # there is no ref that begins with f + test_completion "git diff f" <<-\EOF && + file1 + file2 + EOF + test_completion "git diff -- f" <<-\EOF + file1 + file2 + EOF +' + +test_expect_success 'git -C diff completes paths in specified repo' ' + test_when_finished "rm -rf repo-for-diff" && + git init repo-for-diff && + echo content >repo-for-diff/otherfile && + echo content >repo-for-diff/lostfile && + git -C repo-for-diff add otherfile && + git -C repo-for-diff add lostfile && + git -C repo-for-diff commit -m otherfile && + echo untracked >repo-for-diff/oops && + rm -f repo-for-diff/lostfile && + + test_completion "git -C repo-for-diff diff o" <<-\EOF && + otherfile + EOF + test_completion "git -C repo-for-diff diff l" <<-\EOF && + lostfile + EOF + + test_completion "git -C repo-for-diff diff -- o" <<-\EOF && + otherfile + EOF + test_completion "git -C repo-for-diff diff -- l" <<-\EOF + lostfile + EOF +' + test_expect_success 'show completes all refs' ' test_completion "git show m" <<-\EOF main Z From 354d1bf3a094c1ac9ff6c1b4931a0f7e563aef93 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 12 Aug 2026 09:25:51 -0700 Subject: [PATCH 03/16] completion: 'git diff' completes untracked paths as a last resort We taught 'git diff' to first try to complete revisions (unless '--' is present on the command line) and, failing that, to complete tracked paths. If this yields nothing, it lets the Bash default, which offers paths in $PWD, kick in. Teach it to complete untracked paths before giving up and letting the Bash default kick in. With this change, $ git -C another-directory diff un finds the 'untracked' file in another-directory and offers it as a completion candidate. Signed-off-by: Junio C Hamano --- contrib/completion/git-completion.bash | 4 ++++ t/t9902-completion.sh | 20 +++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 76181e8714aa89..d35b4f30243cb0 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1985,6 +1985,10 @@ _git_diff () if [ ${#COMPREPLY[@]} -eq 0 ]; then __git_complete_index_file "" fi + + if [ ${#COMPREPLY[@]} -eq 0 ]; then + __git_complete_index_file "--others --directory" + fi } __git_mergetools_common="diffuse diffmerge ecmerge emerge kdiff3 meld opendiff diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh index 32e5d484c714b1..b889ec8c77d13a 100755 --- a/t/t9902-completion.sh +++ b/t/t9902-completion.sh @@ -2664,6 +2664,7 @@ test_expect_success 'setup for integration tests' ' echo more >file2 && git add file1 file2 && echo untracked >file3 && + echo untracked >ufile && git commit -m one && git branch mybranch && git tag mytag @@ -2726,6 +2727,16 @@ test_expect_success 'git diff completes tracked paths when no refs match' ' EOF ' +test_expect_success 'git diff [--] completes untracked paths, too' ' + # ufile is not tracked and there is no ref that begins with u + test_completion "git diff u" <<-\EOF && + ufile + EOF + test_completion "git diff -- u" <<-\EOF + ufile + EOF +' + test_expect_success 'git -C diff completes paths in specified repo' ' test_when_finished "rm -rf repo-for-diff" && git init repo-for-diff && @@ -2735,6 +2746,7 @@ test_expect_success 'git -C diff completes paths in specified repo' ' git -C repo-for-diff add lostfile && git -C repo-for-diff commit -m otherfile && echo untracked >repo-for-diff/oops && + echo untracked >repo-for-diff/ufile && rm -f repo-for-diff/lostfile && test_completion "git -C repo-for-diff diff o" <<-\EOF && @@ -2743,13 +2755,19 @@ test_expect_success 'git -C diff completes paths in specified repo' ' test_completion "git -C repo-for-diff diff l" <<-\EOF && lostfile EOF + test_completion "git -C repo-for-diff diff u" <<-\EOF && + ufile + EOF test_completion "git -C repo-for-diff diff -- o" <<-\EOF && otherfile EOF - test_completion "git -C repo-for-diff diff -- l" <<-\EOF + test_completion "git -C repo-for-diff diff -- l" <<-\EOF && lostfile EOF + test_completion "git -C repo-for-diff diff -- u" <<-\EOF + ufile + EOF ' test_expect_success 'show completes all refs' ' From e4621a0169bdc2e7177f0609745957abe999dfbf Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 13 Aug 2026 14:56:49 +0000 Subject: [PATCH 04/16] packfile: fix perf regression with many packs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since 589127caa730 (packfile: move list of packs into the packfile store, 2025-10-30), there is a performance regression when many packfiles need to be loaded: `packfile_store_add_pack()` now calls `packfile_list_remove_internal()` to detect whether the packfile was _already_ in the list, and if so, move it to the end of the list. This function linearly scans the existing list before every insertion. Newly loading N packs therefore has complexity O(N²). In one reported use case (https://github.com/microsoft/git/issues/970), N equals 37,815 and caused a slow-down of a simple `git rev-parse --short HEAD` (which is regularly executed as part of `GIT_PS1`) from 0.4s to 4.5s. Let's fix this by establishing a fast path for known-new packfiles. The keen reader will note that there is currently only a single, "known-new" caller of the `packfile_list_append()` function, and wonder why not simply remove this check whether the packfile already exists in the list? Originally, when above-mentioned commit introduced that logic, there was a second caller in `prepare_midx()`, which would have required that check, but that caller was removed in 6aff1f25a046 (packfile: always add packfiles to MRU when adding a pack, 2025-10-30). Still, the function is declared in a header file, and to avoid any problems with in-flight or downstream callers, it is safer to extend the signature to be explicit whether or not to skip that check. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- packfile-list.c | 5 +++-- packfile-list.h | 3 ++- packfile.c | 2 +- t/perf/p5303-many-packs.sh | 4 ++++ 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packfile-list.c b/packfile-list.c index 01fb913abf78fc..d6d411823c34a9 100644 --- a/packfile-list.c +++ b/packfile-list.c @@ -57,11 +57,12 @@ void packfile_list_prepend(struct packfile_list *list, struct packed_git *pack) list->tail = entry; } -void packfile_list_append(struct packfile_list *list, struct packed_git *pack) +void packfile_list_append(struct packfile_list *list, struct packed_git *pack, + int skip_dup_check) { struct packfile_list_entry *entry; - entry = packfile_list_remove_internal(list, pack); + entry = skip_dup_check ? NULL : packfile_list_remove_internal(list, pack); if (!entry) { entry = xmalloc(sizeof(*entry)); entry->pack = pack; diff --git a/packfile-list.h b/packfile-list.h index 1b05e2aa36de08..2b4b98b22673a4 100644 --- a/packfile-list.h +++ b/packfile-list.h @@ -15,7 +15,8 @@ struct packfile_list_entry { void packfile_list_clear(struct packfile_list *list); void packfile_list_remove(struct packfile_list *list, struct packed_git *pack); void packfile_list_prepend(struct packfile_list *list, struct packed_git *pack); -void packfile_list_append(struct packfile_list *list, struct packed_git *pack); +void packfile_list_append(struct packfile_list *list, struct packed_git *pack, + int skip_dup_check); /* * Find the pack within the "packs" list whose index contains the object diff --git a/packfile.c b/packfile.c index 1d1b23b6cc782f..4217ce2174e239 100644 --- a/packfile.c +++ b/packfile.c @@ -781,7 +781,7 @@ void packfile_store_add_pack(struct odb_source_packed *store, if (pack->pack_fd != -1) pack_open_fds++; - packfile_list_append(&store->packs, pack); + packfile_list_append(&store->packs, pack, 1); strmap_put(&store->packs_by_path, pack->pack_name, pack); } diff --git a/t/perf/p5303-many-packs.sh b/t/perf/p5303-many-packs.sh index af173a7b73e398..4221f9dd706243 100755 --- a/t/perf/p5303-many-packs.sh +++ b/t/perf/p5303-many-packs.sh @@ -141,4 +141,8 @@ test_perf "load 10,000 packs" ' git rev-parse --verify "HEAD^{commit}" ' +test_perf "abbreviate with 10,000 packs" ' + git rev-parse --short HEAD +' + test_done From 735514635b49332efba080ed8e2c75b5e3c37a6e Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 13 Aug 2026 12:12:32 -0700 Subject: [PATCH 05/16] completion: no-op refactoring of checkout completion The 'git checkout' completion function punts very early when it sees '--' on the command line, as it indicates that options or revisions can no longer appear. By returning early, it allows the default Bash action (which completes files in '$PWD') to kick in. In preparation for changing what happens in the next step when option or revision completion yields no matching candidates, or when '--' is present, reorganize the control flow to avoid this early return, and add explicit returns to the option completion branches. Signed-off-by: Junio C Hamano --- contrib/completion/git-completion.bash | 84 +++++++++++++------------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index d35b4f30243cb0..38dec1cabe9130 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1735,49 +1735,51 @@ __git_checkout_default_dwim_mode () _git_checkout () { - __git_has_doubledash && return - - local dwim_opt="$(__git_checkout_default_dwim_mode)" - - case "$prev" in - -b|-B|--orphan) - # Complete local branches (and DWIM branch - # remote branch names) for an option argument - # specifying a new branch name. This is for - # convenience, assuming new branches are - # possibly based on pre-existing branch names. - __git_complete_refs $dwim_opt --mode="heads" - return - ;; - *) - ;; - esac + if ! __git_has_doubledash; then + local dwim_opt="$(__git_checkout_default_dwim_mode)" - case "$cur" in - --conflict=*) - __gitcomp "diff3 merge zdiff3" "" "${cur##--conflict=}" - ;; - --*) - __gitcomp_builtin checkout - ;; - *) - # At this point, we've already handled special completion for - # the arguments to -b/-B, and --orphan. There are 3 main - # things left we can possibly complete: - # 1) a start-point for -b/-B, -d/--detach, or --orphan - # 2) a remote head, for --track - # 3) an arbitrary reference, possibly including DWIM names - # + case "$prev" in + -b|-B|--orphan) + # Complete local branches (and DWIM branch + # remote branch names) for an option argument + # specifying a new branch name. This is for + # convenience, assuming new branches are + # possibly based on pre-existing branch names. + __git_complete_refs $dwim_opt --mode="heads" + return + ;; + *) + ;; + esac - if [ -n "$(__git_find_on_cmdline "-b -B -d --detach --orphan")" ]; then - __git_complete_refs --mode="refs" - elif [ -n "$(__git_find_on_cmdline "-t --track")" ]; then - __git_complete_refs --mode="remote-heads" - else - __git_complete_refs $dwim_opt --mode="refs" - fi - ;; - esac + case "$cur" in + --conflict=*) + __gitcomp "diff3 merge zdiff3" "" "${cur##--conflict=}" + return + ;; + --*) + __gitcomp_builtin checkout + return + ;; + *) + # At this point, we've already handled special completion for + # the arguments to -b/-B, and --orphan. There are 3 main + # things left we can possibly complete: + # 1) a start-point for -b/-B, -d/--detach, or --orphan + # 2) a remote head, for --track + # 3) an arbitrary reference, possibly including DWIM names + # + + if [ -n "$(__git_find_on_cmdline "-b -B -d --detach --orphan")" ]; then + __git_complete_refs --mode="refs" + elif [ -n "$(__git_find_on_cmdline "-t --track")" ]; then + __git_complete_refs --mode="remote-heads" + else + __git_complete_refs $dwim_opt --mode="refs" + fi + ;; + esac + fi } __git_sequencer_inprogress_options="--continue --quit --abort --skip" From 3fe92099860aabc8af341334562ff7cdb5e3a9a3 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 13 Aug 2026 12:12:33 -0700 Subject: [PATCH 06/16] completion: complete tracked paths for "git checkout" When completing arguments for "git checkout", _git_checkout() delegates to __git_complete_refs(), which only completes revision references. This is good, as mixing revisions and paths in a single list from which the user can choose is confusing. However, if no reference matches, or if "--" is given, _git_checkout() leaves COMPREPLY empty. Bash then falls back to the default filename completion in $PWD. This fails when "git -C " is used, as $PWD is not the target repository. Update _git_checkout() to use __git_complete_index_file() when "--" is present, or when revision reference completion yields no matching candidates, so that tracked paths are offered as candidates. Signed-off-by: Junio C Hamano --- contrib/completion/git-completion.bash | 4 +++ t/t9902-completion.sh | 39 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 38dec1cabe9130..0eecfcbf8bf662 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1780,6 +1780,10 @@ _git_checkout () ;; esac fi + + if [ ${#COMPREPLY[@]} -eq 0 ]; then + __git_complete_index_file "" + fi } __git_sequencer_inprogress_options="--continue --quit --abort --skip" diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh index b889ec8c77d13a..13fa5c65c32f3a 100755 --- a/t/t9902-completion.sh +++ b/t/t9902-completion.sh @@ -2714,6 +2714,45 @@ test_expect_success 'git -C checkout uses the right repo' ' EOF ' +test_expect_success 'git checkout completes tracked paths when no refs match' ' + # file1 and file2 are tracked but file3 is not + # there is no ref that begins with f + test_completion "git checkout f" <<-\EOF && + file1 + file2 + EOF + test_completion "git checkout -- f" <<-\EOF + file1 + file2 + EOF +' + +test_expect_success 'git -C checkout completes paths in specified repo' ' + # otherfile is tracked, oops is not + # lostfile is tracked but lost + test_when_finished "rm -rf repo-for-checkout" && + git init repo-for-checkout && + echo content >repo-for-checkout/otherfile && + echo content >repo-for-checkout/lostfile && + git -C repo-for-checkout add otherfile && + git -C repo-for-checkout add lostfile && + git -C repo-for-checkout commit -m otherfile && + echo untracked >repo-for-checkout/oops && + rm -f repo-for-checkout/lostfile && + test_completion "git -C repo-for-checkout checkout o" <<-\EOF && + otherfile + EOF + test_completion "git -C repo-for-checkout checkout -- o" <<-\EOF && + otherfile + EOF + test_completion "git -C repo-for-checkout checkout l" <<-\EOF && + lostfile + EOF + test_completion "git -C repo-for-checkout checkout -- l" <<-\EOF + lostfile + EOF +' + test_expect_success 'git diff completes tracked paths when no refs match' ' # file1 and file2 are tracked but file3 is not # there is no ref that begins with f From 05e2ab1f31dd79ab6e17fc8f69a640ac8d0169d5 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 13 Aug 2026 12:12:34 -0700 Subject: [PATCH 07/16] completion: 'git checkout' completes untracked paths as a last resort We taught 'git checkout' to first try to complete revisions (unless '--' is present on the command line) and, failing that, to complete tracked paths. If this yields nothing, it lets the Bash default, which offers paths in $PWD, kick in. Teach it to complete untracked paths before giving up and letting the Bash default kick in. With this change, $ git -C another-directory checkout un finds the 'untracked' file in another-directory and offers it as a completion candidate. Note that this is of somewhat dubious value, as an untracked path by definition does not exist in the index, so checking it out from the index would not work well. Even when used to check out the path from a different branch, it is still of dubious value because it is unlikely that a path tracked in another branch is lying untracked in the working tree, as switching from a branch with the path to a branch without it will normally remove the file in the working tree. A better behavior probably is to detect the tree-ish argument on the command line and offer paths with the given prefix as candidates, but there is no __git_complete_from_tree() helper readily usable, so mark this as #leftoverbits to wait for another day. Signed-off-by: Junio C Hamano --- contrib/completion/git-completion.bash | 4 ++++ t/t9902-completion.sh | 21 +++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 0eecfcbf8bf662..e6dce62d3c3fb4 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1784,6 +1784,10 @@ _git_checkout () if [ ${#COMPREPLY[@]} -eq 0 ]; then __git_complete_index_file "" fi + + if [ ${#COMPREPLY[@]} -eq 0 ]; then + __git_complete_index_file "--others --directory" + fi } __git_sequencer_inprogress_options="--continue --quit --abort --skip" diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh index 13fa5c65c32f3a..e8418f069b5a85 100755 --- a/t/t9902-completion.sh +++ b/t/t9902-completion.sh @@ -2727,9 +2727,19 @@ test_expect_success 'git checkout completes tracked paths when no refs match' ' EOF ' +test_expect_success 'git checkout completes untracked paths, too' ' + # ufile is not tracked and there is no ref that begins with u + test_completion "git checkout u" <<-\EOF && + ufile + EOF + test_completion "git checkout -- u" <<-\EOF + ufile + EOF +' + test_expect_success 'git -C checkout completes paths in specified repo' ' # otherfile is tracked, oops is not - # lostfile is tracked but lost + # lostfile is tracked but lost, ufile is untracked. test_when_finished "rm -rf repo-for-checkout" && git init repo-for-checkout && echo content >repo-for-checkout/otherfile && @@ -2738,6 +2748,7 @@ test_expect_success 'git -C checkout completes paths in specified repo' ' git -C repo-for-checkout add lostfile && git -C repo-for-checkout commit -m otherfile && echo untracked >repo-for-checkout/oops && + echo untracked >repo-for-checkout/ufile && rm -f repo-for-checkout/lostfile && test_completion "git -C repo-for-checkout checkout o" <<-\EOF && otherfile @@ -2748,9 +2759,15 @@ test_expect_success 'git -C checkout completes paths in specified repo' ' test_completion "git -C repo-for-checkout checkout l" <<-\EOF && lostfile EOF - test_completion "git -C repo-for-checkout checkout -- l" <<-\EOF + test_completion "git -C repo-for-checkout checkout -- l" <<-\EOF && lostfile EOF + test_completion "git -C repo-for-checkout checkout u" <<-\EOF && + ufile + EOF + test_completion "git -C repo-for-checkout checkout -- u" <<-\EOF + ufile + EOF ' test_expect_success 'git diff completes tracked paths when no refs match' ' From 1e746b00aacc149f9c11b27b05fa6bedee6dd7df Mon Sep 17 00:00:00 2001 From: Siddharth Shrimali Date: Fri, 14 Aug 2026 01:38:25 +0530 Subject: [PATCH 08/16] builtin/repack: add --drop-filtered and --dry-run options Add two new command-line options to 'git-repack': --drop-filtered: intended to eventually delete objects that match the filter specification. Requires --filter and -a, and is incompatible with --filter-to. --dry-run: show which objects would be dropped without making any changes. Only meaningful with --drop-filtered. Keep --dry-run as a separate option rather than folding it into --drop-filtered (e.g. --drop-filtered=dry-run), to stay consistent with the --dry-run option other Git commands already provide and to leave room for it to describe other repack behavior later. A --drop-filtered= form can still be added later if more drop-specific modes are needed. --drop-filtered also requires a promisor remote to be configured, since dropping objects without a remote to fetch them back from would be permanent data loss. --drop-filtered is incompatible with bitmap writing: filtering breaks the "all objects in one pack" closure that bitmaps require. Detect an explicit -b/--write-bitmap-index on the command line with a dedicated option callback that sets a "write_bitmaps_given" flag, so it can be distinguished from a repack.writeBitmaps configuration value even when config already enables bitmaps. An explicit -b is reported as a conflict, while a config-provided default is silently disabled for the duration of the command. These options currently only perform validation. The actual enumeration and deletion will be added in follow-up commits. Mentored-by: Christian Couder Mentored-by: Siddharth Asthana Signed-off-by: Siddharth Shrimali Signed-off-by: Junio C Hamano --- builtin/repack.c | 74 +++++++++++++++++++++++++++++++-- t/meson.build | 1 + t/t7706-repack-drop-filtered.sh | 55 ++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 3 deletions(-) create mode 100755 t/t7706-repack-drop-filtered.sh diff --git a/builtin/repack.c b/builtin/repack.c index db504d673fcf52..ed79c04e13ba77 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -14,6 +14,7 @@ #include "promisor-remote.h" #include "repack.h" #include "shallow.h" +#include "list-objects-filter-options.h" #define ALL_INTO_ONE 1 #define LOOSEN_UNREACHABLE 2 @@ -28,11 +29,15 @@ static int use_delta_islands; static int run_update_server_info = 1; static char *packdir, *packtmp_name, *packtmp; static int midx_must_contain_cruft = 1; +static int drop_filtered; +static int dry_run; +static int write_bitmaps_given; static const char *const git_repack_usage[] = { N_("git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n" "[--window=] [--depth=] [--threads=] [--keep-pack=]\n" - "[--write-midx[=]] [--name-hash-version=] [--path-walk]"), + "[--write-midx[=]] [--name-hash-version=] [--path-walk]\n" + "[--filter=] [--drop-filtered [--dry-run]]"), NULL }; @@ -111,6 +116,21 @@ static int repack_config(const char *var, const char *value, return git_default_config(var, value, ctx, cb); } +static int option_parse_write_bitmaps(const struct option *opt, const char *arg, + int unset) +{ + int *value = opt->value; + + BUG_ON_OPT_ARG(arg); + if (unset) + *value = 0; + else + *value = 1; + + write_bitmaps_given = 1; + return 0; +} + static int option_parse_write_midx(const struct option *opt, const char *arg, int unset) { @@ -194,8 +214,9 @@ int cmd_repack(int argc, OPT__QUIET(&po_args.quiet, N_("be quiet")), OPT_BOOL('l', "local", &po_args.local, N_("pass --local to git-pack-objects")), - OPT_BOOL('b', "write-bitmap-index", &write_bitmaps, - N_("write bitmap index")), + OPT_CALLBACK_F('b', "write-bitmap-index", &write_bitmaps, NULL, + N_("write bitmap index"), + PARSE_OPT_NOARG, option_parse_write_bitmaps), OPT_BOOL('i', "delta-islands", &use_delta_islands, N_("pass --delta-islands to git-pack-objects")), OPT_STRING(0, "unpack-unreachable", &unpack_unreachable, N_("approxidate"), @@ -231,6 +252,10 @@ int cmd_repack(int argc, N_("pack prefix to store a pack containing pruned objects")), OPT_STRING(0, "filter-to", &filter_to, N_("dir"), N_("pack prefix to store a pack containing filtered out objects")), + OPT_BOOL(0, "drop-filtered", &drop_filtered, + N_("delete filtered out objects (requires --filter)")), + OPT_BOOL(0, "dry-run", &dry_run, + N_("only show which objects would be dropped")), OPT_END() }; @@ -252,6 +277,49 @@ int cmd_repack(int argc, po_args.depth = xstrdup_or_null(opt_depth); po_args.threads = xstrdup_or_null(opt_threads); + die_for_incompatible_opt2(drop_filtered, "--drop-filtered", + !!filter_to, "--filter-to"); + + if (dry_run && !drop_filtered) + die(_("--dry-run only takes effect with --drop-filtered")); + + if (drop_filtered) { + if (!dry_run) + die(_("--drop-filtered does not work without --dry-run yet")); + + if (!po_args.filter_options.choice) + die(_("--drop-filtered requires --filter")); + + if (!(pack_everything & ALL_INTO_ONE)) + die(_("--drop-filtered requires -a")); + + /* + * Only blob:limit= is supported for now. Reject other + * filter choices early, before walking the object database. + */ + if (po_args.filter_options.choice != LOFC_BLOB_LIMIT) + die(_("--drop-filtered only supports --filter=blob:limit= for now")); + + /* + * An explicit -b on the command line is a conflict we have to + * report; a bitmap setting from config is silently overridden + * for the duration of the command. + */ + if (write_bitmaps_given && write_bitmaps > 0) + die(_("options '%s' and '%s' cannot be used together"), + "--drop-filtered", "--write-bitmap-index"); + + /* + * Without a promisor remote there is nowhere to re-fetch the + * dropped objects from, so dropping them would be permanent + * data loss. + */ + if (!repo_has_promisor_remote(repo)) + die(_("--drop-filtered requires a promisor remote")); + + write_bitmaps = 0; + } + if (delete_redundant && repo->repository_format_precious_objects) die(_("cannot delete packs in a precious-objects repo")); diff --git a/t/meson.build b/t/meson.build index a25f37d2f5ae7d..92352e43c4a36a 100644 --- a/t/meson.build +++ b/t/meson.build @@ -964,6 +964,7 @@ integration_tests = [ 't7703-repack-geometric.sh', 't7704-repack-cruft.sh', 't7705-repack-incremental-midx.sh', + 't7706-repack-drop-filtered.sh', 't7800-difftool.sh', 't7810-grep.sh', 't7811-grep-open.sh', diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh new file mode 100755 index 00000000000000..07a976874ad105 --- /dev/null +++ b/t/t7706-repack-drop-filtered.sh @@ -0,0 +1,55 @@ +#!/bin/sh + +test_description='git repack --drop-filtered option validation' + +. ./test-lib.sh + +# Check option validation before any promisor walk +test_expect_success 'setup plain repo for validation' ' + git init plain && + test_commit -C plain initial && + git clone --bare plain plain.git && + git -C plain.git repack -a -d +' + +test_expect_success '--drop-filtered requires --filter' ' + test_must_fail git -C plain.git repack --drop-filtered --dry-run -a 2>err && + test_grep "drop-filtered requires --filter" err +' + +test_expect_success '--drop-filtered cannot be used with --filter-to' ' + test_must_fail git -C plain.git repack --drop-filtered \ + --filter=blob:limit=1k --filter-to=./filter-out 2>err && + test_grep "options .--drop-filtered. and .--filter-to. cannot be used together" err +' + +test_expect_success '--dry-run only takes effect with --drop-filtered' ' + test_must_fail git -C plain.git repack --dry-run 2>err && + test_grep "dry-run only takes effect with --drop-filtered" err +' + +test_expect_success '--drop-filtered requires -a' ' + test_must_fail git -C plain.git repack --drop-filtered \ + --filter=blob:limit=1k --dry-run 2>err && + test_grep "drop-filtered requires -a" err +' + +test_expect_success '--drop-filtered fails with --write-bitmap-index' ' + test_must_fail git -C plain.git repack --drop-filtered \ + --filter=blob:limit=1k --dry-run -a -b 2>err && + test_grep "options .--drop-filtered. and .--write-bitmap-index. cannot be used together" err +' + +test_expect_success '--drop-filtered rejects explicit -b even when repack.writeBitmaps=true' ' + test_must_fail git -C plain.git -c repack.writeBitmaps=true \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a -b 2>err && + test_grep "options .--drop-filtered. and .--write-bitmap-index. cannot be used together" err +' + +test_expect_success '--drop-filtered fails without a promisor remote' ' + test_must_fail git -C plain.git repack --drop-filtered \ + --filter=blob:limit=1k --dry-run -a 2>err && + test_grep "drop-filtered requires a promisor remote" err +' + +test_done From 401c3086712532395794cdc6899772a8de7aa07e Mon Sep 17 00:00:00 2001 From: Siddharth Shrimali Date: Fri, 14 Aug 2026 01:38:26 +0530 Subject: [PATCH 09/16] list-objects-filter: add list_objects_filter__filter_oidset() The existing filter entry point, list_objects_filter__filter_object(), is built around the object-walk path: it expects traversal context and provisional omit sets, and is meant to be called as objects are visited during a walk. A caller that already has a set of OIDs in hand and only wants to know which ones a filter would select has no usable entry point into the filter API. --drop-filtered is exactly such a caller: it collects promisor blobs into an oidset and needs to know which of them exceed the filter threshold, without performing an object walk. Add a helper, list_objects_filter__filter_oidset(), that takes a set of OIDs and populates an "omitted" set with those that would be filtered out by the given filter options. Only blob:limit=N filters are supported for now. This helper does not actually reuse the existing filter machinery. It reimplements the blob:limit size check directly. That machinery is tied to the object-walk path and cannot easily be driven from a plain oidset. A NEEDSWORK comment marks this so the helper can later be refactored to reuse the real filter logic instead of duplicating it. OBJECT_INFO_SKIP_FETCH_OBJECT is passed when reading object info so the helper never triggers a lazy fetch. Mentored-by: Christian Couder Mentored-by: Siddharth Asthana Signed-off-by: Siddharth Shrimali Signed-off-by: Junio C Hamano --- list-objects-filter.c | 45 +++++++++++++++++++++++++++++++++++++++++++ list-objects-filter.h | 16 +++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/list-objects-filter.c b/list-objects-filter.c index c912ff3079a7d7..10e44f135780ba 100644 --- a/list-objects-filter.c +++ b/list-objects-filter.c @@ -828,3 +828,48 @@ void list_objects_filter__free(struct filter *filter) filter->free_fn(filter->filter_data); free(filter); } + +/* + * NEEDSWORK: this reimplements the blob:limit size check rather than + * reusing the existing filter machinery in + * list_objects_filter__filter_object(). That machinery is currently + * tied to the object-walk path and cannot easily be driven from a + * plain oidset. It would be nice to refactor the filter code so this + * helper can reuse it instead of duplicating the size check. + */ +int list_objects_filter__filter_oidset(struct repository *r, + const struct list_objects_filter_options *opts, + const struct oidset *in, + struct oidset *omitted) +{ + struct oidset_iter iter; + const struct object_id *oid; + + if (opts->choice != LOFC_BLOB_LIMIT) + return error(_("filter_oidset: only blob:limit filters are supported")); + + oidset_iter_init(in, &iter); + while ((oid = oidset_iter_next(&iter))) { + struct object_info info = OBJECT_INFO_INIT; + enum object_type type; + size_t size; + + info.typep = &type; + info.sizep = &size; + + /* + * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering + * a lazy fetch while inspecting candidates for removal. + */ + if (odb_read_object_info_extended(r->objects, oid, &info, + OBJECT_INFO_SKIP_FETCH_OBJECT) < 0) + continue; + + if (type != OBJ_BLOB) + continue; + + if (size >= opts->blob_limit_value) + oidset_insert(omitted, oid); + } + return 0; +} diff --git a/list-objects-filter.h b/list-objects-filter.h index 9e98814111cee4..5207ab70a9188e 100644 --- a/list-objects-filter.h +++ b/list-objects-filter.h @@ -94,4 +94,20 @@ enum list_objects_filter_result list_objects_filter__filter_object( */ void list_objects_filter__free(struct filter *filter); +/* + * Given a set of OIDs in 'in', populate 'omitted' with those that + * would be filtered by 'opts'. Currently only blob:limit=N is + * supported. Objects that cannot be read are silently skipped. + * + * NEEDSWORK: this reimplements the blob:limit size check rather than + * reusing the existing filter machinery. See the matching comment in + * list-objects-filter.c. + * + * Return 0 on success, -1 if the filter is not supported. + */ +int list_objects_filter__filter_oidset(struct repository *r, + const struct list_objects_filter_options *opts, + const struct oidset *in, + struct oidset *omitted); + #endif /* LIST_OBJECTS_FILTER_H */ From 85531bbf298d9018aa9459df3a9110b62d3fbbc1 Mon Sep 17 00:00:00 2001 From: Siddharth Shrimali Date: Fri, 14 Aug 2026 01:38:27 +0530 Subject: [PATCH 10/16] repack-promisor: allow excluding objects from the rebuilt promisor pack Add a to_drop oidset parameter to repack_promisor_objects(). When it is non-NULL, write_oid() omits those objects from the rebuilt promisor pack. This is the mechanism --drop-filtered will use to remove promisor blobs, i.e. rebuild the promisor pack without them. All existing callers pass NULL, so behavior is unchanged. Mentored-by: Christian Couder Mentored-by: Siddharth Asthana Signed-off-by: Siddharth Shrimali Signed-off-by: Junio C Hamano --- builtin/repack.c | 2 +- repack-promisor.c | 15 ++++++++++++++- repack.h | 4 +++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/builtin/repack.c b/builtin/repack.c index ed79c04e13ba77..2ad6358535fa8c 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -430,7 +430,7 @@ int cmd_repack(int argc, strvec_push(&cmd.args, "--delta-islands"); if (pack_everything & ALL_INTO_ONE) { - repack_promisor_objects(repo, &po_args, &names, packtmp); + repack_promisor_objects(repo, &po_args, &names, packtmp, NULL); if (existing_packs_has_non_kept(&existing) && delete_redundant && diff --git a/repack-promisor.c b/repack-promisor.c index 90318ce15093f5..fabfdc168a9b86 100644 --- a/repack-promisor.c +++ b/repack-promisor.c @@ -6,10 +6,12 @@ #include "path.h" #include "repository.h" #include "run-command.h" +#include "oidset.h" struct write_oid_context { struct child_process *cmd; const struct git_hash_algo *algop; + const struct oidset *to_drop; }; /* @@ -23,6 +25,15 @@ static int write_oid(const struct object_id *oid, struct write_oid_context *ctx = data; struct child_process *cmd = ctx->cmd; + /* + * Objects in to_drop are being removed from the repository, so + * omit them from the rebuilt promisor pack. Each such object is a + * promisor object and therefore remains recoverable from the + * promisor remote. + */ + if (ctx->to_drop && oidset_contains(ctx->to_drop, oid)) + return 0; + if (cmd->in == -1) { if (start_command(cmd)) die(_("could not start pack-objects to repack promisor objects")); @@ -81,7 +92,8 @@ static void finish_repacking_promisor_objects(struct repository *repo, void repack_promisor_objects(struct repository *repo, const struct pack_objects_args *args, - struct string_list *names, const char *packtmp) + struct string_list *names, const char *packtmp, + const struct oidset *to_drop) { struct write_oid_context ctx; struct child_process cmd = CHILD_PROCESS_INIT; @@ -98,6 +110,7 @@ void repack_promisor_objects(struct repository *repo, */ ctx.cmd = &cmd; ctx.algop = repo->hash_algo; + ctx.to_drop = to_drop; odb_for_each_object(repo->objects, NULL, write_oid, &ctx, ODB_FOR_EACH_OBJECT_PROMISOR_ONLY); diff --git a/repack.h b/repack.h index f9fbc895f02940..a5a3f7c6babbe0 100644 --- a/repack.h +++ b/repack.h @@ -3,6 +3,7 @@ #include "list-objects-filter-options.h" #include "string-list.h" +#include "oidset.h" struct pack_objects_args { char *window; @@ -100,7 +101,8 @@ void generated_pack_install(struct generated_pack *pack, const char *name, void repack_promisor_objects(struct repository *repo, const struct pack_objects_args *args, - struct string_list *names, const char *packtmp); + struct string_list *names, const char *packtmp, + const struct oidset *to_drop); struct pack_geometry { struct packed_git **pack; From 8bb2a3f45454e8d901f570df38ff8fe87e5440db Mon Sep 17 00:00:00 2001 From: Siddharth Shrimali Date: Fri, 14 Aug 2026 01:38:28 +0530 Subject: [PATCH 11/16] builtin/repack: enumerate promisor blobs for --drop-filtered Add enumeration logic for --drop-filtered. In --dry-run mode, print the OIDs of locally-held promisor blobs that exceed the filter threshold, as candidates for removal. Reading from write_filtered_pack() cannot work for partial clones. git repack routes promisor objects through a separate path: repack_promisor_objects() repacks them first, and the main pack-objects run uses --exclude-promisor-objects. By the time write_filtered_pack() runs, the promisor blobs are already consumed by the main pack. The filtered pack is always empty on a partial clone. Instead, walk promisor objects directly via odb_for_each_object() with ODB_FOR_EACH_OBJECT_PROMISOR_ONLY, collecting all promisor blobs into an oidset. The blobs exceeding the filter threshold are then selected using list_objects_filter__filter_oidset(). Every object enumerated this way is a promisor object, so it is recoverable from the promisor remote in the same sense as the rest of a partial clone, as long as the remote still has it. This holds without a separate is_promisor_object() check. A future implementation can verify availability against the remote directly once a client-side remote-object-info query exists. OBJECT_INFO_SKIP_FETCH_OBJECT is passed to every object info query so enumeration never triggers a lazy fetch. The enumeration collects candidates into a caller-provided oidset and --dry-run prints them. Actually removing the objects, together with the required promisor-remote verification, is written in a later commit. Mentored-by: Christian Couder Mentored-by: Siddharth Asthana Signed-off-by: Siddharth Shrimali Signed-off-by: Junio C Hamano --- builtin/repack.c | 20 +++++++- repack-filtered.c | 82 +++++++++++++++++++++++++++++++ repack.h | 4 ++ t/t7706-repack-drop-filtered.sh | 85 ++++++++++++++++++++++++++++++++- 4 files changed, 189 insertions(+), 2 deletions(-) diff --git a/builtin/repack.c b/builtin/repack.c index 2ad6358535fa8c..3633b17ce8cce8 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -15,6 +15,8 @@ #include "repack.h" #include "shallow.h" #include "list-objects-filter-options.h" +#include "oidset.h" +#include "hex.h" #define ALL_INTO_ONE 1 #define LOOSEN_UNREACHABLE 2 @@ -160,6 +162,7 @@ int cmd_repack(int argc, struct string_list_item *item; struct string_list names = STRING_LIST_INIT_DUP; struct existing_packs existing = EXISTING_PACKS_INIT; + struct oidset drop_oids = OIDSET_INIT; struct pack_geometry geometry = { 0 }; struct tempfile *refs_snapshot = NULL; int i, ret; @@ -318,6 +321,20 @@ int cmd_repack(int argc, die(_("--drop-filtered requires a promisor remote")); write_bitmaps = 0; + + ret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids); + + if (ret) + goto cleanup; + + if (dry_run) { + struct oidset_iter iter; + const struct object_id *oid; + + oidset_iter_init(&drop_oids, &iter); + while ((oid = oidset_iter_next(&iter))) + printf("%s\n", oid_to_hex(oid)); + } } if (delete_redundant && repo->repository_format_precious_objects) @@ -613,7 +630,7 @@ int cmd_repack(int argc, } } - if (po_args.filter_options.choice) { + if (po_args.filter_options.choice && !drop_filtered) { struct write_pack_opts opts = { .po_args = &po_args, .destination = filter_to, @@ -706,6 +723,7 @@ int cmd_repack(int argc, cleanup: string_list_clear(&keep_pack_list, 0); string_list_clear(&names, 1); + oidset_clear(&drop_oids); existing_packs_release(&existing); pack_geometry_release(&geometry); pack_objects_args_release(&po_args); diff --git a/repack-filtered.c b/repack-filtered.c index edcf7667c5c378..869b9fc6e3b94c 100644 --- a/repack-filtered.c +++ b/repack-filtered.c @@ -3,6 +3,12 @@ #include "repository.h" #include "run-command.h" #include "string-list.h" +#include "hex.h" +#include "packfile.h" +#include "list-objects-filter-options.h" +#include "list-objects-filter.h" +#include "odb.h" +#include "promisor-remote.h" int write_filtered_pack(const struct write_pack_opts *opts, struct existing_packs *existing, @@ -49,3 +55,79 @@ int write_filtered_pack(const struct write_pack_opts *opts, return finish_pack_objects_cmd(existing->repo->hash_algo, opts, &cmd, names); } + +struct collect_cb_data { + struct repository *repo; + struct oidset *set; +}; + +static int collect_promisor_blob(const struct object_id *oid, + struct object_info *oi UNUSED, + void *cb_data) +{ + struct collect_cb_data *data = cb_data; + struct object_info info = OBJECT_INFO_INIT; + enum object_type type; + + info.typep = &type; + + /* + * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering a + * lazy fetch while collecting promisor blobs. + */ + if (odb_read_object_info_extended(data->repo->objects, oid, &info, + OBJECT_INFO_SKIP_FETCH_OBJECT) < 0) + return 0; + + if (type == OBJ_BLOB) + oidset_insert(data->set, oid); + + return 0; +} + +int enumerate_promisor_blobs(struct repository *repo, + const struct list_objects_filter_options *filter, + struct oidset *to_drop) +{ + struct oidset all_promisor_blobs = OIDSET_INIT; + struct collect_cb_data cb = { + .repo = repo, + .set = &all_promisor_blobs + }; + int ret = 0; + + /* + * The caller (cmd_repack) is responsible for validating that a + * blob:limit filter and a promisor remote are present before + * calling this function. + * + * Walk only promisor objects. Every object visited here is a + * promisor object, so it is recoverable from the promisor remote + * as long as the remote still has it, the same assumption the rest + * of partial clone relies on. + * + * We do not use write_filtered_pack() here because git repack + * routes promisor objects through repack_promisor_objects() + * before the filter machinery runs, so the filtered pack never + * contains promisor blobs. Direct enumeration via + * ODB_FOR_EACH_OBJECT_PROMISOR_ONLY is the correct approach. + */ + ret = odb_for_each_object(repo->objects, NULL, + collect_promisor_blob, &cb, + ODB_FOR_EACH_OBJECT_PROMISOR_ONLY); + if (ret) + goto cleanup; + + /* + * Apply the filter to find which blobs exceed the threshold. + * The caller has to_drop and is responsible for clearing it. + */ + ret = list_objects_filter__filter_oidset(repo, + filter, + &all_promisor_blobs, + to_drop); + +cleanup: + oidset_clear(&all_promisor_blobs); + return ret; +} diff --git a/repack.h b/repack.h index a5a3f7c6babbe0..61e554e4ed383d 100644 --- a/repack.h +++ b/repack.h @@ -167,6 +167,10 @@ int write_filtered_pack(const struct write_pack_opts *opts, struct existing_packs *existing, struct string_list *names); +int enumerate_promisor_blobs(struct repository *repo, + const struct list_objects_filter_options *filter, + struct oidset *to_drop); + int write_cruft_pack(const struct write_pack_opts *opts, const char *cruft_expiration, unsigned long combine_cruft_below_size, diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh index 07a976874ad105..6352f1fdce8bc6 100755 --- a/t/t7706-repack-drop-filtered.sh +++ b/t/t7706-repack-drop-filtered.sh @@ -1,9 +1,37 @@ #!/bin/sh -test_description='git repack --drop-filtered option validation' +test_description='git repack --drop-filtered enumerates filtered promisor blobs' . ./test-lib.sh +# Delete a loose or packed object from "repo". +delete_object () { + local repo="$1" && + local obj="$2" && + local path="$repo/.git/objects/$(test_oid_to_path "$obj")" && + rm "$path" +} + +# Pack the objects into a promisor pack inside "repo". It is a pack +# accompanied by an empty ".promisor" marker file. Objects +# in such a pack are treated as recoverable from the promisor remote. +pack_as_from_promisor () { + HASH=$(git -C repo pack-objects .git/objects/pack/pack) && + >repo/.git/objects/pack/pack-$HASH.promisor && + echo $HASH +} + +# Write a blob of $1 bytes into "repo", record it as coming from the +# promisor remote, and remove the loose copy so the object is only +# present in the promisor pack. +promisor_blob () { + test-tool genrandom "$1" "$2" >blob_content && + OID=$(git -C repo hash-object -w --stdin /dev/null && + delete_object repo "$OID" && + echo "$OID" +} + # Check option validation before any promisor walk test_expect_success 'setup plain repo for validation' ' git init plain && @@ -52,4 +80,59 @@ test_expect_success '--drop-filtered fails without a promisor remote' ' test_grep "drop-filtered requires a promisor remote" err ' +# Enumeration tests using promisor pack +test_expect_success 'setup repo with a promisor remote' ' + rm -rf repo && + test_create_repo repo && + test_commit -C repo base && + + # Mark the repo as a partial clone with a promisor remote so the + # promisor walk and the safety guard are satisfied. + git -C repo config core.repositoryformatversion 1 && + git -C repo config extensions.partialclone origin && + git -C repo config remote.origin.promisor true && + git -C repo config remote.origin.url "." && + + BIG=$(promisor_blob big 3072) && + SMALL=$(promisor_blob small 512) && + echo "$BIG" >big_oid && + echo "$SMALL" >small_oid +' + +test_expect_success 'promisor blob over the threshold is listed' ' + BIG=$(cat big_oid) && + SMALL=$(cat small_oid) && + + git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a >out && + + test_grep "$BIG" out && + test_grep ! "$SMALL" out +' + +test_expect_success 'locally created blob is never listed' ' + BIG=$(cat big_oid) && + + # Large blob that exists only locally must never be a drop candidate. + # Dropping it would be unrecoverable. + test-tool genrandom local 4096 >local_content && + LOCAL=$(git -C repo hash-object -w --stdin out && + + test_grep "$BIG" out && + test_grep ! "$LOCAL" out +' + +test_expect_success '--dry-run does not remove the filtered objects' ' + BIG=$(cat big_oid) && + + git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a >out && + + # Candidate blob must still be present after a dry run. + git -C repo cat-file -e "$BIG" +' + test_done From 0c4142a25b94ef9e706c8b8b7d2c8300dde9a726 Mon Sep 17 00:00:00 2001 From: Siddharth Shrimali Date: Fri, 14 Aug 2026 01:38:29 +0530 Subject: [PATCH 12/16] builtin/repack: actually drop filtered promisor blobs Make --drop-filtered remove the enumerated promisor blobs instead of only listing them. The drop set is computed before repack_promisor_objects() runs, and on a real run it is passed in so the rebuilt promisor pack omits those blobs. --drop-filtered implies -d so the old promisor packs, which still contain the dropped blobs, are removed. Without this the blobs would survive in the redundant packs. The existing repack machinery performs the write-before-delete and fsync, so the drop is crash-safe. The dropped blobs become absent locally but remain recoverable from the promisor remote, so a later access lazy-fetches them back transparently. --dry-run keeps its previous behavior, i.e. it lists the candidates and changes nothing. Mentored-by: Christian Couder Mentored-by: Siddharth Asthana Signed-off-by: Siddharth Shrimali Signed-off-by: Junio C Hamano --- Documentation/git-repack.adoc | 28 ++++++++++++++++++++++++++++ builtin/repack.c | 14 ++++++++++---- t/t7706-repack-drop-filtered.sh | 12 ++++++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc index 72c42015e23f94..130249a1392622 100644 --- a/Documentation/git-repack.adoc +++ b/Documentation/git-repack.adoc @@ -12,6 +12,7 @@ SYNOPSIS 'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m] [--window=] [--depth=] [--threads=] [--keep-pack=] [--write-midx[=]] [--name-hash-version=] [--path-walk] + [--filter=] [--drop-filtered [--dry-run]] DESCRIPTION ----------- @@ -182,6 +183,33 @@ depth is 4095. `objects` and `objects/info/alternates` sections of linkgit:gitrepository-layout[5]. +--drop-filtered:: + Delete the local objects that match the `--filter` specification + instead of keeping them in a separate packfile, reclaiming the + disk space they occupy. This is intended for partial clones, + where the filtered objects are promisor objects that remain + recoverable from the promisor remote and are lazily re-fetched + on demand when they are next needed. ++ +Only large blobs are supported for now, so `--filter=blob:limit=` +is currently the only accepted filter. Because dropped objects must be +recoverable, this option requires a promisor remote to be configured +and refuses to run otherwise. ++ +This option requires `-a`, and implies `-d`: the objects are dropped by +rebuilding the promisor pack without them and then removing the now +redundant old packs, so the redundant packs must be deleted for the +space to actually be reclaimed. It is incompatible with `--filter-to` +and with bitmap writing (`-b`/`--write-bitmap-index`), since filtering +breaks the single-pack closure that bitmaps require. A bitmap setting +coming from configuration is silently disabled for the duration of the +command. + +--dry-run:: + Only meaningful with `--drop-filtered`. List the objects that + would be dropped, one object ID per line, without rebuilding any + pack or deleting anything. + -b:: --write-bitmap-index:: Write a reachability bitmap index as part of the repack. This diff --git a/builtin/repack.c b/builtin/repack.c index 3633b17ce8cce8..a5f13fdd8741e8 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -287,9 +287,6 @@ int cmd_repack(int argc, die(_("--dry-run only takes effect with --drop-filtered")); if (drop_filtered) { - if (!dry_run) - die(_("--drop-filtered does not work without --dry-run yet")); - if (!po_args.filter_options.choice) die(_("--drop-filtered requires --filter")); @@ -322,6 +319,14 @@ int cmd_repack(int argc, write_bitmaps = 0; + /* + * Dropping objects means rebuilding the promisor packs + * without them and then removing the old packs, so the + * redundant packs must be deleted. Imply -d on a real run. + */ + if (!dry_run) + delete_redundant = 1; + ret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids); if (ret) @@ -447,7 +452,8 @@ int cmd_repack(int argc, strvec_push(&cmd.args, "--delta-islands"); if (pack_everything & ALL_INTO_ONE) { - repack_promisor_objects(repo, &po_args, &names, packtmp, NULL); + repack_promisor_objects(repo, &po_args, &names, packtmp, + (drop_filtered && !dry_run) ? &drop_oids : NULL); if (existing_packs_has_non_kept(&existing) && delete_redundant && diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh index 6352f1fdce8bc6..80c695742fef57 100755 --- a/t/t7706-repack-drop-filtered.sh +++ b/t/t7706-repack-drop-filtered.sh @@ -135,4 +135,16 @@ test_expect_success '--dry-run does not remove the filtered objects' ' git -C repo cat-file -e "$BIG" ' +test_expect_success '--drop-filtered removes the promisor blob locally' ' + BIG=$(cat big_oid) && + SMALL=$(cat small_oid) && + + git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k -a && + + git -C repo cat-file --batch-all-objects --batch-check="%(objectname)" >present && + test_grep ! "$BIG" present && + test_grep "$SMALL" present +' + test_done From c6fed8b7a674840c5fc698c9aa1ddd9b8cc9b97d Mon Sep 17 00:00:00 2001 From: Siddharth Shrimali Date: Fri, 14 Aug 2026 01:38:30 +0530 Subject: [PATCH 13/16] builtin/repack: add guards for --drop-filtered --drop-filtered removes local promisor blobs. That is only safe when the repository is not mid-operation and when the blobs are not actively in use, so add two guards, both skipped for bare repositories which have neither a worktree nor an index. First, refuse to run while a merge, rebase, am, cherry-pick, revert, or bisect is in progress. During these operations the working tree and index are in an intermediate state, and rewriting packs and deleting objects underneath a half-finished operation is unsafe. Second, refuse to drop a blob that the current index references. Such a blob is needed by the working tree, so dropping it would only cause the next command that touches the worktree to lazy-fetch it straight back, reclaiming nothing. The offending path is reported so the user can see why the drop was refused. Mentored-by: Christian Couder Mentored-by: Siddharth Asthana Signed-off-by: Siddharth Shrimali Signed-off-by: Junio C Hamano --- Documentation/git-repack.adoc | 9 ++++++ builtin/repack.c | 52 +++++++++++++++++++++++++++++++++ t/t7706-repack-drop-filtered.sh | 35 ++++++++++++++++++++++ 3 files changed, 96 insertions(+) diff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc index 130249a1392622..a1f9e64f668750 100644 --- a/Documentation/git-repack.adoc +++ b/Documentation/git-repack.adoc @@ -204,6 +204,15 @@ and with bitmap writing (`-b`/`--write-bitmap-index`), since filtering breaks the single-pack closure that bitmaps require. A bitmap setting coming from configuration is silently disabled for the duration of the command. ++ +As a convenience, since dropped objects remain recoverable by lazy fetch, +`--drop-filtered` refuses to run while another operation +(merge, rebase, am, cherry-pick, revert, or bisect) is in progress, to +avoid a surprising network fetch mid-operation, and refuses to drop any +blob that the current index references, since such a blob would only be +lazily re-fetched by the next command that inspects the working tree. +These checks are skipped in bare repositories, which have neither a +working tree nor an index. --dry-run:: Only meaningful with `--drop-filtered`. List the objects that diff --git a/builtin/repack.c b/builtin/repack.c index a5f13fdd8741e8..c4360382c1fce2 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -17,6 +17,8 @@ #include "list-objects-filter-options.h" #include "oidset.h" #include "hex.h" +#include "wt-status.h" +#include "read-cache-ll.h" #define ALL_INTO_ONE 1 #define LOOSEN_UNREACHABLE 2 @@ -317,6 +319,33 @@ int cmd_repack(int argc, if (!repo_has_promisor_remote(repo)) die(_("--drop-filtered requires a promisor remote")); + /* + * Refuse to run while another operation is in progress. A + * dropped object would just be lazily re-fetched when the + * operation resumes, but triggering a network fetch in the + * middle of a half-finished + * merge/rebase/cherry-pick/revert/bisect is a poor + * experience, so this is a UX convenience rather than a + * safety measure. Bare repositories have no such state, so + * the check is skipped there. + */ + if (!is_bare_repository(repo)) { + struct wt_status_state state = { 0 }; + + wt_status_get_state(repo, &state, 0); + if (state.merge_in_progress || state.revert_in_progress || + state.rebase_in_progress || state.bisect_in_progress || + state.cherry_pick_in_progress || state.am_in_progress || + state.rebase_interactive_in_progress) { + wt_status_state_free_buffers(&state); + die(_("--drop-filtered cannot be used while " + "another operation (merge, rebase, am, " + "cherry-pick, revert, or bisect) is in " + "progress")); + } + wt_status_state_free_buffers(&state); + } + write_bitmaps = 0; /* @@ -332,6 +361,29 @@ int cmd_repack(int argc, if (ret) goto cleanup; + /* + * Refuse to drop blobs that the current index references. + * Such a blob would only be lazily re-fetched by the next + * command that touches the worktree, so dropping it reclaims + * nothing. This guard just avoids that churn. Bare + * repositories have no index, so the check is skipped there. + */ + if (!is_bare_repository(repo) && oidset_size(&drop_oids)) { + struct index_state *istate = repo->index; + unsigned int i; + + if (repo_read_index(repo) < 0) + die(_("could not read the index")); + + for (i = 0; i < istate->cache_nr; i++) { + const struct cache_entry *ce = istate->cache[i]; + + if (oidset_contains(&drop_oids, &ce->oid)) + die(_("cannot drop '%s' (%s): it is referenced by the current index"), + ce->name, oid_to_hex(&ce->oid)); + } + } + if (dry_run) { struct oidset_iter iter; const struct object_id *oid; diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh index 80c695742fef57..cb361158349c13 100755 --- a/t/t7706-repack-drop-filtered.sh +++ b/t/t7706-repack-drop-filtered.sh @@ -147,4 +147,39 @@ test_expect_success '--drop-filtered removes the promisor blob locally' ' test_grep "$SMALL" present ' +test_expect_success '--drop-filtered refuses when a merge is in progress' ' + test_when_finished "git -C repo merge --abort || :" && + + # Create a conflicting merge so wt_status reports it. + git -C repo checkout -B mergebase base && + echo one >repo/conflict.txt && + git -C repo add conflict.txt && + git -C repo commit -m one && + + git -C repo checkout -B mergeother base && + echo two >repo/conflict.txt && + git -C repo add conflict.txt && + git -C repo commit -m two && + + test_must_fail git -C repo merge mergebase && + + test_must_fail git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err && + test_grep "in progress" err +' + +test_expect_success '--drop-filtered refuses to drop an index-referenced blob' ' + # Create a large blob, add it to the index and make it a promisor object + # so the index references it and enumeration picks it up. + test-tool genrandom idx 4096 >repo/tracked-big.bin && + git -C repo add tracked-big.bin && + OID=$(git -C repo rev-parse :tracked-big.bin) && + printf "%s\n" "$OID" | pack_as_from_promisor >/dev/null && + delete_object repo "$OID" && + + test_must_fail git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err && + test_grep "referenced by the current index" err +' + test_done From 026636128f6a99854562412104a6f58db0df47bc Mon Sep 17 00:00:00 2001 From: Swapnil Saste | INDIA Date: Fri, 14 Aug 2026 18:46:59 +0000 Subject: [PATCH 14/16] doc: fix typo in submitting patches Remove the article "an" before "incremental updates". Signed-off-by: Swapnil Saste | INDIA Signed-off-by: Junio C Hamano --- Documentation/SubmittingPatches | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index 8332073e270330..d820844d365a75 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -64,7 +64,7 @@ help you find out who they are. can still continue to further improve them by adding more patches on top, but by the time a topic gets merged to 'next', it is expected that everybody agrees that the scope and the basic direction of the - topic are appropriate, so such an incremental updates are limited to + topic are appropriate, so such incremental updates are limited to small corrections and polishing. After a topic cooks for some time (like 7 calendar days) in 'next' without needing further tweaks on top, it gets merged to the 'master' branch and wait to become part From f17d211c97f511d8bd21bf7a956edda99381986a Mon Sep 17 00:00:00 2001 From: Colin Hinton Date: Fri, 14 Aug 2026 14:42:10 -0700 Subject: [PATCH 15/16] chdir-notify.h: Removed unused param 'name' The `name` parameter in `chdir_notify_entry` was only ever used by chdir_notify_reparent() to produce trace output. That function was removed in 5bf546755c (chdir-notify: drop unused `chdir_notify_reparent()`, 2026-06-25), which left `name` with no remaining consumers. Prior to that removal, most callers had already stopped passing a meaningful name, switching to NULL in 1f43ff2c7e (refs: unregister reference stores from "chdir_notify", 2026-06-25) and 0de2467e6c (odb/source-packed: start converting to a proper `struct odb_source`, 2026-06-17). Since no caller has populated `name` with real data for some time, and its last consumer is gone, drop it from chdir_notify_register(), chdir_notify_unregister(), and the callback signature to simplify the API. Signed-off-by: Colin Hinton Signed-off-by: Junio C Hamano --- chdir-notify.c | 12 ++++-------- chdir-notify.h | 8 +++----- odb/source-files.c | 7 +++---- odb/source-loose.c | 7 +++---- odb/source-packed.c | 7 +++---- refs/files-backend.c | 7 +++---- refs/packed-backend.c | 7 +++---- refs/reftable-backend.c | 7 +++---- setup.c | 5 ++--- tmp-objdir.c | 7 +++---- 10 files changed, 30 insertions(+), 44 deletions(-) diff --git a/chdir-notify.c b/chdir-notify.c index 1237a45e2e6492..55773c24c96eda 100644 --- a/chdir-notify.c +++ b/chdir-notify.c @@ -7,25 +7,22 @@ #include "trace.h" struct chdir_notify_entry { - const char *name; chdir_notify_callback cb; void *data; struct list_head list; }; static LIST_HEAD(chdir_notify_entries); -void chdir_notify_register(const char *name, - chdir_notify_callback cb, +void chdir_notify_register(chdir_notify_callback cb, void *data) { struct chdir_notify_entry *e = xmalloc(sizeof(*e)); - e->name = name; e->cb = cb; e->data = data; list_add_tail(&e->list, &chdir_notify_entries); } -void chdir_notify_unregister(const char *name, chdir_notify_callback cb, +void chdir_notify_unregister(chdir_notify_callback cb, void *data) { struct list_head *pos, *p; @@ -34,8 +31,7 @@ void chdir_notify_unregister(const char *name, chdir_notify_callback cb, struct chdir_notify_entry *e = list_entry(pos, struct chdir_notify_entry, list); - if (e->cb != cb || e->data != data || !e->name != !name || - (e->name && strcmp(e->name, name))) + if (e->cb != cb || e->data != data) continue; list_del(pos); @@ -64,7 +60,7 @@ int chdir_notify(const char *new_cwd) list_for_each(pos, &chdir_notify_entries) { struct chdir_notify_entry *e = list_entry(pos, struct chdir_notify_entry, list); - e->cb(e->name, old_cwd.buf, new_cwd, e->data); + e->cb(old_cwd.buf, new_cwd, e->data); } strbuf_release(&old_cwd); diff --git a/chdir-notify.h b/chdir-notify.h index 36b4114472e31d..e4ae38e12dd8b9 100644 --- a/chdir-notify.h +++ b/chdir-notify.h @@ -33,13 +33,11 @@ * $GIT_TRACE_SETUP. It may be NULL, but if non-NULL should point to * storage which lasts as long as the registration is active. */ -typedef void (*chdir_notify_callback)(const char *name, - const char *old_cwd, +typedef void (*chdir_notify_callback)(const char *old_cwd, const char *new_cwd, void *data); -void chdir_notify_register(const char *name, chdir_notify_callback cb, void *data); -void chdir_notify_unregister(const char *name, chdir_notify_callback cb, - void *data); +void chdir_notify_register(chdir_notify_callback cb, void *data); +void chdir_notify_unregister(chdir_notify_callback cb, void *data); /* * diff --git a/odb/source-files.c b/odb/source-files.c index bbd1784b337c6d..00d9af70f86c18 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -12,8 +12,7 @@ #include "strbuf.h" #include "write-or-die.h" -static void odb_source_files_reparent(const char *name UNUSED, - const char *old_cwd, +static void odb_source_files_reparent(const char *old_cwd, const char *new_cwd, void *cb_data) { @@ -27,7 +26,7 @@ static void odb_source_files_reparent(const char *name UNUSED, static void odb_source_files_free(struct odb_source *source) { struct odb_source_files *files = odb_source_files_downcast(source); - chdir_notify_unregister(NULL, odb_source_files_reparent, files); + chdir_notify_unregister(odb_source_files_reparent, files); odb_source_free(&files->loose->base); odb_source_free(&files->packed->base); odb_source_release(&files->base); @@ -292,7 +291,7 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb, * paths in the primary ODB source in some user-facing functionality. */ if (!is_absolute_path(path)) - chdir_notify_register(NULL, odb_source_files_reparent, files); + chdir_notify_register(odb_source_files_reparent, files); return files; } diff --git a/odb/source-loose.c b/odb/source-loose.c index 6211348a4d3561..eb7223e985f1ab 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -675,8 +675,7 @@ static void odb_source_loose_close(struct odb_source *source UNUSED) /* Nothing to do. */ } -static void odb_source_loose_reparent(const char *name UNUSED, - const char *old_cwd, +static void odb_source_loose_reparent(const char *old_cwd, const char *new_cwd, void *cb_data) { @@ -692,7 +691,7 @@ static void odb_source_loose_free(struct odb_source *source) struct odb_source_loose *loose = odb_source_loose_downcast(source); odb_source_loose_clear_cache(loose); loose_object_map_clear(&loose->map); - chdir_notify_unregister(NULL, odb_source_loose_reparent, loose); + chdir_notify_unregister(odb_source_loose_reparent, loose); odb_source_release(&loose->base); free(loose); } @@ -722,7 +721,7 @@ struct odb_source_loose *odb_source_loose_new(struct object_database *odb, loose->base.write_alternate = odb_source_loose_write_alternate; if (!is_absolute_path(loose->base.path)) - chdir_notify_register(NULL, odb_source_loose_reparent, loose); + chdir_notify_register(odb_source_loose_reparent, loose); return loose; } diff --git a/odb/source-packed.c b/odb/source-packed.c index decc81aa52bfe6..42c4995aa0028d 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -691,8 +691,7 @@ static void odb_source_packed_reprepare(struct odb_source *source) odb_source_packed_prepare(packed); } -static void odb_source_packed_reparent(const char *name UNUSED, - const char *old_cwd, +static void odb_source_packed_reparent(const char *old_cwd, const char *new_cwd, void *cb_data) { @@ -721,7 +720,7 @@ static void odb_source_packed_free(struct odb_source *source) { struct odb_source_packed *packed = odb_source_packed_downcast(source); - chdir_notify_unregister(NULL, odb_source_packed_reparent, packed); + chdir_notify_unregister(odb_source_packed_reparent, packed); for (struct packfile_list_entry *e = packed->packs.head; e; e = e->next) free(e->pack); @@ -758,7 +757,7 @@ struct odb_source_packed *odb_source_packed_new(struct object_database *odb, packed->base.write_alternate = odb_source_packed_write_alternate; if (!is_absolute_path(path)) - chdir_notify_register(NULL, odb_source_packed_reparent, packed); + chdir_notify_register(odb_source_packed_reparent, packed); return packed; } diff --git a/refs/files-backend.c b/refs/files-backend.c index 3df56c25c8c585..99131c32ec2de3 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -109,8 +109,7 @@ static void clear_loose_ref_cache(struct files_ref_store *refs) } } -static void files_ref_store_reparent(const char *name UNUSED, - const char *old_cwd, +static void files_ref_store_reparent(const char *old_cwd, const char *new_cwd, void *payload) { @@ -180,7 +179,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo, packed_ref_store_init(repo, NULL, refs->gitcommondir, opts); refs->store_flags = opts->access_flags; - chdir_notify_register(NULL, files_ref_store_reparent, refs); + chdir_notify_register(files_ref_store_reparent, refs); strbuf_release(&refdir); @@ -232,7 +231,7 @@ static void files_ref_store_release(struct ref_store *ref_store) free(refs->gitcommondir); ref_store_release(refs->packed_ref_store); free(refs->packed_ref_store); - chdir_notify_unregister(NULL, files_ref_store_reparent, refs); + chdir_notify_unregister(files_ref_store_reparent, refs); } static void files_reflog_path(struct files_ref_store *refs, diff --git a/refs/packed-backend.c b/refs/packed-backend.c index 499cb55dface4f..e7fc4d91f36267 100644 --- a/refs/packed-backend.c +++ b/refs/packed-backend.c @@ -211,8 +211,7 @@ static size_t snapshot_hexsz(const struct snapshot *snapshot) return snapshot->refs->base.repo->hash_algo->hexsz; } -static void packed_ref_store_reparent(const char *name UNUSED, - const char *old_cwd, +static void packed_ref_store_reparent(const char *old_cwd, const char *new_cwd, void *payload) { @@ -242,7 +241,7 @@ struct ref_store *packed_ref_store_init(struct repository *repo, strbuf_addf(&sb, "%s/packed-refs", gitdir); refs->path = strbuf_detach(&sb, NULL); - chdir_notify_register(NULL, packed_ref_store_reparent, refs); + chdir_notify_register(packed_ref_store_reparent, refs); return ref_store; } @@ -287,7 +286,7 @@ static void packed_ref_store_release(struct ref_store *ref_store) clear_snapshot(refs); rollback_lock_file(&refs->lock); delete_tempfile(&refs->tempfile); - chdir_notify_unregister(NULL, packed_ref_store_reparent, refs); + chdir_notify_unregister(packed_ref_store_reparent, refs); free(refs->path); } diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index 212408c769c5e6..96b633f1bd3ee8 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -391,8 +391,7 @@ static const struct reftable_be_write_options *reftable_be_write_options(struct return opts; } -static void reftable_be_reparent(const char *name UNUSED, - const char *old_cwd, +static void reftable_be_reparent(const char *old_cwd, const char *new_cwd, void *payload) { @@ -465,7 +464,7 @@ static struct ref_store *reftable_be_init(struct repository *repo, goto done; } - chdir_notify_register(NULL, reftable_be_reparent, refs); + chdir_notify_register(reftable_be_reparent, refs); done: assert(refs->err != REFTABLE_API_ERROR); @@ -492,7 +491,7 @@ static void reftable_be_release(struct ref_store *ref_store) free(be); } strmap_clear(&refs->worktree_backends, 0); - chdir_notify_unregister(NULL, reftable_be_reparent, refs); + chdir_notify_unregister(reftable_be_reparent, refs); } static int reftable_be_create_on_disk(struct ref_store *ref_store, diff --git a/setup.c b/setup.c index 0de56a074f7c15..b83d7101e822c9 100644 --- a/setup.c +++ b/setup.c @@ -1082,8 +1082,7 @@ static void set_git_dir_1(struct repository *repo, const char *path) setup_git_env_internal(repo, path); } -static void update_relative_gitdir(const char *name UNUSED, - const char *old_cwd, +static void update_relative_gitdir(const char *old_cwd, const char *new_cwd, void *data) { @@ -1108,7 +1107,7 @@ static void set_git_dir(struct repository *repo, const char *path, int make_real set_git_dir_1(repo, path); if (!is_absolute_path(path)) - chdir_notify_register(NULL, update_relative_gitdir, repo); + chdir_notify_register(update_relative_gitdir, repo); strbuf_release(&realpath); } diff --git a/tmp-objdir.c b/tmp-objdir.c index d199d39e7c9d51..520df2df8c5a6b 100644 --- a/tmp-objdir.c +++ b/tmp-objdir.c @@ -37,8 +37,7 @@ static void tmp_objdir_free(struct tmp_objdir *t) free(t); } -static void tmp_objdir_reparent(const char *name UNUSED, - const char *old_cwd, +static void tmp_objdir_reparent(const char *old_cwd, const char *new_cwd, void *cb_data) { @@ -67,7 +66,7 @@ int tmp_objdir_destroy(struct tmp_objdir *t) err = remove_dir_recursively(&t->path, 0); - chdir_notify_unregister(NULL, tmp_objdir_reparent, t); + chdir_notify_unregister(tmp_objdir_reparent, t); tmp_objdir_free(t); return err; @@ -155,7 +154,7 @@ struct tmp_objdir *tmp_objdir_create(struct repository *r, repo_get_object_directory(r), prefix); if (!is_absolute_path(t->path.buf)) - chdir_notify_register(NULL, tmp_objdir_reparent, t); + chdir_notify_register(tmp_objdir_reparent, t); if (!mkdtemp(t->path.buf)) { /* free, not destroy, as we never touched the filesystem */ From f78ce2f7b6df702f93d40b85d6bda92a3f65da79 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Tue, 25 Aug 2026 10:52:31 -0700 Subject: [PATCH 16/16] The 19th batch Signed-off-by: Junio C Hamano --- Documentation/RelNotes/2.56.0.adoc | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Documentation/RelNotes/2.56.0.adoc b/Documentation/RelNotes/2.56.0.adoc index d33dd57996a486..65f0d965633959 100644 --- a/Documentation/RelNotes/2.56.0.adoc +++ b/Documentation/RelNotes/2.56.0.adoc @@ -121,6 +121,11 @@ UI, Workflows & Features code did not check the presence of a value and instead segfaulted without one, which has been corrected. + * 'git repack' has been taught '--drop-filtered' to delete local + promisor blobs exceeding a limit (currently 'blob:limit=') in partial + clones, reclaiming space. Guards prevent running during other + operations or if referenced by the index. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -419,6 +424,16 @@ Performance, Internal Implementation, Development Support etc. * The setting of a now-unused member '.pretty_given' in the sequencer machinery has been removed. + * The performance of adding numerous new packfiles has been improved + by introducing a fast path for known-new packfiles to skip an + unnecessary traversal in packfile_list_append(), avoiding a + quadratic complexity regression on load. + + * The unused name parameter in 'struct chdir_notify_entry' has been + removed from chdir_notify_register(), chdir_notify_unregister(), and + related callback signatures across several subsystems, simplifying the + API now that trace output no longer uses it. + Fixes since v2.55 ----------------- @@ -669,3 +684,14 @@ Fixes since v2.55 * The help text for the '-l' option of 'git diff' has been updated. (merge 764243bdf4 en/diff-l-opt-help later to maint). + + * 'git -C diff fi' did not complete 'file', which has + been corrected. + (merge 354d1bf3a0 jc/complete-diff-tracked-paths later to maint). + + * 'git -C checkout fi' did not complete 'file', which has + been corrected. + (merge 05e2ab1f31 jc/complete-checkout later to maint). + + * Other code cleanup, docfix, build fix, etc. + (merge 026636128f ss/submittingpatches-typofix later to maint).