use std::path::Path;
use crate::error::Result;
use crate::pktline;
const CAPABILITIES: &str = "multi_ack multi_ack_detailed side-band-64k ofs-delta";
const ZERO_OID: &str = "0000000000000000000000000000000000000000";
pub fn advertise_refs(repo_path: &Path) -> Result<Vec<u8>> {
let repo = gix::open(repo_path)?;
let mut output = Vec::new();
output.extend_from_slice(&pktline::encode_comment("service=git-upload-pack"));
output.extend_from_slice(pktline::flush());
let mut refs: Vec<(String, String)> = Vec::new();
if let Ok(id) = repo.head_id() {
refs.push((id.to_string(), "HEAD".to_string()));
}
if let Ok(platform) = repo.references()
&& let Ok(iter) = platform.all()
{
for mut r in iter.flatten() {
let name = r.name().as_bstr().to_string();
if let Ok(id) = r.peel_to_id() {
refs.push((id.to_string(), name));
}
}
}
if refs.is_empty() {
let line = format!("{ZERO_OID} capabilities^{{}}\0{CAPABILITIES}\n");
output.extend_from_slice(&pktline::encode(line.as_bytes()));
} else {
let (oid, name) = &refs[0];
let first_line = format!("{oid} {name}\0{CAPABILITIES}\n");
output.extend_from_slice(&pktline::encode(first_line.as_bytes()));
for (oid, name) in &refs[1..] {
let line = format!("{oid} {name}\n");
output.extend_from_slice(&pktline::encode(line.as_bytes()));
}
}
output.extend_from_slice(pktline::flush());
Ok(output)
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
use super::*;
fn create_repo_with_commit(root: &Path) -> PathBuf {
let bare_path = root.join("test.git");
let clone_path = root.join("workdir");
let out = Command::new("git")
.args(["init", "--bare", bare_path.to_str().unwrap()])
.output()
.expect("git init --bare failed");
assert!(out.status.success(), "git init --bare failed: {:?}", out);
let out = Command::new("git")
.args(["symbolic-ref", "HEAD", "refs/heads/main"])
.current_dir(&bare_path)
.output()
.expect("git symbolic-ref failed");
assert!(out.status.success());
let out = Command::new("git")
.args([
"clone",
bare_path.to_str().unwrap(),
clone_path.to_str().unwrap(),
])
.output()
.expect("git clone failed");
assert!(out.status.success(), "git clone failed: {:?}", out);
for (key, val) in [("user.name", "Test User"), ("user.email", "test@test.com")] {
Command::new("git")
.args(["config", key, val])
.current_dir(&clone_path)
.output()
.expect("git config failed");
}
let out = Command::new("git")
.args(["commit", "--allow-empty", "-m", "initial commit"])
.current_dir(&clone_path)
.env("GIT_AUTHOR_NAME", "Test User")
.env("GIT_AUTHOR_EMAIL", "test@test.com")
.env("GIT_COMMITTER_NAME", "Test User")
.env("GIT_COMMITTER_EMAIL", "test@test.com")
.output()
.expect("git commit failed");
assert!(out.status.success(), "git commit failed: {:?}", out);
let out = Command::new("git")
.args(["push", "origin", "main"])
.current_dir(&clone_path)
.output()
.expect("git push failed");
assert!(out.status.success(), "git push failed: {:?}", out);
bare_path
}
#[test]
fn advertise_refs_starts_with_service_header() {
let dir = TempDir::new().unwrap();
let repo_path = create_repo_with_commit(dir.path());
let output = advertise_refs(&repo_path).unwrap();
let output_str = String::from_utf8_lossy(&output);
assert!(
output_str.starts_with("001e# service=git-upload-pack\n0000"),
"output does not start with service header: {output_str:?}"
);
}
#[test]
fn advertise_refs_contains_capabilities() {
let dir = TempDir::new().unwrap();
let repo_path = create_repo_with_commit(dir.path());
let output = advertise_refs(&repo_path).unwrap();
let output_str = String::from_utf8_lossy(&output);
assert!(
output_str.contains("multi_ack"),
"output missing multi_ack: {output_str:?}"
);
assert!(
output_str.contains("multi_ack_detailed"),
"output missing multi_ack_detailed: {output_str:?}"
);
assert!(
output_str.contains("side-band-64k"),
"output missing side-band-64k: {output_str:?}"
);
assert!(
output_str.contains("ofs-delta"),
"output missing ofs-delta: {output_str:?}"
);
assert!(
!output_str.contains("thin-pack"),
"output should not advertise thin-pack: {output_str:?}"
);
assert!(
!output_str.contains("shallow"),
"output should not advertise shallow: {output_str:?}"
);
}
#[test]
fn advertise_refs_contains_main_branch() {
let dir = TempDir::new().unwrap();
let repo_path = create_repo_with_commit(dir.path());
let output = advertise_refs(&repo_path).unwrap();
let output_str = String::from_utf8_lossy(&output);
assert!(
output_str.contains("refs/heads/main"),
"output missing refs/heads/main: {output_str:?}"
);
}
#[test]
fn advertise_refs_ends_with_flush() {
let dir = TempDir::new().unwrap();
let repo_path = create_repo_with_commit(dir.path());
let output = advertise_refs(&repo_path).unwrap();
assert!(
output.ends_with(b"0000"),
"output does not end with flush packet: {:?}",
String::from_utf8_lossy(&output)
);
}
}