use std::collections::HashSet;
use std::path::Path;
use anyhow::Context as _;
use super::cmd::git_info;
use super::refs::ExportHead;
use super::replies::Replies;
use crate::snapshot::RepoSnapshot;
pub(crate) fn push_pkt_line(out: &mut Vec<u8>, parts: &[&str]) {
use std::io::Write as _;
let len: usize = parts.iter().map(|part| part.len()).sum();
let _ = write!(out, "{:04x}", len + 4);
for part in parts {
out.extend_from_slice(part.as_bytes());
}
}
pub(crate) fn pkt_line(data: &[u8]) -> Vec<u8> {
let mut out = format!("{:04x}", data.len() + 4).into_bytes();
out.extend_from_slice(data);
out
}
pub(crate) fn ensure_advertised(
advertisement: &[u8],
refs: &[crate::snapshot::PublicRef],
) -> anyhow::Result<()> {
let advertised = advertised_refs(advertisement);
for r in refs {
anyhow::ensure!(
advertised.contains(format!("{} {}", r.raw, r.name).as_bytes()),
"smart advertisement is missing ref {}",
r.name
);
}
Ok(())
}
pub(crate) fn advertised_refs(advertisement: &[u8]) -> HashSet<&[u8]> {
let mut refs = HashSet::new();
let mut rest = advertisement;
while let Some(len) = rest
.get(..4)
.and_then(|head| std::str::from_utf8(head).ok())
.and_then(|head| usize::from_str_radix(head, 16).ok())
{
let Some(line) = rest.get(4..len.max(4)) else {
break;
};
let line = line.strip_suffix(b"\n").unwrap_or(line);
refs.extend(line.split(|&byte| byte == 0).next());
rest = &rest[len.max(4)..];
}
refs
}
pub(crate) fn starts_with(path: &Path, prefix: &[u8]) -> anyhow::Result<bool> {
use std::io::Read as _;
let mut start = Vec::with_capacity(prefix.len());
let file = std::fs::File::open(path).with_context(|| format!("open {}", path.display()))?;
(file.take(prefix.len() as u64).read_to_end(&mut start))
.with_context(|| format!("read {}", path.display()))?;
Ok(start == prefix)
}
pub(crate) fn v2_command(
snapshot: &RepoSnapshot,
name: &str,
agent: &str,
args: &[&str],
) -> Vec<u8> {
let mut request = pkt_line(format!("command={name}\n").as_bytes());
request.extend(pkt_line(format!("agent={agent}\n").as_bytes()));
let format = snapshot.object_format.capability();
if format != "sha1" {
request.extend(pkt_line(format!("object-format={format}\n").as_bytes()));
}
request.extend_from_slice(b"0001");
for arg in args {
request.extend(pkt_line(format!("{arg}\n").as_bytes()));
}
request
}
/// the same pack answers every `done` fetch, and a negotiation round gets a bare nak
/// unless the request names a base or depth-1 tip (see `write_fetch_replies`).
pub(crate) fn write_v2_artifacts(
snapshot: &RepoSnapshot,
export_dir: &Path,
head: &ExportHead,
replies: &Replies,
pack: &dyn Fn() -> anyhow::Result<u64>,
candidates: &[Vec<String>],
write: &dyn Fn(&str, &[u8]) -> anyhow::Result<()>,
) -> anyhow::Result<()> {
const FLUSH: &[u8] = b"0000";
let command = |name: &str, agent: &str, args: &[&str]| v2_command(snapshot, name, agent, args);
let advertisement = v2_advertisement(snapshot)?;
anyhow::ensure!(
advertisement.starts_with(&pkt_line(b"version 2\n")),
"git did not answer with a protocol v2 advertisement"
);
write("adv-v2", &advertisement)?;
write("ls-refs", &v2_ls_refs(snapshot, None, true))?;
write("ls-refs-head", &v2_ls_refs(snapshot, Some(head), false))?;
// git >= 2.48 asks for HEAD on every fetch, cob refspecs or not
write("ls-refs-head-cobs", &v2_ls_refs(snapshot, Some(head), true))?;
let mut negotiation = pkt_line(b"acknowledgments\n");
negotiation.extend(pkt_line(b"NAK\n"));
negotiation.extend_from_slice(FLUSH);
write("fetch-nodone", &negotiation)?;
let fetch = |args: &[&str]| {
let mut request = command("fetch", "knot-static", args);
request.extend_from_slice(FLUSH);
request
};
super::fetch::write_fetch_replies(
snapshot, export_dir, replies, candidates, pack, &fetch, write,
)
}
pub(crate) fn v0_advertisement(
snapshot: &RepoSnapshot,
head: &ExportHead,
) -> anyhow::Result<Vec<u8>> {
let agent = &git_info()?.agent;
let capability = snapshot.object_format.capability();
let mut public: Vec<&crate::snapshot::PublicRef> = snapshot.public_refs.iter().collect();
public.sort_by(|a, b| a.name.as_bytes().cmp(b.name.as_bytes()));
let symref_cap = match (&head.symbolic, &head.oid) {
(Some(sym), Some(_)) => format!(" symref=HEAD:{sym}"),
_ => String::new(),
};
let caps = format!(
"multi_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed no-done{symref_cap} object-format={capability} agent={agent}"
);
let mut out = Vec::new();
let mut first = true;
let mut line = |out: &mut Vec<u8>, oid: &str, name: &str, suffix: &str| {
let (nul, caps) = match std::mem::take(&mut first) {
true => ("\0", caps.as_str()),
false => ("", ""),
};
push_pkt_line(out, &[oid, " ", name, suffix, nul, caps, "\n"]);
};
if let Some(oid) = &head.oid {
line(&mut out, oid, "HEAD", "");
}
for r in public {
line(&mut out, &r.raw, &r.name, "");
if let Some(peeled) = &r.peeled {
line(&mut out, peeled, &r.name, "^{}");
}
}
if head.oid.is_none() && snapshot.public_refs.is_empty() {
let zero = "0".repeat(snapshot.object_format.kind().len_in_hex());
line(&mut out, &zero, "capabilities^{}", "");
}
out.extend_from_slice(b"0000");
Ok(out)
}
pub(crate) fn v2_advertisement(snapshot: &RepoSnapshot) -> anyhow::Result<Vec<u8>> {
let agent = &git_info()?.agent;
let capability = snapshot.object_format.capability();
let mut out = pkt_line(b"version 2\n");
out.extend_from_slice(&pkt_line(format!("agent={agent}\n").as_bytes()));
out.extend_from_slice(&pkt_line(b"ls-refs=unborn\n"));
out.extend_from_slice(&pkt_line(b"fetch=shallow wait-for-done\n"));
out.extend_from_slice(&pkt_line(b"server-option\n"));
out.extend_from_slice(&pkt_line(
format!("object-format={capability}\n").as_bytes(),
));
out.extend_from_slice(b"0000");
Ok(out)
}
pub(crate) fn v2_ls_refs(
snapshot: &RepoSnapshot,
head: Option<&ExportHead>,
cobs: bool,
) -> Vec<u8> {
let mut out = Vec::new();
if let Some(head) = head {
match (&head.oid, &head.symbolic) {
(Some(oid), Some(sym)) => {
push_pkt_line(&mut out, &[oid, " HEAD symref-target:", sym, "\n"]);
}
(None, Some(sym)) => {
push_pkt_line(&mut out, &["unborn HEAD symref-target:", sym, "\n"])
}
(Some(oid), None) => push_pkt_line(&mut out, &[oid, " HEAD\n"]),
(None, None) => {}
}
}
let mut refs: Vec<(&str, &str, Option<&str>)> = snapshot
.public_refs
.iter()
.filter(|r| r.name.starts_with("refs/heads/") || r.name.starts_with("refs/tags/"))
.map(|r| (r.name.as_str(), r.raw.as_str(), r.peeled.as_deref()))
.chain(
snapshot
.cob_refs
.iter()
.filter(|(name, _)| {
cobs && (name.starts_with("refs/cobs/") || name.starts_with("refs/tngl/keep/"))
})
.map(|(name, raw)| (name.as_str(), raw.as_str(), None)),
)
.collect();
refs.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
for (name, raw, peeled) in refs {
ls_refs_line(&mut out, raw, name, peeled);
}
out.extend_from_slice(b"0000");
out
}
fn ls_refs_line(out: &mut Vec<u8>, raw: &str, name: &str, peeled: Option<&str>) {
match peeled {
Some(peeled) => push_pkt_line(out, &[raw, " ", name, " peeled:", peeled, "\n"]),
None => push_pkt_line(out, &[raw, " ", name, "\n"]),
}
}