# [BUG] repack --drop-filtered --dry-run writes packs and honors -d in Git 2.56.0

1 messages from 2026-10-04 to 2026-10-04. Participants: Coy Geek.
Thread: https://gitlist.dev/t/66462

## Coy Geek, 2026-10-04 23:47

Subject: [BUG] repack --drop-filtered --dry-run writes packs and honors -d in Git 2.56.0
Message-ID: <CACgTecOm+=vbf50tZNXhcYvRi1ZTsQwbjVoJAbQqs2CmXdJCxg@mail.gmail.com>

```
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.

```
