{"thread":{"id":"66462","subject":"[BUG] repack --drop-filtered --dry-run writes packs and honors -d in Git 2.56.0","startedAt":"2026-10-04T23:47:50Z","lastAt":"2026-10-04T23:47:50Z","messageCount":1,"participants":["Coy Geek"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"554135","messageId":"CACgTecOm+=vbf50tZNXhcYvRi1ZTsQwbjVoJAbQqs2CmXdJCxg@mail.gmail.com","threadId":"66462","inReplyTo":null,"subject":"[BUG] repack --drop-filtered --dry-run writes packs and honors -d in Git 2.56.0","fromName":"Coy Geek","fromEmail":"coygeek@gmail.com","sentAt":"2026-10-04T23:47:37Z","receivedAt":"2026-10-04T23:47:50Z","isPatch":false,"body":"fix(repack): --drop-filtered --dry-run writes packs and deletes old\npacks with -d\n\n## Summary\n\nOn Git 2.56.0, `git repack -a --filter=blob:limit=1m --drop-filtered\n--dry-run` prints the candidate blob but also writes a new promisor\npack and its sidecar files. Adding explicit `-d` also removes the old\nredundant packs and their sidecar files. Both commands exit\nsuccessfully despite the documented promise to list candidates without\nrebuilding packs or deleting anything.\n\nThis makes the advertised preview mutate repository storage. The\ntested blob remains available after both commands; this report does\nnot demonstrate loss of object content.\n\n## Steps to reproduce\n\nThe following Python 3 script was executed with Git 2.56.0 on macOS.\nIt requires `git` on `PATH`, creates only disposable local\nrepositories, disables ambient Git configuration and hooks, and uses a\nsynthetic commit identity. It creates a 2,200,000-byte historical blob\nabsent from the current index, makes a filtered clone through a local\npromisor remote, fetches that blob into a promisor pack, and snapshots\nthe pack directory before each preview. Each variant uses its own\nfresh partial clone. The script removes its fixture after a successful\nrun.\n\n```python\nimport hashlib\nimport os\nfrom pathlib import Path\nimport subprocess\nimport shutil\nimport tempfile\n\nwork = Path(tempfile.mkdtemp(prefix='agent-work.', dir='/tmp'))\nenv = {k: v for k, v in os.environ.items() if not k.startswith('GIT_')}\nenv.update(GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL=os.devnull,\n           LC_ALL='C', GIT_TERMINAL_PROMPT='0')\n\ndef git(repo, *args):\n    command = ['git', '-c', 'gc.auto=0', '-c', 'core.hooksPath=' + os.devnull,\n               '-c', 'user.name=Fixture', '-c',\n'user.email=fixture@example.invalid',\n               '-C', str(repo), *args]\n    return subprocess.run(command, env=env, check=True, text=True,\n                          stdout=subprocess.PIPE,\nstderr=subprocess.PIPE).stdout.strip()\n\ndef inventory(repo):\n    packdir = repo / '.git' / 'objects' / 'pack'\n    return {p.name: hashlib.sha256(p.read_bytes()).hexdigest()\n            for p in packdir.iterdir() if p.is_file()}\n\nprint(git(work, '--version'))\nsource = work / 'source'\nsource.mkdir()\ngit(source, 'init', '--template=', '-b', 'main')\n(source / 'large.bin').write_bytes(b'LARGE BLOB\\n' * 200000)\ngit(source, 'add', 'large.bin')\ngit(source, 'commit', '-m', 'Add historical blob')\nblob = git(source, 'rev-parse', 'HEAD:large.bin')\ngit(source, 'rm', 'large.bin')\n(source / 'tip.txt').write_text('tip\\n')\ngit(source, 'add', 'tip.txt')\ngit(source, 'commit', '-m', 'Remove historical blob')\nremote = work / 'remote.git'\ngit(work, 'clone', '--bare', str(source), str(remote))\ngit(remote, 'config', 'uploadpack.allowFilter', 'true')\n\nfor label, extra in [('dry-run', []), ('dry-run -d', ['-d'])]:\n    repo = work / ('clone-delete' if extra else 'clone-preview')\n    git(work, '-c', 'protocol.file.allow=always', 'clone', '--filter=blob:none',\n        remote.as_uri(), str(repo))\n    assert git(repo, 'cat-file', '-s', blob) == '2200000'\n    before = inventory(repo)\n    output = git(repo, 'repack', '-a', *extra, '--filter=blob:limit=1m',\n                 '--drop-filtered', '--dry-run')\n    after = inventory(repo)\n    added = sorted(after.keys() - before.keys())\n    removed = sorted(before.keys() - after.keys())\n    changed = sorted(k for k in before.keys() & after.keys() if\nbefore[k] != after[k])\n    print(label + ': exit 0')\n    print('candidate=' + output)\n    print('pack directory: before=%d after=%d added=%d removed=%d changed=%d' %\n          (len(before), len(after), len(added), len(removed), len(changed)))\n    print('added extensions=' + ','.join(sorted(Path(k).suffix for k in added)))\n    print('removed extensions=' + ','.join(sorted(Path(k).suffix for k\nin removed)))\n    print('candidate size after=' + git(repo, 'cat-file', '-s', blob))\nshutil.rmtree(work)\n```\n\n## Expected behavior\n\nThe Git 2.56.0 `git-repack` manual says `--dry-run` should \"List the\nobjects that would be dropped, one object ID per line, without\nrebuilding any pack or deleting anything.\"\n\nFor both commands, Git should print the candidate blob and leave the\nexisting pack files and their contents unchanged. Explicit `-d` should\nnot cause deletion during this dry run. The fixture satisfies the\ndocumented prerequisites: `-a`, a supported `blob:limit` filter, a\nconfigured promisor remote, no operation in progress, and a candidate\nabsent from the current index.\n\n## Actual behavior\n\nThe executed script produced this output. The counts include regular\nfiles in `objects/pack`, and `changed` compares SHA-256 content hashes\nfor names present before and after.\n\n```text\ngit version 2.56.0\ndry-run: exit 0\ncandidate=faed553c77de798540bd1edc898e174bf4436b28\npack directory: before=12 after=16 added=4 removed=0 changed=0\nadded extensions=.idx,.pack,.promisor,.rev\nremoved extensions=\ncandidate size after=2200000\ndry-run -d: exit 0\ncandidate=faed553c77de798540bd1edc898e174bf4436b28\npack directory: before=12 after=4 added=4 removed=12 changed=0\nadded extensions=.idx,.pack,.promisor,.rev\nremoved extensions=.idx,.idx,.idx,.pack,.pack,.pack,.promisor,.promisor,.promisor,.rev,.rev,.rev\ncandidate size after=2200000\n```\n\nThe ordinary preview added four files, one each with `.pack`, `.idx`,\n`.promisor`, and `.rev` extensions. The preview with explicit `-d`\nadded the same four types and deleted the 12 previous pack and sidecar\nfiles. Both invocations printed the expected candidate object ID and\nexited 0. A subsequent `cat-file -s` still reported the blob's\noriginal size in both clones.\n\nThis report records one successful invocation of each variant in the\nfinal script execution. A preceding fixture execution reproduced the\nsame counts. These observations establish the Git 2.56.0 behavior;\ncurrent development Git was not built or executed for this report.\n\n## Evidence\n\n- Expected source: Git 2.56.0 [Documentation/git-repack.adoc, the\n`--dry-run` option](https://github.com/git/git/blob/v2.56.0/Documentation/git-repack.adoc#L217-L220),\nwhich promises no pack rebuilding or deletion.\n- Failure source: The inline script and its captured output in Actual\nbehavior. The before/after pack inventories show four added files for\nboth variants and 12 deleted files when explicit `-d` is present.\n- Evidence provenance: observed\n- Local verification: reproduced\n- Reproduction completeness: complete\n\nThe primary violation evidence is execution of the inline fixture with\nGit 2.56.0. Both commands exited 0 with the pack-directory changes\nshown above. The manual and source links provide the contract and\nsupporting static evidence.\n\nThe release source at [builtin/repack.c, lines\n356-394](https://github.com/git/git/blob/v2.56.0/builtin/repack.c#L356-L394)\nguards the implicit `delete_redundant` setting with `!dry_run`, then\nprints candidates when `dry_run` is set. Execution continues beyond\nthat block. This is a source breadcrumb consistent with the observed\nresults, rather than a required implementation change.\n\nThe original implementation discussion also describes dry run as\nleaving the repository unchanged: [v5\n1/6](https://lore.kernel.org/git/20260813200830.84348-2-r.siddharth.shrimali@gmail.com/)\nand [v5 5/6](https://lore.kernel.org/git/20260813200830.84348-6-r.siddharth.shrimali@gmail.com/).\n\n## Restoration check\n\nRun the same fixture after a fix. Both preview variants should still\nprint the candidate object ID and exit successfully, with `added=0\nremoved=0 changed=0` and equal before/after pack-directory counts.\nRetaining the candidate output distinguishes a working preview from\nsimply skipping the operation. The cached blob should remain\navailable. Use the ordinary command and the explicit `-d` variant as\nseparate checks of the same no-write contract.\n"}]}