Skip to content

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

Open
DanielHabenicht wants to merge 1 commit into
rolandjitsu:mainfrom
DanielHabenicht:fix/pr-merge-ref-fetch
Open

fix(git): serve want-by-sha for unadvertised refs (PR merge commits)#27
DanielHabenicht wants to merge 1 commit 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 96.13734% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.20%. Comparing base (c4b9d89) to head (6e36d14).

Files with missing lines Patch % Lines
src/git.rs 96.06% 9 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #27      +/-   ##
==========================================
+ Coverage   96.12%   96.20%   +0.07%     
==========================================
  Files           6        6              
  Lines        1650     1869     +219     
==========================================
+ Hits         1586     1798     +212     
- Misses         64       71       +7     

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

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

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

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