use crate::pack::{PackBuilder, PackParser};
use crate::pktline::{PktLine, PktLineReader, PktLineWriter};
use crate::Result;
use guts_storage::{ObjectId, ObjectStore, Reference, Repository};
use std::io::{Read, Write};
const CAPABILITIES: &str =
"report-status delete-refs side-band-64k quiet ofs-delta agent=guts/0.1.0";
#[derive(Debug, Clone)]
pub struct RefAdvertisement {
pub id: ObjectId,
pub name: String,
}
pub fn advertise_refs<W: Write>(writer: &mut W, repo: &Repository, service: &str) -> Result<()> {
let mut pkt_writer = PktLineWriter::new(writer);
let refs = repo.refs.list_all();
let head = repo.refs.resolve_head().ok();
let first_ref = if let Some(head_id) = head {
format!("{} HEAD\0{}\n", head_id, CAPABILITIES)
} else if let Some((name, Reference::Direct(id))) = refs.first() {
format!("{} {}\0{}\n", id, name, CAPABILITIES)
} else {
let zero_id = "0000000000000000000000000000000000000000";
format!("{} capabilities^{{}}\0{}\n", zero_id, CAPABILITIES)
};
pkt_writer.write(&PktLine::from_string(&format!("# service={}\n", service)))?;
pkt_writer.flush_pkt()?;
pkt_writer.write(&PktLine::from_string(&first_ref))?;
for (name, reference) in &refs {
if let Reference::Direct(id) = reference {
pkt_writer.write_line(&format!("{} {}", id, name))?;
}
}
pkt_writer.flush_pkt()?;
pkt_writer.flush()?;
Ok(())
}
#[derive(Debug, Clone)]
pub struct WantHave {
pub wants: Vec<ObjectId>,
pub haves: Vec<ObjectId>,
}
impl WantHave {
pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
let mut pkt_reader = PktLineReader::new(reader);
let mut wants = Vec::new();
let mut haves = Vec::new();
loop {
match pkt_reader.read()? {
Some(PktLine::Data(data)) => {
let line = String::from_utf8_lossy(&data);
let line = line.trim();
if line.starts_with("want ") {
let id_str = &line[5..45];
wants.push(ObjectId::from_hex(id_str)?);
} else if line.starts_with("have ") {
let id_str = &line[5..45];
haves.push(ObjectId::from_hex(id_str)?);
} else if line == "done" {
break;
}
}
Some(PktLine::Flush) => {
continue;
}
_ => break,
}
}
Ok(Self { wants, haves })
}
}
pub fn upload_pack<R: Read, W: Write>(
reader: &mut R,
writer: &mut W,
repo: &Repository,
) -> Result<()> {
let want_have = WantHave::parse(reader)?;
let mut pkt_writer = PktLineWriter::new(writer);
if want_have.wants.is_empty() {
pkt_writer.write_line("NAK")?;
pkt_writer.flush()?;
return Ok(());
}
let mut builder = PackBuilder::new();
for want_id in &want_have.wants {
collect_objects(&repo.objects, want_id, &want_have.haves, &mut builder)?;
}
let pack = builder.build()?;
pkt_writer.write_line("NAK")?;
for chunk in pack.chunks(65515) {
let mut data = vec![1u8]; data.extend_from_slice(chunk);
pkt_writer.write(&PktLine::Data(data))?;
}
pkt_writer.flush_pkt()?;
pkt_writer.flush()?;
Ok(())
}
fn collect_objects(
store: &ObjectStore,
id: &ObjectId,
have: &[ObjectId],
builder: &mut PackBuilder,
) -> Result<()> {
if have.contains(id) {
return Ok(());
}
if let Ok(object) = store.get(id) {
builder.add(object.clone());
if object.object_type == guts_storage::ObjectType::Commit {
let content = String::from_utf8_lossy(&object.data);
for line in content.lines() {
if let Some(tree_hex) = line.strip_prefix("tree ") {
if let Ok(tree_id) = ObjectId::from_hex(tree_hex) {
collect_objects(store, &tree_id, have, builder)?;
}
} else if let Some(parent_hex) = line.strip_prefix("parent ") {
if let Ok(parent_id) = ObjectId::from_hex(parent_hex) {
collect_objects(store, &parent_id, have, builder)?;
}
} else if line.is_empty() {
break; }
}
}
else if object.object_type == guts_storage::ObjectType::Tree {
}
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct Command {
pub old_id: ObjectId,
pub new_id: ObjectId,
pub ref_name: String,
}
impl Command {
pub fn is_create(&self) -> bool {
self.old_id.to_hex() == "0000000000000000000000000000000000000000"
}
pub fn is_delete(&self) -> bool {
self.new_id.to_hex() == "0000000000000000000000000000000000000000"
}
}
pub fn receive_pack<R: Read, W: Write>(
reader: &mut R,
writer: &mut W,
repo: &Repository,
) -> Result<Vec<Command>> {
let mut pkt_reader = PktLineReader::new(reader);
let mut commands = Vec::new();
loop {
match pkt_reader.read()? {
Some(PktLine::Data(data)) => {
let line = String::from_utf8_lossy(&data);
let line = line.trim();
let parts: Vec<&str> = line.splitn(3, ' ').collect();
if parts.len() >= 3 {
let old_id = ObjectId::from_hex(parts[0])?;
let new_id = ObjectId::from_hex(parts[1])?;
let ref_name = parts[2].split('\0').next().unwrap_or(parts[2]).to_string();
commands.push(Command {
old_id,
new_id,
ref_name,
});
}
}
Some(PktLine::Flush) | None => break,
_ => continue,
}
}
let mut pack_data = Vec::new();
pkt_reader.inner_mut().read_to_end(&mut pack_data)?;
if !pack_data.is_empty() {
let mut parser = PackParser::new(&pack_data);
parser.parse(&repo.objects)?;
}
for cmd in &commands {
if cmd.is_delete() {
let _ = repo.refs.delete(&cmd.ref_name);
} else {
repo.refs.set(&cmd.ref_name, cmd.new_id);
}
}
let mut pkt_writer = PktLineWriter::new(writer);
pkt_writer.write_line("unpack ok")?;
for cmd in &commands {
pkt_writer.write_line(&format!("ok {}", cmd.ref_name))?;
}
pkt_writer.flush_pkt()?;
pkt_writer.flush()?;
Ok(commands)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ref_advertisement() {
let repo = Repository::new("test", "alice");
let blob = guts_storage::GitObject::blob(b"test".to_vec());
let id = repo.objects.put(blob);
repo.refs.set("refs/heads/main", id);
let mut output = Vec::new();
advertise_refs(&mut output, &repo, "git-upload-pack").unwrap();
let output_str = String::from_utf8_lossy(&output);
assert!(output_str.contains("git-upload-pack"));
assert!(output_str.contains(&id.to_hex()));
}
}