git/list[1] front-page[2] threads[3] people[4] search[5] about
 

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

From
D. Ben Knoble <ben.knoble@gmail.com>
Date
Oct 7, 2026, 19:10 UTC
Message-ID
<CALnO6CCmv7PmxW2HTfvD8i6-PCVqMWgasyU9PYhPLXGj1sz=Hg@mail.gmail.com>
In-Reply-To
<CACgTecOm+=vbf50tZNXhcYvRi1ZTsQwbjVoJAbQqs2CmXdJCxg@mail.gmail.com>
Cc: Siddharth Shrimali <r.siddharth.shrimali@gmail.com> who authored
the --drop-filtered code.
On Sun, Oct 4, 2026 at 7:47 PM Coy Geek <coygeek@gmail.com> wrote:
Show 182 quoted lines
>
> 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
Previous: Coy GeekNext: Siddharth Shrimali
Message 2 of 3 in “[BUG] repack --drop-filtered --dry-run writes packs and honors -d in Git 2.56.0”
  1. Coy GeekOct 4, 2026
  2. D. Ben KnobleOct 7, 2026
  3. Siddharth ShrimaliOct 8, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.