1use crate::pack::{PackBuilder, PackParser};
7use crate::pktline::{PktLine, PktLineReader, PktLineWriter};
8use crate::Result;
9use guts_storage::{ObjectId, ObjectStore, Reference, Repository};
10use std::io::{Read, Write};
11
12const CAPABILITIES: &str =
14 "report-status delete-refs side-band-64k quiet ofs-delta agent=guts/0.1.0";
15
16#[derive(Debug, Clone)]
18pub struct RefAdvertisement {
19 pub id: ObjectId,
21 pub name: String,
23}
24
25pub fn advertise_refs<W: Write>(writer: &mut W, repo: &Repository, service: &str) -> Result<()> {
27 let mut pkt_writer = PktLineWriter::new(writer);
28
29 let refs = repo.refs.list_all();
31 let head = repo.refs.resolve_head().ok();
32
33 let first_ref = if let Some(head_id) = head {
35 format!("{} HEAD\0{}\n", head_id, CAPABILITIES)
36 } else if let Some((name, Reference::Direct(id))) = refs.first() {
37 format!("{} {}\0{}\n", id, name, CAPABILITIES)
38 } else {
39 let zero_id = "0000000000000000000000000000000000000000";
41 format!("{} capabilities^{{}}\0{}\n", zero_id, CAPABILITIES)
42 };
43
44 pkt_writer.write(&PktLine::from_string(&format!("# service={}\n", service)))?;
45 pkt_writer.flush_pkt()?;
46
47 pkt_writer.write(&PktLine::from_string(&first_ref))?;
48
49 for (name, reference) in &refs {
51 if let Reference::Direct(id) = reference {
52 pkt_writer.write_line(&format!("{} {}", id, name))?;
53 }
54 }
55
56 pkt_writer.flush_pkt()?;
57 pkt_writer.flush()?;
58
59 Ok(())
60}
61
62#[derive(Debug, Clone)]
64pub struct WantHave {
65 pub wants: Vec<ObjectId>,
67 pub haves: Vec<ObjectId>,
69}
70
71impl WantHave {
72 pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
74 let mut pkt_reader = PktLineReader::new(reader);
75 let mut wants = Vec::new();
76 let mut haves = Vec::new();
77
78 loop {
80 match pkt_reader.read()? {
81 Some(PktLine::Data(data)) => {
82 let line = String::from_utf8_lossy(&data);
83 let line = line.trim();
84
85 if line.starts_with("want ") {
86 let id_str = &line[5..45];
87 wants.push(ObjectId::from_hex(id_str)?);
88 } else if line.starts_with("have ") {
89 let id_str = &line[5..45];
90 haves.push(ObjectId::from_hex(id_str)?);
91 } else if line == "done" {
92 break;
93 }
94 }
95 Some(PktLine::Flush) => {
96 continue;
98 }
99 _ => break,
100 }
101 }
102
103 Ok(Self { wants, haves })
104 }
105}
106
107pub fn upload_pack<R: Read, W: Write>(
109 reader: &mut R,
110 writer: &mut W,
111 repo: &Repository,
112) -> Result<()> {
113 let want_have = WantHave::parse(reader)?;
114 let mut pkt_writer = PktLineWriter::new(writer);
115
116 if want_have.wants.is_empty() {
117 pkt_writer.write_line("NAK")?;
118 pkt_writer.flush()?;
119 return Ok(());
120 }
121
122 let mut builder = PackBuilder::new();
124
125 for want_id in &want_have.wants {
126 collect_objects(&repo.objects, want_id, &want_have.haves, &mut builder)?;
128 }
129
130 let pack = builder.build()?;
131
132 pkt_writer.write_line("NAK")?; for chunk in pack.chunks(65515) {
138 let mut data = vec![1u8]; data.extend_from_slice(chunk);
141 pkt_writer.write(&PktLine::Data(data))?;
142 }
143
144 pkt_writer.flush_pkt()?;
145 pkt_writer.flush()?;
146
147 Ok(())
148}
149
150fn collect_objects(
152 store: &ObjectStore,
153 id: &ObjectId,
154 have: &[ObjectId],
155 builder: &mut PackBuilder,
156) -> Result<()> {
157 if have.contains(id) {
159 return Ok(());
160 }
161
162 if let Ok(object) = store.get(id) {
163 builder.add(object.clone());
164
165 if object.object_type == guts_storage::ObjectType::Commit {
167 let content = String::from_utf8_lossy(&object.data);
169 for line in content.lines() {
170 if let Some(tree_hex) = line.strip_prefix("tree ") {
171 if let Ok(tree_id) = ObjectId::from_hex(tree_hex) {
172 collect_objects(store, &tree_id, have, builder)?;
173 }
174 } else if let Some(parent_hex) = line.strip_prefix("parent ") {
175 if let Ok(parent_id) = ObjectId::from_hex(parent_hex) {
176 collect_objects(store, &parent_id, have, builder)?;
177 }
178 } else if line.is_empty() {
179 break; }
181 }
182 }
183 else if object.object_type == guts_storage::ObjectType::Tree {
185 }
188 }
189
190 Ok(())
191}
192
193#[derive(Debug, Clone)]
195pub struct Command {
196 pub old_id: ObjectId,
198 pub new_id: ObjectId,
200 pub ref_name: String,
202}
203
204impl Command {
205 pub fn is_create(&self) -> bool {
207 self.old_id.to_hex() == "0000000000000000000000000000000000000000"
208 }
209
210 pub fn is_delete(&self) -> bool {
212 self.new_id.to_hex() == "0000000000000000000000000000000000000000"
213 }
214}
215
216pub fn receive_pack<R: Read, W: Write>(
218 reader: &mut R,
219 writer: &mut W,
220 repo: &Repository,
221) -> Result<Vec<Command>> {
222 let mut pkt_reader = PktLineReader::new(reader);
223 let mut commands = Vec::new();
224
225 loop {
227 match pkt_reader.read()? {
228 Some(PktLine::Data(data)) => {
229 let line = String::from_utf8_lossy(&data);
230 let line = line.trim();
231
232 let parts: Vec<&str> = line.splitn(3, ' ').collect();
234 if parts.len() >= 3 {
235 let old_id = ObjectId::from_hex(parts[0])?;
236 let new_id = ObjectId::from_hex(parts[1])?;
237 let ref_name = parts[2].split('\0').next().unwrap_or(parts[2]).to_string();
238
239 commands.push(Command {
240 old_id,
241 new_id,
242 ref_name,
243 });
244 }
245 }
246 Some(PktLine::Flush) | None => break,
247 _ => continue,
248 }
249 }
250
251 let mut pack_data = Vec::new();
253 pkt_reader.inner_mut().read_to_end(&mut pack_data)?;
255
256 if !pack_data.is_empty() {
257 let mut parser = PackParser::new(&pack_data);
259 parser.parse(&repo.objects)?;
260 }
261
262 for cmd in &commands {
264 if cmd.is_delete() {
265 let _ = repo.refs.delete(&cmd.ref_name);
266 } else {
267 repo.refs.set(&cmd.ref_name, cmd.new_id);
268 }
269 }
270
271 let mut pkt_writer = PktLineWriter::new(writer);
273 pkt_writer.write_line("unpack ok")?;
274 for cmd in &commands {
275 pkt_writer.write_line(&format!("ok {}", cmd.ref_name))?;
276 }
277 pkt_writer.flush_pkt()?;
278 pkt_writer.flush()?;
279
280 Ok(commands)
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_ref_advertisement() {
289 let repo = Repository::new("test", "alice");
290
291 let blob = guts_storage::GitObject::blob(b"test".to_vec());
293 let id = repo.objects.put(blob);
294 repo.refs.set("refs/heads/main", id);
295
296 let mut output = Vec::new();
297 advertise_refs(&mut output, &repo, "git-upload-pack").unwrap();
298
299 let output_str = String::from_utf8_lossy(&output);
300 assert!(output_str.contains("git-upload-pack"));
301 assert!(output_str.contains(&id.to_hex()));
302 }
303}