Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

A push no longer changes `refs/pull/*`, as on GitHub: they are hidden refs that only g1t moves. A push that names one is refused for that ref alone, `deny updating a hidden ref`, and the rest of it goes to the store, whose report gains the refusals (on channel 1 when git asked for side-band), so `git push --mirror` of a repository cloned with `--mirror` from GitHub moves every branch and tag and reports only its pull request refs as rejected; one that names nothing else, or an `--atomic` one, is answered without reaching the store. The receive-pack advertisement leaves them out, as GitHub's does, so a mirror push does not try to delete them; fetches and `git clone --mirror` still get them. g1t's own updates of `refs/pull/<number>/head` and `/merge` go straight to the store and are untouched. The git guide says so.

Claude-Session:
https://claude.ai/code/session_01PnwTRSNaC7vEHTAuBzjhrn
Claudeagent and Claudeagentcommitted Parentc5c643bBrowse files
4 files+462−60/4 viewed
+27−0
197197 made, whoever asked for it. Pushes to a fork
198198 update the pull request's head commit on its page.
199199
200+## Pull request refs
201+
202+g1t keeps `refs/pull/<number>/head` and `refs/pull/<number>/merge` in a
203+repository for each open pull request (see
204+[What a pull request's run builds](/guides/actions/#what-a-pull-requests-run-builds)). You can
205+fetch them like any branch, and a `git clone --mirror` gets them, as on
206+GitHub:
207+
208+```sh
209+git fetch origin refs/pull/12/head
210+```
211+
212+They are hidden refs, as on GitHub: only g1t moves them. A push leaves
213+them out of what it reads from the repository, and a push that names one
214+is refused for that ref alone, with the rest of the push applied. So
215+`git push --mirror` of a repository you cloned with `--mirror` from
216+GitHub moves every branch and tag and reports only the pull request refs:
217+
218+```text
219+ ! [remote rejected] refs/pull/1/head -> refs/pull/1/head (deny updating a hidden ref)
220+```
221+
222+Git then says some refs failed to push; the branches and tags are there.
223+To leave them out, push branches and tags alone:
224+`git push origin --all && git push origin --tags`. A push with `--atomic`
225+that names one is refused whole.
226+
200227 ## Limits
201228
202229 ### Size limits
+35−6
1212 use worker::wasm_bindgen::JsValue;
1313 use worker::{Fetch, Fetcher, Headers, Method, Request, RequestInit, Response, Result, Url};
1414
15+use crate::hidden_refs;
1516 use crate::meters;
1617 use crate::pack_limits::{PackSizer, Violation};
1718 use crate::push_checks::{Checks, Verdict};
350351 0
351352 }
352353
353−fn pkt_line(payload: &[u8]) -> Vec<u8> {
354+pub(crate) fn pkt_line(payload: &[u8]) -> Vec<u8> {
354355 let mut line = format!("{:04x}", payload.len() + 4).into_bytes();
355356 line.extend_from_slice(payload);
356357 line
409410 /// A report-status as git expects it: inside channel 1 when the client
410411 /// asked for side-band, after `messages` on channel 2, which git prints as
411412 /// `remote:` lines. Without side-band the messages cannot be shown.
412−fn framed(report: Vec<u8>, capabilities: &str, messages: &[String]) -> Vec<u8> {
413+pub(crate) fn framed(report: Vec<u8>, capabilities: &str, messages: &[String]) -> Vec<u8> {
413414 let sideband = capabilities
414415 .split(' ')
415416 .any(|capability| capability.starts_with("side-band"));
679680 /// One packet of a pkt-line stream: data, or a flush (`0000`), delimiter
680681 /// (`0001`) or response-end (`0002`) packet, kept as its four bytes.
681682 #[derive(Debug, PartialEq, Eq)]
682−enum Packet {
683+pub(crate) enum Packet {
683684 Data(Vec<u8>),
684685 Special([u8; 4]),
685686 }
686687
687688 /// The packets in `bytes`, or `None` if it is not a whole pkt-line stream.
688−fn packets(bytes: &[u8]) -> Option<Vec<Packet>> {
689+pub(crate) fn packets(bytes: &[u8]) -> Option<Vec<Packet>> {
689690 let mut out = Vec::new();
690691 let mut position = 0;
691692 while position < bytes.len() {
702703 Some(out)
703704 }
704705
705−fn encode(packets: &[Packet]) -> Vec<u8> {
706+pub(crate) fn encode(packets: &[Packet]) -> Vec<u8> {
706707 let mut out = Vec::new();
707708 for packet in packets {
708709 match packet {
10321033 headers.delete("content-length")?;
10331034 let body = response.bytes().await?;
10341035 let body = with_no_thin(&body).unwrap_or(body);
1036+ // Without the refs g1t keeps, as GitHub hides them from a push.
1037+ let body = hidden_refs::advertisement_without_hidden(&body).unwrap_or(body);
10351038 response = Response::from_bytes(body)?.with_headers(headers);
10361039 }
10371040 if git.endpoint == "git-upload-pack"
10841087 }
10851088 }
10861089 timing.mark("recv");
1090+ // Commands to the refs g1t keeps for itself (`refs/pull/*`) are taken
1091+ // out and refused ref by ref once the store has answered for the rest
1092+ // (hidden_refs.rs). One that leaves nothing to send is answered here.
1093+ let hidden = match hidden_refs::split(&head) {
1094+ hidden_refs::Split::Untouched => Vec::new(),
1095+ hidden_refs::Split::Without { body, hidden } => {
1096+ head = body;
1097+ hidden
1098+ }
1099+ hidden_refs::Split::Refused { report, .. } => {
1100+ if !ended {
1101+ drain(&mut stream).await?;
1102+ }
1103+ return Ok(Push::Declined(report_response(report)?, hidden_refs::HIDDEN_REASON.to_owned()));
1104+ }
1105+ };
10871106 // The workflow gate and the rules of the branches and tags it changes,
10881107 // with push protection alongside for a push read whole that is within
10891108 // the size limits (push_checks.rs). What the rules refuse is refused
11321151 }
11331152 let pushed = pushed_branches(&head);
11341153 let deleted = deleted_refs(&head);
1154+ // The commands as sent, for framing the refusals of hidden refs.
1155+ let reported_head: Vec<u8> = if hidden.is_empty() { Vec::new() } else { head.iter().take(64 * 1024).copied().collect() };
11351156 let mut init = RequestInit::new();
11361157 init.with_method(Method::Post).with_headers(headers);
11371158 let started = g1t_kit::now_ms();
11941215 ms,
11951216 );
11961217 // A push is never tried again: the store may have taken it.
1197− let response = match (answered, failure) {
1218+ let mut response = match (answered, failure) {
11981219 (Ok(response), None) => response,
11991220 (Ok(response), Some(Failure::RateLimited)) => {
12001221 drop(response);
12061227 busy_response(Busy { rate_limited: false, retry_after: 5, read_only: false })?
12071228 }
12081229 };
1230+ if !hidden.is_empty() && response.status_code() == 200 {
1231+ let sideband = commands(&reported_head).1.split(' ').any(|capability| capability.starts_with("side-band"));
1232+ let headers = response.headers().clone();
1233+ headers.delete("content-length")?;
1234+ let answer = response.bytes().await?;
1235+ let answer = hidden_refs::with_refused(&answer, &hidden, sideband).unwrap_or(answer);
1236+ response = Response::from_bytes(answer)?.with_headers(headers);
1237+ }
12091238 Ok(Push::Forwarded(Forwarded {
12101239 response,
12111240 pushed,
+399−0
1+//! Refs g1t keeps for itself, which nobody pushes to: a pull request's
2+//! `refs/pull/<number>/head` and `refs/pull/<number>/merge` (test_merge.rs,
3+//! forks.rs). As on GitHub, they are hidden refs: fetched like any other,
4+//! left out of the advertisement a push reads, and refused in a push with
5+//! `deny updating a hidden ref`, ref by ref, while the rest of the push is
6+//! applied. So `git push --mirror` of a repository cloned with `--mirror`
7+//! from GitHub moves every branch and tag and reports only its
8+//! `refs/pull/*` as rejected.
9+//!
10+//! g1t moves them itself straight on the git store (land.rs `push_ref`),
11+//! never through the git remote people push to, so none of this touches
12+//! that.
13+
14+use crate::git_http::{Packet, encode, framed, packets, pkt_line};
15+
16+/// The refs g1t keeps.
17+pub const HIDDEN_PREFIX: &str = "refs/pull/";
18+
19+/// What git is told for each of them, as GitHub (and git's own
20+/// `receive.hideRefs`) tells it.
21+pub const HIDDEN_REASON: &str = "deny updating a hidden ref";
22+
23+/// What the other refs of an `--atomic` push are told, as git's
24+/// receive-pack tells them.
25+const ATOMIC_REASON: &str = "atomic transaction failed";
26+
27+pub fn is_hidden(name: &str) -> bool {
28+ name.starts_with(HIDDEN_PREFIX)
29+}
30+
31+/// A push's commands with those to hidden refs taken out.
32+#[derive(Debug, PartialEq, Eq)]
33+pub enum Split {
34+ /// No command names a hidden ref: the push goes on as it came.
35+ Untouched,
36+ /// The push without them, to send on, and the refs that were taken
37+ /// out, to report as refused once the store has answered for the rest.
38+ Without { body: Vec<u8>, hidden: Vec<String> },
39+ /// Nothing is sent on: every command names a hidden ref, or the push is
40+ /// atomic and one of them does. The report to answer with.
41+ Refused { report: Vec<u8>, hidden: Vec<String> },
42+}
43+
44+/// One pkt-line of a push's command list, as it came.
45+struct Line {
46+ data: Vec<u8>,
47+ /// The ref, when the line is a command (`<old> <new> <ref>`).
48+ name: Option<String>,
49+}
50+
51+fn command_name(data: &[u8]) -> Option<String> {
52+ let command = data.split(|byte| *byte == 0).next()?;
53+ let command = std::str::from_utf8(command).ok()?.trim_end();
54+ let mut parts = command.splitn(3, ' ');
55+ let (old, new, name) = (parts.next()?, parts.next()?, parts.next()?);
56+ let oid = |value: &str| matches!(value.len(), 40 | 64) && value.bytes().all(|byte| byte.is_ascii_hexdigit());
57+ (oid(old) && oid(new) && !name.is_empty()).then(|| name.to_owned())
58+}
59+
60+/// The command lines at the start of a receive-pack request, up to the
61+/// flush that ends them, and where the rest (push options, the pack)
62+/// starts. `None` when they do not end within `body`.
63+fn command_lines(body: &[u8]) -> Option<(Vec<Line>, usize)> {
64+ let mut lines = Vec::new();
65+ let mut position = 0;
66+ loop {
67+ let header = body.get(position..position + 4)?;
68+ let length = usize::from_str_radix(std::str::from_utf8(header).ok()?, 16).ok()?;
69+ if length == 0 {
70+ return Some((lines, position + 4));
71+ }
72+ let data = body.get(position + 4..position + length)?.to_vec();
73+ position += length;
74+ let name = command_name(&data);
75+ lines.push(Line { data, name });
76+ }
77+}
78+
79+/// The capabilities on the first command, after its NUL, as sent
80+/// (with the newline, when there is one).
81+fn capabilities_of(lines: &[Line]) -> Option<Vec<u8>> {
82+ let first = lines.iter().find(|line| line.name.is_some())?;
83+ let nul = first.data.iter().position(|byte| *byte == 0)?;
84+ Some(first.data[nul + 1..].to_vec())
85+}
86+
87+fn has_capability(capabilities: &[u8], wanted: &str) -> bool {
88+ String::from_utf8_lossy(capabilities)
89+ .split_ascii_whitespace()
90+ .any(|capability| capability == wanted)
91+}
92+
93+/// Takes the commands to hidden refs out of a receive-pack request (see
94+/// [`Split`]). The capabilities stay on the first command that is left.
95+pub fn split(body: &[u8]) -> Split {
96+ let Some((lines, rest)) = command_lines(body) else {
97+ return Split::Untouched;
98+ };
99+ let hidden: Vec<String> = lines
100+ .iter()
101+ .filter_map(|line| line.name.as_deref())
102+ .filter(|name| is_hidden(name))
103+ .map(str::to_owned)
104+ .collect();
105+ if hidden.is_empty() {
106+ return Split::Untouched;
107+ }
108+ let capabilities = capabilities_of(&lines).unwrap_or_default();
109+ let caps_text = String::from_utf8_lossy(&capabilities).trim().to_owned();
110+ let kept: Vec<&Line> = lines
111+ .iter()
112+ .filter(|line| line.name.as_deref().is_some_and(|name| !is_hidden(name)))
113+ .collect();
114+ if kept.is_empty() || has_capability(&capabilities, "atomic") {
115+ let mut report = pkt_line(b"unpack ok\n");
116+ for line in lines.iter().filter_map(|line| line.name.as_deref()) {
117+ let reason = if is_hidden(line) { HIDDEN_REASON } else { ATOMIC_REASON };
118+ report.extend(pkt_line(format!("ng {line} {reason}\n").as_bytes()));
119+ }
120+ report.extend_from_slice(b"0000");
121+ return Split::Refused { report: framed(report, &caps_text, &[]), hidden };
122+ }
123+ let mut out = Vec::with_capacity(body.len());
124+ let mut capabilities_placed = false;
125+ for line in &lines {
126+ match line.name.as_deref() {
127+ Some(name) if is_hidden(name) => continue,
128+ Some(_) => {
129+ // `<old> <new> <ref>` without what followed a NUL, then the
130+ // capabilities on the first command that is sent.
131+ let command = line.data.split(|byte| *byte == 0).next().unwrap_or_default();
132+ let command = command.strip_suffix(b"\n").unwrap_or(command);
133+ let mut data = command.to_vec();
134+ if !capabilities_placed && !capabilities.is_empty() {
135+ data.push(0);
136+ data.extend_from_slice(&capabilities);
137+ } else {
138+ data.push(b'\n');
139+ }
140+ capabilities_placed = true;
141+ out.extend(pkt_line(&data));
142+ }
143+ // `shallow` lines and the like stay where they were.
144+ None => out.extend(pkt_line(&line.data)),
145+ }
146+ }
147+ out.extend_from_slice(b"0000");
148+ out.extend_from_slice(&body[rest..]);
149+ Split::Without { body: out, hidden }
150+}
151+
152+fn ng_lines(hidden: &[String]) -> Vec<Packet> {
153+ hidden
154+ .iter()
155+ .map(|name| Packet::Data(format!("ng {name} {HIDDEN_REASON}\n").into_bytes()))
156+ .collect()
157+}
158+
159+/// A report-status (bare packets ending in a flush) with `ng` lines for
160+/// `hidden` before its flush.
161+fn report_with(report: &[u8], hidden: &[String]) -> Option<Vec<u8>> {
162+ let mut packets = packets(report)?;
163+ let flush = packets.iter().rposition(|packet| *packet == Packet::Special(*b"0000"))?;
164+ let ng = ng_lines(hidden);
165+ packets.splice(flush..flush, ng);
166+ Some(encode(&packets))
167+}
168+
169+/// The store's answer to a push it was sent without the commands to
170+/// hidden refs, with each of those refused in its report, so git prints
171+/// `! [remote rejected] refs/pull/1/head (deny updating a hidden ref)`
172+/// beside the refs that moved. Inside channel 1 when the client asked for
173+/// side-band; progress and errors on the other channels are kept as they
174+/// were. `None` when the answer is not a report this can read.
175+pub fn with_refused(answer: &[u8], hidden: &[String], sideband: bool) -> Option<Vec<u8>> {
176+ if !sideband {
177+ return report_with(answer, hidden);
178+ }
179+ let packets = packets(answer)?;
180+ let mut report = Vec::new();
181+ let mut others = Vec::new();
182+ for packet in packets {
183+ match &packet {
184+ Packet::Data(data) if data.first() == Some(&1) => report.extend_from_slice(&data[1..]),
185+ Packet::Special([b'0', b'0', b'0', b'0']) => {}
186+ _ => others.push(packet),
187+ }
188+ }
189+ let report = report_with(&report, hidden)?;
190+ let mut out = encode(&others);
191+ // side-band (not -64k) packets carry at most 1000 bytes.
192+ for chunk in report.chunks(990) {
193+ let mut packet = vec![1u8];
194+ packet.extend_from_slice(chunk);
195+ out.extend(pkt_line(&packet));
196+ }
197+ out.extend_from_slice(b"0000");
198+ Some(out)
199+}
200+
201+/// The receive-pack ref advertisement without hidden refs, as GitHub
202+/// advertises it, so `git push --mirror` does not try to delete what g1t
203+/// keeps. The capabilities move to the first ref left, or to a
204+/// `capabilities^{}` line when none is. `None` when there is nothing to
205+/// change or the answer is not a whole advertisement.
206+pub fn advertisement_without_hidden(body: &[u8]) -> Option<Vec<u8>> {
207+ let packets = packets(body)?;
208+ let name_of = |data: &[u8]| -> Option<String> {
209+ let line = data.split(|byte| *byte == 0).next()?;
210+ let (oid, name) = std::str::from_utf8(line).ok()?.trim_end().split_once(' ')?;
211+ (matches!(oid.len(), 40 | 64) && oid.bytes().all(|byte| byte.is_ascii_hexdigit())).then(|| name.to_owned())
212+ };
213+ let is_ref = |packet: &Packet| matches!(packet, Packet::Data(data) if !data.starts_with(b"# service=") && !data.starts_with(b"version ") && name_of(data).is_some());
214+ if !packets.iter().any(|packet| matches!(packet, Packet::Data(data) if is_ref(packet) && name_of(data).is_some_and(|name| is_hidden(&name)))) {
215+ return None;
216+ }
217+ let first = packets.iter().position(is_ref)?;
218+ let Packet::Data(first_line) = &packets[first] else { return None };
219+ let nul = first_line.iter().position(|byte| *byte == 0)?;
220+ let capabilities = first_line[nul + 1..].to_vec();
221+ let mut out: Vec<Packet> = Vec::with_capacity(packets.len());
222+ let mut placed = false;
223+ let mut end_of_refs = None;
224+ for (index, packet) in packets.into_iter().enumerate() {
225+ if index < first || !is_ref(&packet) {
226+ if index > first && end_of_refs.is_none() {
227+ end_of_refs = Some(out.len());
228+ }
229+ out.push(packet);
230+ continue;
231+ }
232+ let Packet::Data(data) = packet else { continue };
233+ let name = name_of(&data).unwrap_or_default();
234+ if is_hidden(&name) {
235+ continue;
236+ }
237+ let command = data.split(|byte| *byte == 0).next().unwrap_or_default();
238+ let command = command.strip_suffix(b"\n").unwrap_or(command).to_vec();
239+ let mut line = command;
240+ if placed {
241+ line.push(b'\n');
242+ } else {
243+ line.push(0);
244+ line.extend_from_slice(&capabilities);
245+ placed = true;
246+ }
247+ out.push(Packet::Data(line));
248+ }
249+ if !placed {
250+ let zero = "0".repeat(40);
251+ let mut line = format!("{zero} capabilities^{{}}").into_bytes();
252+ line.push(0);
253+ line.extend_from_slice(&capabilities);
254+ out.insert(end_of_refs.unwrap_or(out.len()).min(out.len()), Packet::Data(line));
255+ }
256+ Some(encode(&out))
257+}
258+
259+#[cfg(test)]
260+mod tests {
261+ use super::*;
262+
263+ const ZERO: &str = "0000000000000000000000000000000000000000";
264+ const OLD: &str = "c71546fcd893ef8b0f57388b65e620d759705dda";
265+ const NEW: &str = "4807077b296e6edbf410d55e72749d3e1170c291";
266+
267+ fn pkt(payload: &str) -> Vec<u8> {
268+ format!("{:04x}{payload}", payload.len() + 4).into_bytes()
269+ }
270+
271+ fn text(bytes: &[u8]) -> String {
272+ String::from_utf8_lossy(bytes).into_owned()
273+ }
274+
275+ const PACK: &[u8] = b"PACK\0\0\0\x02\0\0\0\0";
276+
277+ #[test]
278+ fn a_push_without_hidden_refs_is_untouched() {
279+ let body = [pkt(&format!("{OLD} {NEW} refs/heads/main\0 report-status\n")), b"0000".to_vec(), PACK.to_vec()].concat();
280+ assert_eq!(split(&body), Split::Untouched);
281+ }
282+
283+ #[test]
284+ fn hidden_refs_are_taken_out_and_the_rest_sent_on() {
285+ // A mirror push: refs/pull first, carrying the capabilities.
286+ let body = [
287+ pkt(&format!("{ZERO} {NEW} refs/pull/1/head\0 report-status side-band-64k quiet\n")),
288+ pkt(&format!("{OLD} {NEW} refs/heads/main\n")),
289+ pkt(&format!("{ZERO} {NEW} refs/pull/1/merge\n")),
290+ pkt(&format!("{ZERO} {NEW} refs/tags/v1\n")),
291+ b"0000".to_vec(),
292+ PACK.to_vec(),
293+ ]
294+ .concat();
295+ let Split::Without { body: sent, hidden } = split(&body) else { panic!("expected a split") };
296+ assert_eq!(hidden, ["refs/pull/1/head", "refs/pull/1/merge"]);
297+ let expected = [
298+ pkt(&format!("{OLD} {NEW} refs/heads/main\0 report-status side-band-64k quiet\n")),
299+ pkt(&format!("{ZERO} {NEW} refs/tags/v1\n")),
300+ b"0000".to_vec(),
301+ PACK.to_vec(),
302+ ]
303+ .concat();
304+ assert_eq!(text(&sent), text(&expected));
305+ }
306+
307+ #[test]
308+ fn a_push_of_hidden_refs_alone_is_answered_here() {
309+ let body = [
310+ pkt(&format!("{OLD} {ZERO} refs/pull/7/head\0 report-status\n")),
311+ b"0000".to_vec(),
312+ ]
313+ .concat();
314+ let Split::Refused { report, hidden } = split(&body) else { panic!("expected a refusal") };
315+ assert_eq!(hidden, ["refs/pull/7/head"]);
316+ assert_eq!(text(&report), text(&[pkt("unpack ok\n"), pkt("ng refs/pull/7/head deny updating a hidden ref\n"), b"0000".to_vec()].concat()));
317+ }
318+
319+ #[test]
320+ fn an_atomic_push_with_a_hidden_ref_is_refused_whole() {
321+ let body = [
322+ pkt(&format!("{OLD} {NEW} refs/heads/main\0 report-status atomic side-band-64k\n")),
323+ pkt(&format!("{ZERO} {NEW} refs/pull/1/head\n")),
324+ b"0000".to_vec(),
325+ PACK.to_vec(),
326+ ]
327+ .concat();
328+ let Split::Refused { report, .. } = split(&body) else { panic!("expected a refusal") };
329+ let report = text(&report);
330+ // Framed on channel 1 for side-band.
331+ assert!(report.contains("ng refs/heads/main atomic transaction failed\n"));
332+ assert!(report.contains("ng refs/pull/1/head deny updating a hidden ref\n"));
333+ assert!(report.ends_with("0000"));
334+ }
335+
336+ #[test]
337+ fn the_stores_report_gains_the_refusals() {
338+ let hidden = vec!["refs/pull/1/head".to_owned()];
339+ let bare = [pkt("unpack ok\n"), pkt("ok refs/heads/main\n"), b"0000".to_vec()].concat();
340+ let changed = with_refused(&bare, &hidden, false).unwrap();
341+ assert_eq!(
342+ text(&changed),
343+ text(&[pkt("unpack ok\n"), pkt("ok refs/heads/main\n"), pkt("ng refs/pull/1/head deny updating a hidden ref\n"), b"0000".to_vec()].concat())
344+ );
345+ // Side-band: progress kept, the report re-framed on channel 1.
346+ let inner = [pkt("unpack ok\n"), pkt("ok refs/heads/main\n"), b"0000".to_vec()].concat();
347+ let mut channel_one = vec![1u8];
348+ channel_one.extend_from_slice(&inner);
349+ let framed_answer = [pkt("\x02Resolving deltas: 100%\n"), pkt(&text(&channel_one)), b"0000".to_vec()].concat();
350+ let changed = with_refused(&framed_answer, &hidden, true).unwrap();
351+ let packets = packets(&changed).unwrap();
352+ assert_eq!(packets[0], Packet::Data(b"\x02Resolving deltas: 100%\n".to_vec()));
353+ let Packet::Data(report) = &packets[1] else { panic!() };
354+ assert_eq!(report[0], 1);
355+ assert!(text(&report[1..]).contains("ng refs/pull/1/head deny updating a hidden ref\n"));
356+ assert!(text(&report[1..]).ends_with("0000"));
357+ assert_eq!(packets.last(), Some(&Packet::Special(*b"0000")));
358+ // Not a report: left alone.
359+ assert_eq!(with_refused(b"garbage", &hidden, false), None);
360+ }
361+
362+ #[test]
363+ fn the_push_advertisement_leaves_hidden_refs_out() {
364+ let advertisement = [
365+ pkt("# service=git-receive-pack\n"),
366+ b"0000".to_vec(),
367+ pkt(&format!("{OLD} refs/heads/main\0report-status delete-refs side-band-64k ofs-delta\n")),
368+ pkt(&format!("{NEW} refs/pull/1/head\n")),
369+ pkt(&format!("{NEW} refs/pull/1/merge\n")),
370+ pkt(&format!("{OLD} refs/tags/v1\n")),
371+ b"0000".to_vec(),
372+ ]
373+ .concat();
374+ let changed = advertisement_without_hidden(&advertisement).unwrap();
375+ assert_eq!(
376+ text(&changed),
377+ text(
378+ &[
379+ pkt("# service=git-receive-pack\n"),
380+ b"0000".to_vec(),
381+ pkt(&format!("{OLD} refs/heads/main\0report-status delete-refs side-band-64k ofs-delta\n")),
382+ pkt(&format!("{OLD} refs/tags/v1\n")),
383+ b"0000".to_vec(),
384+ ]
385+ .concat()
386+ )
387+ );
388+ // Nothing hidden: nothing to change.
389+ let plain = [pkt("# service=git-receive-pack\n"), b"0000".to_vec(), pkt(&format!("{OLD} refs/heads/main\0report-status\n")), b"0000".to_vec()].concat();
390+ assert_eq!(advertisement_without_hidden(&plain), None);
391+ // Only hidden refs: the capabilities stay, on capabilities^{}.
392+ let only = [pkt("# service=git-receive-pack\n"), b"0000".to_vec(), pkt(&format!("{NEW} refs/pull/1/head\0report-status\n")), b"0000".to_vec()].concat();
393+ let changed = advertisement_without_hidden(&only).unwrap();
394+ assert_eq!(
395+ text(&changed),
396+ text(&[pkt("# service=git-receive-pack\n"), b"0000".to_vec(), pkt(&format!("{ZERO} capabilities^{{}}\0report-status\n")), b"0000".to_vec()].concat())
397+ );
398+ }
399+}
+1−0
1818 mod forks;
1919 mod git_http;
2020 mod git_ops;
21+mod hidden_refs;
2122 mod import;
2223 mod land;
2324 mod languages;

Comments

Sign in to comment.