From: D. Ben Knoble Date: Wed, 07 Oct 2026 19:10:24 GMT Subject: Re: [BUG] repack --drop-filtered --dry-run writes packs and honors -d in Git 2.56.0 Message-ID: In-Reply-To: Cc: Siddharth Shrimali who authored the --drop-filtered code. On Sun, Oct 4, 2026 at 7:47 PM Coy Geek wrote: > > fix(repack): --drop-filtered --dry-run writes packs and deletes old > packs with -d > > ## Summary > > On Git 2.56.0, `git repack -a --filter=blob:limit=1m --drop-filtered > --dry-run` prints the candidate blob but also writes a new promisor > pack and its sidecar files. Adding explicit `-d` also removes the old > redundant packs and their sidecar files. Both commands exit > successfully despite the documented promise to list candidates without > rebuilding packs or deleting anything. > > This makes the advertised preview mutate repository storage. The > tested blob remains available after both commands; this report does > not demonstrate loss of object content. > > ## Steps to reproduce > > The following Python 3 script was executed with Git 2.56.0 on macOS. > It requires `git` on `PATH`, creates only disposable local > repositories, disables ambient Git configuration and hooks, and uses a > synthetic commit identity. It creates a 2,200,000-byte historical blob > absent from the current index, makes a filtered clone through a local > promisor remote, fetches that blob into a promisor pack, and snapshots > the pack directory before each preview. Each variant uses its own > fresh partial clone. The script removes its fixture after a successful > run. > > ```python > import hashlib > import os > from pathlib import Path > import subprocess > import shutil > import tempfile > > work = Path(tempfile.mkdtemp(prefix='agent-work.', dir='/tmp')) > env = {k: v for k, v in os.environ.items() if not k.startswith('GIT_')} > env.update(GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL=os.devnull, > LC_ALL='C', GIT_TERMINAL_PROMPT='0') > > def git(repo, *args): > command = ['git', '-c', 'gc.auto=0', '-c', 'core.hooksPath=' + os.devnull, > '-c', 'user.name=Fixture', '-c', > 'user.email=fixture@example.invalid', > '-C', str(repo), *args] > return subprocess.run(command, env=env, check=True, text=True, > stdout=subprocess.PIPE, > stderr=subprocess.PIPE).stdout.strip() > > def inventory(repo): > packdir = repo / '.git' / 'objects' / 'pack' > return {p.name: hashlib.sha256(p.read_bytes()).hexdigest() > for p in packdir.iterdir() if p.is_file()} > > print(git(work, '--version')) > source = work / 'source' > source.mkdir() > git(source, 'init', '--template=', '-b', 'main') > (source / 'large.bin').write_bytes(b'LARGE BLOB\n' * 200000) > git(source, 'add', 'large.bin') > git(source, 'commit', '-m', 'Add historical blob') > blob = git(source, 'rev-parse', 'HEAD:large.bin') > git(source, 'rm', 'large.bin') > (source / 'tip.txt').write_text('tip\n') > git(source, 'add', 'tip.txt') > git(source, 'commit', '-m', 'Remove historical blob') > remote = work / 'remote.git' > git(work, 'clone', '--bare', str(source), str(remote)) > git(remote, 'config', 'uploadpack.allowFilter', 'true') > > for label, extra in [('dry-run', []), ('dry-run -d', ['-d'])]: > repo = work / ('clone-delete' if extra else 'clone-preview') > git(work, '-c', 'protocol.file.allow=always', 'clone', '--filter=blob:none', > remote.as_uri(), str(repo)) > assert git(repo, 'cat-file', '-s', blob) == '2200000' > before = inventory(repo) > output = git(repo, 'repack', '-a', *extra, '--filter=blob:limit=1m', > '--drop-filtered', '--dry-run') > after = inventory(repo) > added = sorted(after.keys() - before.keys()) > removed = sorted(before.keys() - after.keys()) > changed = sorted(k for k in before.keys() & after.keys() if > before[k] != after[k]) > print(label + ': exit 0') > print('candidate=' + output) > print('pack directory: before=%d after=%d added=%d removed=%d changed=%d' % > (len(before), len(after), len(added), len(removed), len(changed))) > print('added extensions=' + ','.join(sorted(Path(k).suffix for k in added))) > print('removed extensions=' + ','.join(sorted(Path(k).suffix for k > in removed))) > print('candidate size after=' + git(repo, 'cat-file', '-s', blob)) > shutil.rmtree(work) > ``` > > ## Expected behavior > > The Git 2.56.0 `git-repack` manual says `--dry-run` should "List the > objects that would be dropped, one object ID per line, without > rebuilding any pack or deleting anything." > > For both commands, Git should print the candidate blob and leave the > existing pack files and their contents unchanged. Explicit `-d` should > not cause deletion during this dry run. The fixture satisfies the > documented prerequisites: `-a`, a supported `blob:limit` filter, a > configured promisor remote, no operation in progress, and a candidate > absent from the current index. > > ## Actual behavior > > The executed script produced this output. The counts include regular > files in `objects/pack`, and `changed` compares SHA-256 content hashes > for names present before and after. > > ```text > git version 2.56.0 > dry-run: exit 0 > candidate=faed553c77de798540bd1edc898e174bf4436b28 > pack directory: before=12 after=16 added=4 removed=0 changed=0 > added extensions=.idx,.pack,.promisor,.rev > removed extensions= > candidate size after=2200000 > dry-run -d: exit 0 > candidate=faed553c77de798540bd1edc898e174bf4436b28 > pack directory: before=12 after=4 added=4 removed=12 changed=0 > added extensions=.idx,.pack,.promisor,.rev > removed extensions=.idx,.idx,.idx,.pack,.pack,.pack,.promisor,.promisor,.promisor,.rev,.rev,.rev > candidate size after=2200000 > ``` > > The ordinary preview added four files, one each with `.pack`, `.idx`, > `.promisor`, and `.rev` extensions. The preview with explicit `-d` > added the same four types and deleted the 12 previous pack and sidecar > files. Both invocations printed the expected candidate object ID and > exited 0. A subsequent `cat-file -s` still reported the blob's > original size in both clones. > > This report records one successful invocation of each variant in the > final script execution. A preceding fixture execution reproduced the > same counts. These observations establish the Git 2.56.0 behavior; > current development Git was not built or executed for this report. > > ## Evidence > > - Expected source: Git 2.56.0 [Documentation/git-repack.adoc, the > `--dry-run` option](https://github.com/git/git/blob/v2.56.0/Documentation/git-repack.adoc#L217-L220), > which promises no pack rebuilding or deletion. > - Failure source: The inline script and its captured output in Actual > behavior. The before/after pack inventories show four added files for > both variants and 12 deleted files when explicit `-d` is present. > - Evidence provenance: observed > - Local verification: reproduced > - Reproduction completeness: complete > > The primary violation evidence is execution of the inline fixture with > Git 2.56.0. Both commands exited 0 with the pack-directory changes > shown above. The manual and source links provide the contract and > supporting static evidence. > > The release source at [builtin/repack.c, lines > 356-394](https://github.com/git/git/blob/v2.56.0/builtin/repack.c#L356-L394) > guards the implicit `delete_redundant` setting with `!dry_run`, then > prints candidates when `dry_run` is set. Execution continues beyond > that block. This is a source breadcrumb consistent with the observed > results, rather than a required implementation change. > > The original implementation discussion also describes dry run as > leaving the repository unchanged: [v5 > 1/6](https://lore.kernel.org/git/20260813200830.84348-2-r.siddharth.shrimali@gmail.com/) > and [v5 5/6](https://lore.kernel.org/git/20260813200830.84348-6-r.siddharth.shrimali@gmail.com/). > > ## Restoration check > > Run the same fixture after a fix. Both preview variants should still > print the candidate object ID and exit successfully, with `added=0 > removed=0 changed=0` and equal before/after pack-directory counts. > Retaining the candidate output distinguishes a working preview from > simply skipping the operation. The cached blob should remain > available. Use the ordinary command and the explicit `-d` variant as > separate checks of the same no-write contract. > -- D. Ben Knoble