1use crate::object::ObjectId;
10use crate::pack::{PackBuilder, parse_packfile};
11use crate::protocol::*;
12use crate::refs::Ref;
13use crate::storage::GitStorage;
14use crate::{Error, Result};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Service {
19 UploadPack,
20 ReceivePack,
21}
22
23impl Service {
24 pub fn from_str(s: &str) -> Option<Self> {
25 match s {
26 "git-upload-pack" => Some(Service::UploadPack),
27 "git-receive-pack" => Some(Service::ReceivePack),
28 _ => None,
29 }
30 }
31
32 pub fn as_str(&self) -> &'static str {
33 match self {
34 Service::UploadPack => "git-upload-pack",
35 Service::ReceivePack => "git-receive-pack",
36 }
37 }
38
39 pub fn content_type(&self) -> &'static str {
40 match self {
41 Service::UploadPack => "application/x-git-upload-pack-advertisement",
42 Service::ReceivePack => "application/x-git-receive-pack-advertisement",
43 }
44 }
45
46 pub fn result_content_type(&self) -> &'static str {
47 match self {
48 Service::UploadPack => "application/x-git-upload-pack-result",
49 Service::ReceivePack => "application/x-git-receive-pack-result",
50 }
51 }
52}
53
54pub fn handle_info_refs(storage: &GitStorage, service: Service) -> Result<(String, Vec<u8>)> {
56 let mut writer = PktLineWriter::new();
57
58 writer.write_str(&format!("# service={}", service.as_str()));
60 writer.flush();
61
62 let refs = storage.list_refs()?;
64
65 let caps = match service {
67 Service::UploadPack => format_capabilities(UPLOAD_PACK_CAPABILITIES),
68 Service::ReceivePack => format_capabilities(RECEIVE_PACK_CAPABILITIES),
69 };
70
71 if refs.is_empty() {
72 let zero = ObjectId::ZERO;
74 writer.write_str(&format!("{} capabilities^{{}}\0{}", zero, caps));
75 } else {
76 let mut first = true;
77 if let Ok(head_oid) = storage.resolve_ref("HEAD") {
79 if first {
80 writer.write_str(&format!("{} HEAD\0{}", head_oid, caps));
81 first = false;
82 } else {
83 writer.write_str(&format!("{} HEAD", head_oid));
84 }
85 }
86
87 for named_ref in &refs {
89 if named_ref.name == "HEAD" {
90 continue;
91 }
92 if let Ok(oid) = storage.resolve_ref(&named_ref.name) {
93 if first {
94 writer.write_str(&format!("{} {}\0{}", oid, named_ref.name, caps));
95 first = false;
96 } else {
97 writer.write_str(&format!("{} {}", oid, named_ref.name));
98 }
99 }
100 }
101 }
102
103 writer.flush();
104 Ok((service.content_type().to_string(), writer.into_bytes()))
105}
106
107pub fn handle_upload_pack(storage: &GitStorage, body: &[u8]) -> Result<Vec<u8>> {
109 let mut reader = PktLineReader::new(body);
110 let mut wants = Vec::new();
111 let mut haves = Vec::new();
112 let mut _done = false;
113
114 while let Some(pkt) = reader.read()? {
116 match pkt {
117 PktLine::Flush => break,
118 PktLine::Data(data) => {
119 let line = std::str::from_utf8(data)
120 .map_err(|_| Error::ProtocolError("invalid utf8".into()))?
121 .trim();
122
123 if let Some(rest) = line.strip_prefix("want ") {
124 let oid_hex = rest.split(' ').next().unwrap_or(rest);
125 if let Some(oid) = ObjectId::from_hex(oid_hex) {
126 wants.push(oid);
127 }
128 } else if let Some(oid_hex) = line.strip_prefix("have ") {
129 if let Some(oid) = ObjectId::from_hex(oid_hex.trim()) {
130 haves.push(oid);
131 }
132 } else if line == "done" {
133 _done = true;
134 }
135 }
137 _ => {}
138 }
139 }
140
141 let remaining = reader.remaining();
143 if !remaining.is_empty() {
144 let mut reader2 = PktLineReader::new(remaining);
145 while let Some(pkt) = reader2.read()? {
146 if let PktLine::Data(data) = pkt {
147 let line = std::str::from_utf8(data).unwrap_or("").trim();
148 if line == "done" {
149 _done = true;
150 } else if let Some(oid_hex) = line.strip_prefix("have ") {
151 if let Some(oid) = ObjectId::from_hex(oid_hex.trim()) {
152 haves.push(oid);
153 }
154 }
155 }
156 }
157 }
158
159 let mut response = PktLineWriter::new();
160
161 if wants.is_empty() {
162 response.write_str("NAK");
164 response.flush();
165 return Ok(response.into_bytes());
166 }
167
168 let mut common_commits = Vec::new();
171 for have in &haves {
172 if storage.has_object(have)? {
173 common_commits.push(*have);
174 }
175 }
176
177 if !common_commits.is_empty() {
179 for oid in &common_commits {
180 response.write_str(&format!("ACK {} common", oid));
181 }
182 if let Some(last) = common_commits.last() {
184 response.write_str(&format!("ACK {} ready", last));
185 }
186 }
187
188 response.write_str("NAK");
190
191 let mut builder = PackBuilder::new(storage);
193 for oid in wants {
194 builder.want(oid);
195 }
196 for oid in common_commits {
197 builder.have(oid);
198 }
199
200 let pack = builder.build()?;
201
202 const CHUNK_SIZE: usize = 65515; for chunk in pack.chunks(CHUNK_SIZE) {
206 response.write_raw(&sideband_pkt(sideband::DATA, chunk));
207 }
208
209 response.flush();
211
212 Ok(response.into_bytes())
213}
214
215pub fn handle_receive_pack(storage: &GitStorage, body: &[u8]) -> Result<Vec<u8>> {
217 let mut reader = PktLineReader::new(body);
218 let mut commands = Vec::new();
219 let mut use_sideband = false;
220
221 while let Some(pkt) = reader.read()? {
223 match pkt {
224 PktLine::Flush => break,
225 PktLine::Data(data) => {
226 let line = std::str::from_utf8(data)
227 .map_err(|_| Error::ProtocolError("invalid utf8".into()))?
228 .trim();
229
230 let parts: Vec<&str> = line.splitn(3, ' ').collect();
232 if parts.len() >= 3 {
233 let old_oid = if parts[0] == ObjectId::ZERO.to_hex() {
234 None
235 } else {
236 ObjectId::from_hex(parts[0])
237 };
238 let new_oid = if parts[1] == ObjectId::ZERO.to_hex() {
239 None
240 } else {
241 ObjectId::from_hex(parts[1])
242 };
243 let ref_and_caps = parts[2];
245 let (ref_name, caps) = ref_and_caps.split_once('\0')
246 .map(|(r, c)| (r.to_string(), Some(c)))
247 .unwrap_or_else(|| (ref_and_caps.to_string(), None));
248
249 if let Some(caps_str) = caps {
251 if caps_str.contains("side-band-64k") || caps_str.contains("side-band") {
252 use_sideband = true;
253 }
254 }
255
256 commands.push(RefCommand { old_oid, new_oid, ref_name });
257 }
258 }
259 _ => {}
260 }
261 }
262
263 let pack_data = reader.remaining();
265 if !pack_data.is_empty() {
266 parse_packfile(storage, pack_data)?;
268 }
269
270 let mut report = Vec::new();
272 report.push("unpack ok\n".to_string());
273
274 for cmd in &commands {
275 let result = apply_ref_command(storage, cmd);
276 match result {
277 Ok(()) => {
278 report.push(format!("ok {}\n", cmd.ref_name));
279 }
280 Err(e) => {
281 report.push(format!("ng {} {}\n", cmd.ref_name, e));
282 }
283 }
284 }
285
286 let mut response = PktLineWriter::new();
287
288 if use_sideband {
289 let mut report_pkt = PktLineWriter::new();
292 for line in &report {
293 report_pkt.write_str(line.trim());
294 }
295 report_pkt.flush();
296
297 let report_bytes = report_pkt.into_bytes();
299 response.write_raw(&sideband_pkt(sideband::DATA, &report_bytes));
300 } else {
301 for line in &report {
303 response.write_str(line.trim());
304 }
305 }
306
307 response.flush();
308 Ok(response.into_bytes())
309}
310
311#[derive(Debug)]
313struct RefCommand {
314 old_oid: Option<ObjectId>,
315 new_oid: Option<ObjectId>,
316 ref_name: String,
317}
318
319fn apply_ref_command(storage: &GitStorage, cmd: &RefCommand) -> Result<()> {
321 match (&cmd.old_oid, &cmd.new_oid) {
322 (None, Some(new)) => {
323 storage.write_ref(&cmd.ref_name, &Ref::Direct(*new))?;
325 }
326 (Some(_old), Some(new)) => {
327 storage.write_ref(&cmd.ref_name, &Ref::Direct(*new))?;
329 }
330 (Some(_old), None) => {
331 storage.delete_ref(&cmd.ref_name)?;
333 }
334 (None, None) => {
335 }
337 }
338 Ok(())
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use tempfile::tempdir;
345
346 #[test]
347 fn test_info_refs_empty_repo() {
348 let dir = tempdir().unwrap();
349 let storage = GitStorage::open(dir.path().join("git")).unwrap();
350
351 let (content_type, body) = handle_info_refs(&storage, Service::UploadPack).unwrap();
352 assert_eq!(content_type, "application/x-git-upload-pack-advertisement");
353
354 let body_str = String::from_utf8_lossy(&body);
355 assert!(body_str.contains("# service=git-upload-pack"));
356 assert!(body_str.contains("capabilities^{}"));
357 }
358
359 #[test]
360 fn test_info_refs_with_ref() {
361 let dir = tempdir().unwrap();
362 let storage = GitStorage::open(dir.path().join("git")).unwrap();
363
364 let tree_content = b"";
366 let tree_oid = storage.write_tree(tree_content).unwrap();
367
368 let commit_content = format!(
369 "tree {}\nauthor Test <test@test.com> 1234567890 +0000\ncommitter Test <test@test.com> 1234567890 +0000\n\nInitial commit\n",
370 tree_oid
371 );
372 let commit_oid = storage.write_commit(commit_content.as_bytes()).unwrap();
373
374 storage.write_ref("refs/heads/main", &Ref::Direct(commit_oid)).unwrap();
376 storage.write_ref("HEAD", &Ref::Symbolic("refs/heads/main".into())).unwrap();
377
378 let (_, body) = handle_info_refs(&storage, Service::UploadPack).unwrap();
379 let body_str = String::from_utf8_lossy(&body);
380
381 assert!(body_str.contains(&commit_oid.to_hex()));
382 assert!(body_str.contains("refs/heads/main"));
383 }
384}