Skip to content

fix(git): serve want-by-sha for unadvertised refs (PR merge commits) - #27

Open
DanielHabenicht wants to merge 3 commits into
rolandjitsu:mainfrom
DanielHabenicht:fix/pr-merge-ref-fetch
Open

DanielHabenicht wants to merge 3 commits into
rolandjitsu:mainfrom
DanielHabenicht:fix/pr-merge-ref-fetch

Conversation

@DanielHabenicht

Copy link
Copy Markdown
Contributor

Before serving an upload-pack RPC, parse the client's want lines and, for any SHA missing from the mirror, fetch it from upstream on demand and pin it under a reserved refs/proxy-wants/<sha> ref. Pinning makes the object a valid want tip (so upload-pack serves it) and keeps it from git gc. The namespace is hidden from the ref advertisement (uploadpack.hideRefs) yet still honored as a want tip, and excluded from fetch --prune so pins survive periodic refreshes.

Relies on the upstream serving arbitrary SHAs (GitHub's allowAnySHA1InWant); an upstream that refuses leaves the request to fail as before - no regression. Ordinary branch/tag clones pay only a single cheap cat-file check, never an extra upstream call.

What kind of change does this PR introduce?

  • fix
  • feat
  • refactor
  • perf
  • docs
  • test
  • build / ci
  • chore

Summary

actions/checkout on a pull_request event fetches the synthetic merge commit by bare SHA. That commit lives only under GitHub's unadvertised refs/pull//merge, so clone --mirror never captured it and the mirror's upload-pack rejected the want with "fatal: not our ref ", breaking CI behind the proxy.

Tests

  • Added / updated tests (unit, plus integration where it fits)
  • Not relevant, because: ...

Checklist

  • CI is green locally: cargo fmt --all --check, cargo clippy --all-targets --all-features --locked -- -D warnings, cargo test --all-features
  • Commits follow Conventional Commits; AI-assisted commits carry an Assisted-by: trailer (see CONTRIBUTING.md / AGENTS.md)
  • Preserves the read-only, pull-only invariant (no push or proactive replication to upstream)
  • [-] Docs / README updated if behavior or flags changed

Breaking change?
If yes, describe the impact and the migration path (flags / env, on-disk cache layout).

Comment thread src/git.rs
@codecov

codecov Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.43590% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.80%. Comparing base (9cf2347) to head (5a1eeb0).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/git.rs 97.39% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #27      +/-   ##
==========================================
+ Coverage   96.69%   96.80%   +0.10%     
==========================================
  Files           6        6              
  Lines        1846     2065     +219     
==========================================
+ Hits         1785     1999     +214     
- Misses         61       66       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread src/git.rs
Comment thread src/git.rs Outdated
Comment thread src/git.rs Outdated
Comment thread src/git.rs Outdated
@rolandjitsu

Copy link
Copy Markdown
Owner

Thanks for your contribution @DanielHabenicht ! I left a few comments. You also need to fixup the commit msg/body.

Comment thread src/git.rs
// would be pruned straight away (see `prune_wants`), so fetching it is pure
// waste. The excess is dropped and upload-pack rejects those wants.
if self.cfg.max_wants > 0 && missing.len() > self.cfg.max_wants {
tracing::warn!(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The risk is that if we don't surface this to the user, they'll expect it to work and might be confused why the shas are not available. Someone will need to check the logs to see what's going on.

But, as long as it's a documented behavior, it would be ok.

Comment thread src/git.rs Outdated

/// Collect an iterator of oids into a `HashSet` for order-insensitive assertions
/// against `parse_wants`.
fn want_set<I: IntoIterator<Item = String>>(oids: I) -> HashSet<String> {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Place helper fns at the bottom, after all tests.

Comment thread src/git.rs
.take_while(u8::is_ascii_hexdigit)
.collect();
// sha1 (40) or sha256 (64); ignore anything else (e.g. a stray token).
if oid.len() == 40 || oid.len() == 64 {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could ignore parsing oids over the max_wants here. Then you don't need to truncate and check for missing in ensure_wanted_oids.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am actually not that deep into the code, but Claude told me that this is would break the new feature as it would lead to exceeding the max_wants:

  • parse_wants collects all wants, including ones the mirror already has. The cap in ensure_wanted_oids is applied to missing — the subset the mirror lacks and must fetch/pin.
  • A normal git clone sends a want for every advertised ref. A repo with >100 refs (common with many tags) already exceeds the default max_wants=100 on wants alone — but all of those are present, so missing is empty.
  • If we capped at parse time, the one genuinely-missing unadvertised SHA (the PR merge commit) could be dropped as the 101st want, and the core feature would silently fail for large repos.

If you have time you can take over from here.

@rolandjitsu

Copy link
Copy Markdown
Owner

@DanielHabenicht do you think you'll have the time to address the comments?

@rolandjitsu

Copy link
Copy Markdown
Owner

@DanielHabenicht do you think you'll have the time to address the comments?

I'd be happy to pick this up and wrap it up if you're too busy.

@DanielHabenicht

Copy link
Copy Markdown
Contributor Author

Sorry, so much todo.
I pushed an update with some of your comments fixed.

You can take it from here.

DanielHabenicht and others added 2 commits September 21, 2026 12:00
actions/checkout on a pull_request event fetches the synthetic merge
commit by bare SHA. That commit lives only under GitHub's unadvertised
refs/pull/<n>/merge, so the mirror clone never captured it and
upload-pack rejected the want with "not our ref", breaking CI behind
the proxy.

Before serving, ensure_fresh scans the upload-pack request for want
lines and, for any SHA missing from the mirror, fetches it from
upstream and pins it under a reserved refs/proxy-wants/<sha> ref.
Pinning makes the object a valid want tip (so upload-pack serves it)
and keeps it from git gc. The namespace is hidden from the
advertisement yet still honored as a want tip, and excluded from prune
so pins survive periodic refreshes.

Because those pins are excluded from prune and gc, --max-wants bounds
how many a mirror retains: the oldest are pruned beyond the cap so they
cannot accumulate without bound.

Relies on the upstream serving arbitrary SHAs (GitHub's
allowAnySHA1InWant); an upstream that refuses leaves the want
unsatisfied for upload-pack to reject. Ordinary branch/tag clones pay
only a cheap cat-file check, never an extra upstream call.

Assisted-by: Claude:claude-opus-4-8
Note in the README that per-request wants beyond --max-wants are left
unserved with a warning, and move the want_set test helper below all
tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add e2e coverage for unlimited pins (max_wants=0) and a tolerated
local object-check failure, exercising the uncapped and best-effort
error branches of the want-by-sha path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants