Skip to content

send-pack: avoid resending unchanged objects after rebasing - #2281

Open
mstrap wants to merge 3 commits into
gitgitgadget:masterfrom
mstrap:send-pack-inspect-haves
Open

mstrap wants to merge 3 commits into
gitgitgadget:masterfrom
mstrap:send-pack-inspect-haves

Conversation

@mstrap

@mstrap mstrap commented Oct 26, 2026 •

Copy link
Copy Markdown

A common feature-branch workflow is to push a branch, rebase it onto an
updated upstream, and force-push the rewritten commits. Large blobs added
or changed on the feature branch may survive the rebase unchanged.
Although the receiver already has those blobs through the old branch
tip, Git can pack and send them again. This can turn a small history
rewrite into a large upload.

This series teaches send-pack to request inspection of the old tip's
tree before packing, so more receiver-known objects can be excluded.

The additional inspection is enabled only for forced non-fast-forward
updates of existing branches with locally available old tips. Ordinary
fast-forward pushes do not enable it, including ones made with --force.
No server or wire-protocol change is needed.

The old tip is already a negative revision, but it may lie outside the
boundary visited by the ordinary revision walk. Its tree is then not
necessarily inspected. Passing it to pack-objects as --inspect-have lets
us compare its root tree with the outgoing trees using the existing
sparse tree marking. This excludes additional receiver-known objects
without walking the old tip's commit history, and prunes identical and
receiver-only subtrees.

Shallow pushes keep their existing aggressive edge handling. Partial
clones skip the extra tree inspection to avoid fetching missing objects,
and successful bitmap enumeration keeps its existing path. Old tips are
resolved in pack-objects, where replacement refs are already disabled.

I compared upstream 7e34d8174dcda3435d1404c9a30bef46afe6c13e with the
patched source using trees from Chromium commit
5b05cc16e960a021389ef9b543ab38a037c38d60. A depth-two
clone supplies the objects; the measured fixture has a complete,
non-shallow history with a common base and sibling receiver/outgoing
commits.

Both builds used GCC 15.2.0 and -g -O2 -Wall on an Intel i7-13700H under
WSL2. Measurements used a warm cache, sparse enumeration, no bitmaps,
one packing thread, two warm-up pairs and 15 measured pairs with
alternating build order. Values below are upstream/patched; total wall
times are medians in milliseconds, including process startup.

Case Objects Pack bytes Total wall ms
Reword 3 / 1 421 / 215 2.776 / 1.338
Single file 6 / 5 667 / 553 3.542 / 2.744
~99% of files 552,724 / 552,723 58,082,988 / 58,082,873 4,525.094 / 4,883.623

In the single-file case, enumeration increases from 0.758 to 0.982 ms,
but pack preparation decreases from 1.637 to 0.458 ms. In the last
case, 506,468 regular files receive distinct small replacement blobs;
total packing time increases by about 8%, saving only 115 pack bytes.

These are isolated local pack-objects measurements, not end-to-end
pushes: network transfer and receiving-server work are excluded. Where
redundant large blobs materially inflate the pack, avoiding their
transfer should further reduce end-to-end push time, especially on
slower connections; that benefit was not measured here.

Most objects in this fixture are already excluded through the common
base, so the pack-size savings are small. The widespread-change case
tests inspection overhead on Chromium's directory structure, rather
than realistic compression of a whole-project rewrite. Feedback on this
cost and the scope of the inspection would be particularly useful.

Local verification passed t5322-pack-objects-sparse.sh (19 tests) and
t5516-fetch-push.sh (132 tests). Coverage includes identical trees,
shared blobs under different trees, retained changed objects, subtree
pruning, partial clones and local replacement refs.

cc: Marc Strapetz marc.strapetz@syntevo.com

A forced push of a rebased branch can resend an unchanged blob even
though the receiver has it through the advertised old tip.

Add object-count tests for --force, --force-with-lease and forced
refspecs. Confirm that the blob is unchanged by the rebase and record
the current three-object pack: the rebased commit, its tree and the
unchanged blob. Disable push bitmaps to exercise normal enumeration.

Also cover local replacement refs, which must not cause objects
missing from the receiver to be omitted.

Assisted-by: GPT-6 Astra, GPT-6.1 Sol
Signed-off-by: Marc Strapetz <marc.strapetz@syntevo.com>
A receiver-known commit can be excluded from a revision walk without
becoming a boundary commit. Its tree then need not be inspected, so
objects shared with the outgoing history can still be packed.

Accept --inspect-have <commit> lines in --revs input and carry the
selected tips through the object database pack-generation options.
Compare their root trees with the outgoing tips using the existing
sparse tree marking. This identifies additional receiver-known
objects without walking the selected commits' histories and avoids
descending into identical or receiver-only subtrees.

Resolve the selected commits in pack-objects, where replacement refs
are already disabled, so local replacements cannot substitute trees
the receiver does not have. Keep successful bitmap enumeration on its
existing path and skip the optional inspection in partial clones.

Cover identical root trees, shared blobs under different roots,
retained changed objects, subtree pruning, non-commit input, partial
clone fallback and shared objects in earlier outgoing commits.

Assisted-by: GPT-6 Astra, GPT-6.1 Sol
Signed-off-by: Marc Strapetz <marc.strapetz@syntevo.com>
Rebasing a pushed branch can put its old tip outside the boundary
traversed by pack-objects. Even with that tip among the negative
revisions, unchanged blobs may then be sent again.

For forced non-fast-forward branch updates, pass locally available
advertised old tips as haves_to_inspect. Limit this to refs selected
for sending; leave fast-forward and rejected updates out. Restrict
the hints to branches, which name commits, and skip shallow
repositories, whose thin-pack pushes already inspect negative tips
with --objects-edge-aggressive.

Update the rebase tests to expect two packed objects instead of
three: the rebased commit and its tree. The unchanged blob is
already present on the receiver.

Assisted-by: GPT-6 Astra, GPT-6.1 Sol
Signed-off-by: Marc Strapetz <marc.strapetz@syntevo.com>
@mstrap

mstrap commented Oct 26, 2026

Copy link
Copy Markdown
Author

/preview

@gitgitgadget

gitgitgadget Bot commented Oct 26, 2026

Copy link
Copy Markdown

Preview email sent as pull.2281.git.1793029243.gitgitgadget@gmail.com

@mstrap

mstrap commented Oct 26, 2026

Copy link
Copy Markdown
Author

/submit

@gitgitgadget

gitgitgadget Bot commented Oct 26, 2026

Copy link
Copy Markdown

Submitted as pull.2281.git.1793029565.gitgitgadget@gmail.com

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-2281/mstrap/send-pack-inspect-haves-v1

To fetch this version to local tag pr-2281/mstrap/send-pack-inspect-haves-v1:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-2281/mstrap/send-pack-inspect-haves-v1

@gitgitgadget

gitgitgadget Bot commented Oct 26, 2026

Copy link
Copy Markdown

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

"Marc Strapetz via GitGitGadget" <gitgitgadget@gmail.com> writes:

> A common feature-branch workflow is to push a branch, rebase it onto an
> updated upstream, and force-push the rewritten commits. Large blobs added or
> changed on the feature branch may survive the rebase unchanged. Although the
> receiver already has those blobs through the old branch tip, Git can pack
> and send them again. This can turn a small history rewrite into a large
> upload.

This sounds like the exact problem push negotiation was designed to
address.

@gitgitgadget

gitgitgadget Bot commented Oct 26, 2026

Copy link
Copy Markdown

Marc Strapetz wrote on the Git mailing list (how to reply to this email):

On 26/10/2026 18:26, Junio C Hamano wrote:
> This sounds like the exact problem push negotiation was designed to
> address.

Thanks for pointing this out. This series aims to optimize Git's default
packing for forced non-fast-forward updates, without adding a
negotiation phase. I should have explained that scope more clearly in
the cover letter:

    `pack-objects` can resend unchanged objects from an advertised old
    branch tip even though `send-pack` already supplies that tip as an
    exclusion. This series improves default packing for forced updates
    by applying the existing sparse tree marking to selected old tips.
    It targets this inefficiency without the additional negotiation
    exchanges of `push.negotiate` or the broader recursive tree marking
    of `--objects-edge-aggressive`. Neither is enabled by default for
    ordinary pushes from non-shallow repositories. This series targets
    a narrower opportunity within that default behavior.

    A divergent old tip can lie outside the boundary reached by the
    ordinary revision walk. Passing it to `pack-objects` as
    `--inspect-have` lets us compare its root tree with the outgoing
    trees, pruning identical and receiver-only subtrees. The additional
    inspection is limited to forced non-fast-forward updates of
    existing branches with locally available old tips. Large blobs
    retained across a rebase are a particularly costly example of the
    redundant transfer this avoids.

I would also add the following section at the end to explain how this
relates to the existing optimizations:

    `push.negotiate` discovers additional common commits through a
    separate fetch negotiation, potentially requiring multiple protocol
    round trips. This can reduce the pack when ref advertisement alone
    is insufficient, but adds latency even when the advertised tips
    already suffice. For small pushes that latency can outweigh the
    transfer savings. Even if negotiation achieves the same pack-size
    reduction, improving the default packing path avoids the need to
    pay that additional protocol cost.

    `--objects-edge-aggressive` already considers excluded tips beyond
    the ordinary walk's boundary, but recursively marks their trees.
    The decision to enable it automatically only for shallow pushes was
    made for performance reasons: the 2014 series [1] records a
    regression from 4.785 to 23.322 seconds with 22,400 refs [2], and
    keeps non-shallow repositories on `--objects-edge` for better
    performance in the general case [3]. This series instead limits
    inspection to the old tips of branches being forcibly updated and
    uses sparse comparison to prune identical and receiver-only
    subtrees.

    The additional inspection requires no protocol exchange or walk of
    the old tip's commit history. Ordinary fast-forward pushes keep
    their existing path, including those made with `--force`. No server
    or wire-protocol change is needed.

    [1]
https://lore.kernel.org/git/1419462340-769147-3-git-send-email-sandals@crustytoothpaste.net/T/
    [2]
https://github.com/git/git/commit/1684c1b219e02c91655ad929f752f4f864c72faf
    [3]
https://github.com/git/git/commit/2dacf26d0985521c0f30e535963a45257b63ea21

@gitgitgadget

gitgitgadget Bot commented Oct 26, 2026

Copy link
Copy Markdown

User Marc Strapetz <marc.strapetz@syntevo.com> has been added to the cc: list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant