1use std::collections::{HashMap, HashSet, VecDeque};
13use std::fs;
14use std::io::{Cursor, ErrorKind, Read};
15use std::path::Path;
16use std::time::{SystemTime, UNIX_EPOCH};
17
18use sley_config::GitConfig;
19use sley_core::{
20 Capability, GitError, ObjectFormat, ObjectId, Result, UPSTREAM_GIT_COMPAT_VERSION,
21};
22use sley_object::{Commit, ObjectType, Tag};
23use sley_odb::{
24 FileObjectDatabase, ObjectReader, RawPackInstallOptions, build_and_install_reachable_pack,
25 build_and_install_reachable_pack_filtered, build_reachable_pack, collect_reachable_object_ids,
26};
27use sley_protocol::{
28 PKT_LINE_MAX_PAYLOAD_LEN, ProtocolV2FetchAcknowledgment, ProtocolV2FetchFeatures,
29 ProtocolV2FetchRequest, ProtocolV2FetchResponseSection, ProtocolV2FetchShallowInfo,
30 ProtocolV2LsRefsFeatures, ProtocolV2LsRefsRecord, ProtocolV2LsRefsRef, ProtocolV2LsRefsRequest,
31 ProtocolVersion, ReceivePackCommand, ReceivePackCommandStatus, ReceivePackFeatures,
32 ReceivePackPushRequest, ReceivePackPushRequestHeader, ReceivePackReportStatus,
33 ReceivePackRequest, ReceivePackUnpackStatus, RefAdvertisement, SideBandChannel, SideBandPacket,
34 TransportHandshake, UploadPackFeatures, UploadPackNegotiationRequest,
35 UploadPackPackfileResponse, UploadPackRawPackfileResponse, UploadPackRequest,
36 apply_receive_pack_push_request, build_upload_pack_raw_packfile_response,
37 classify_protocol_v2_command_request, encode_protocol_v2_fetch_capability,
38 encode_protocol_v2_ls_refs_capability, encode_receive_pack_features,
39 encode_upload_pack_features, read_protocol_v2_command_request,
40 read_upload_pack_negotiation_request, read_upload_pack_request,
41 validate_receive_pack_push_request_features, write_protocol_v2_advertisement,
42 write_protocol_v2_fetch_response, write_protocol_v2_ls_refs_response,
43 write_upload_pack_negotiation_request, write_upload_pack_request,
44};
45use sley_refs::{
46 DeleteRef, FileRefStore, Ref, RefDeletePrecondition, RefPrecondition, RefTarget, ReflogEntry,
47};
48
49fn zero_oid(format: ObjectFormat) -> Result<ObjectId> {
52 Ok(ObjectId::null(format))
53}
54
55fn resolve_for_each_ref_target(
58 store: &FileRefStore,
59 reference: &Ref,
60) -> Result<Option<(ObjectId, Option<String>)>> {
61 let mut target = reference.target.clone();
62 let mut symref = None;
63 for _ in 0..5 {
64 match target {
65 RefTarget::Direct(oid) => return Ok(Some((oid, symref))),
66 RefTarget::Symbolic(name) => {
67 symref.get_or_insert_with(|| name.clone());
68 let Some(next) = store.read_ref(&name)? else {
69 return Ok(None);
70 };
71 target = next;
72 }
73 }
74 }
75 Ok(None)
76}
77
78pub fn upload_pack_features(git_dir: &Path, format: ObjectFormat) -> Result<UploadPackFeatures> {
81 let store = FileRefStore::new(git_dir, format);
82 let mut symrefs = Vec::new();
83 if let Some(RefTarget::Symbolic(target)) = store.read_ref("HEAD")? {
84 symrefs.push(format!("HEAD:{target}"));
85 }
86 Ok(UploadPackFeatures {
87 object_format: Some(format),
88 side_band_64k: true,
89 symrefs,
90 ..UploadPackFeatures::default()
91 })
92}
93
94pub fn upload_pack_request_uses_sideband(request: &UploadPackRequest) -> bool {
96 request
97 .capabilities
98 .iter()
99 .any(|capability| matches!(capability.name.as_str(), "side-band" | "side-band-64k"))
100}
101
102pub fn upload_pack_sideband_response(
105 response: UploadPackRawPackfileResponse,
106) -> UploadPackPackfileResponse {
107 let mut sideband = Vec::new();
108 let chunk_len = PKT_LINE_MAX_PAYLOAD_LEN - 1;
109 for chunk in response.packfile.chunks(chunk_len) {
110 sideband.push(SideBandPacket {
111 channel: SideBandChannel::Data,
112 data: chunk.to_vec(),
113 });
114 }
115 UploadPackPackfileResponse {
116 acknowledgments: response.acknowledgments,
117 sideband,
118 }
119}
120
121pub fn attach_upload_pack_capabilities(
124 advertisements: &mut Vec<RefAdvertisement>,
125 format: ObjectFormat,
126 features: &UploadPackFeatures,
127) -> Result<()> {
128 let capabilities = encode_upload_pack_features(features)?;
129 if let Some(first) = advertisements.first_mut() {
130 first.capabilities = capabilities;
131 } else {
132 advertisements.push(RefAdvertisement {
133 oid: zero_oid(format)?,
134 name: "capabilities^{}".into(),
135 capabilities,
136 });
137 }
138 Ok(())
139}
140
141pub fn upload_pack_from_local_repository(
145 git_dir: &Path,
146 format: ObjectFormat,
147 features: &UploadPackFeatures,
148 request: UploadPackRequest,
149 haves: HashSet<ObjectId>,
150) -> Result<UploadPackRawPackfileResponse> {
151 let db = FileObjectDatabase::from_git_dir(git_dir, format);
152 build_upload_pack_raw_packfile_response(
153 features,
154 request,
155 haves,
156 |oid| db.contains(oid),
157 |wants, known_haves| {
158 let excluded = collect_reachable_object_ids(&db, format, known_haves)?;
159 build_reachable_pack(&db, format, wants, &excluded)
160 .map(|pack| pack.map(|pack| pack.pack))
161 },
162 )
163}
164
165pub fn receive_pack_features(format: ObjectFormat) -> ReceivePackFeatures {
168 ReceivePackFeatures {
169 report_status: true,
170 delete_refs: true,
171 ofs_delta: true,
172 push_options: true,
173 quiet: true,
174 object_format: Some(format),
175 ..ReceivePackFeatures::default()
176 }
177}
178
179pub fn receive_pack_request_uses_push_options(request: &ReceivePackRequest) -> bool {
182 request
183 .capabilities
184 .iter()
185 .any(|capability| capability.name == "push-options")
186}
187
188pub fn attach_receive_pack_capabilities(
191 advertisements: &mut Vec<RefAdvertisement>,
192 format: ObjectFormat,
193 features: &ReceivePackFeatures,
194) -> Result<()> {
195 let capabilities = encode_receive_pack_features(features)?;
196 if let Some(first) = advertisements.first_mut() {
197 first.capabilities = capabilities;
198 } else {
199 advertisements.push(RefAdvertisement {
200 oid: zero_oid(format)?,
201 name: "capabilities^{}".into(),
202 capabilities,
203 });
204 }
205 Ok(())
206}
207
208pub fn receive_pack_into_local_repository(
212 remote_git_dir: &Path,
213 format: ObjectFormat,
214 request: &ReceivePackPushRequest,
215) -> Result<ReceivePackReportStatus> {
216 let remote_store = FileRefStore::new(remote_git_dir, format);
217 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
218 let deletes_applied_with_updates = std::cell::RefCell::new(HashSet::<String>::new());
219 apply_receive_pack_push_request(
220 &receive_pack_features(format),
221 request,
222 |name| match remote_store.read_ref(name)? {
223 Some(RefTarget::Direct(oid)) => Ok(Some(oid)),
224 Some(RefTarget::Symbolic(_)) | None => Ok(None),
225 },
226 |packfile| {
227 let mut reader = packfile;
228 remote_db
229 .install_raw_pack_from_reader(&mut reader)
230 .map(|_| ())
231 },
232 |oid| remote_db.contains(oid),
233 |commands| {
234 let applied = apply_receive_pack_ref_transaction(
235 remote_git_dir,
236 format,
237 &remote_store,
238 commands,
239 &request.commands.commands,
240 )?;
241 deletes_applied_with_updates.borrow_mut().extend(applied);
242 Ok(())
243 },
244 |command| {
245 if deletes_applied_with_updates
246 .borrow()
247 .contains(command.name.as_str())
248 {
249 return Ok(());
250 }
251 remote_store
252 .delete_ref_checked(DeleteRef {
253 name: command.name.clone(),
254 expected_old: (!command.old_id.is_null()).then_some(command.old_id),
255 reflog: None,
256 })
257 .map(|_| ())
258 .map_err(|err| GitError::Transaction(err.to_string()))
259 },
260 )
261}
262
263pub fn receive_pack_stream_into_local_repository<R: Read>(
268 remote_git_dir: &Path,
269 format: ObjectFormat,
270 header: &ReceivePackPushRequestHeader,
271 pack_reader: &mut R,
272) -> Result<ReceivePackReportStatus> {
273 let remote_store = FileRefStore::new(remote_git_dir, format);
274 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
275 let pack_prefix = read_optional_pack_prefix(pack_reader)?;
276 let validation_request = ReceivePackPushRequest {
277 commands: header.commands.clone(),
278 push_options: header.push_options.clone(),
279 packfile: pack_prefix.clone().unwrap_or_default(),
280 };
281 validate_receive_pack_push_request_features(
282 &receive_pack_features(format),
283 &validation_request,
284 )?;
285
286 let deletes_applied_with_updates = std::cell::RefCell::new(HashSet::<String>::new());
287 for command in header
288 .commands
289 .commands
290 .iter()
291 .filter(|command| command.new_id.is_null())
292 {
293 let current = match remote_store.read_ref(&command.name)? {
294 Some(RefTarget::Direct(oid)) => Some(oid),
295 Some(RefTarget::Symbolic(_)) | None => None,
296 };
297 if !command.old_id.is_null() && current != Some(command.old_id.clone()) {
298 return Err(GitError::Transaction(format!(
299 "expected ref {} to match",
300 command.name
301 )));
302 }
303 }
304
305 let updates = header
306 .commands
307 .commands
308 .iter()
309 .filter(|command| !command.new_id.is_null())
310 .cloned()
311 .collect::<Vec<_>>();
312 if !updates.is_empty() {
313 if let Some(prefix) = pack_prefix {
314 let mut stream = Cursor::new(prefix).chain(pack_reader);
315 remote_db
316 .install_raw_pack_from_reader(&mut stream)
317 .map(|_| ())?;
318 }
319 for command in &updates {
320 if !remote_db.contains(&command.new_id)? {
321 return Err(GitError::InvalidObject(format!(
322 "receive-pack packfile did not provide {}",
323 command.new_id
324 )));
325 }
326 }
327 let applied = apply_receive_pack_ref_transaction(
328 remote_git_dir,
329 format,
330 &remote_store,
331 &updates,
332 &header.commands.commands,
333 )?;
334 deletes_applied_with_updates.borrow_mut().extend(applied);
335 }
336
337 for command in header
338 .commands
339 .commands
340 .iter()
341 .filter(|command| command.new_id.is_null())
342 {
343 if deletes_applied_with_updates
344 .borrow()
345 .contains(command.name.as_str())
346 {
347 continue;
348 }
349 remote_store
350 .delete_ref_checked(DeleteRef {
351 name: command.name.clone(),
352 expected_old: (!command.old_id.is_null()).then_some(command.old_id),
353 reflog: None,
354 })
355 .map(|_| ())
356 .map_err(|err| GitError::Transaction(err.to_string()))?;
357 }
358
359 Ok(ReceivePackReportStatus {
360 unpack: ReceivePackUnpackStatus::Ok,
361 commands: header
362 .commands
363 .commands
364 .iter()
365 .map(|command| ReceivePackCommandStatus::Ok {
366 name: command.name.clone(),
367 })
368 .collect(),
369 })
370}
371
372fn read_optional_pack_prefix(reader: &mut impl Read) -> Result<Option<Vec<u8>>> {
373 let mut prefix = [0u8; 4];
374 loop {
375 match reader.read(&mut prefix[..1]) {
376 Ok(0) => return Ok(None),
377 Ok(1) => break,
378 Ok(_) => unreachable!("one-byte read returned more than one byte"),
379 Err(err) if err.kind() == ErrorKind::Interrupted => {}
380 Err(err) => return Err(err.into()),
381 }
382 }
383 reader.read_exact(&mut prefix[1..])?;
384 if &prefix != b"PACK" {
385 return Err(GitError::InvalidFormat(
386 "receive-pack packfile must start with PACK".into(),
387 ));
388 }
389 Ok(Some(prefix.to_vec()))
390}
391
392fn receive_pack_log_all_ref_updates(git_dir: &Path) -> bool {
393 let Ok(config) = fs::read_to_string(git_dir.join("config")) else {
394 return false;
395 };
396 let mut in_core = false;
397 for raw_line in config.lines() {
398 let line = raw_line.trim();
399 if line.starts_with('[') && line.ends_with(']') {
400 in_core = line.eq_ignore_ascii_case("[core]");
401 continue;
402 }
403 if !in_core || line.starts_with('#') || line.starts_with(';') {
404 continue;
405 }
406 let Some((name, value)) = line.split_once('=') else {
407 continue;
408 };
409 if name.trim().eq_ignore_ascii_case("logallrefupdates") {
410 return matches!(
411 value.trim().trim_matches('"').to_ascii_lowercase().as_str(),
412 "true" | "yes" | "on" | "1" | "always"
413 );
414 }
415 }
416 false
417}
418
419fn receive_pack_should_write_reflog(refname: &str) -> bool {
420 refname == "HEAD"
421 || refname.starts_with("refs/heads/")
422 || refname.starts_with("refs/remotes/")
423 || refname.starts_with("refs/notes/")
424}
425
426fn receive_pack_reflog_entry(
427 format: ObjectFormat,
428 old_oid: ObjectId,
429 new_oid: ObjectId,
430) -> ReflogEntry {
431 let old_oid = if old_oid.is_null() {
432 ObjectId::null(format)
433 } else {
434 old_oid
435 };
436 ReflogEntry {
437 old_oid,
438 new_oid,
439 committer: receive_pack_reflog_committer(),
440 message: b"push".to_vec(),
441 }
442}
443
444fn receive_pack_reflog_committer() -> Vec<u8> {
445 let seconds = SystemTime::now()
446 .duration_since(UNIX_EPOCH)
447 .map(|duration| duration.as_secs())
448 .unwrap_or(0);
449 format!("Git Rs <sley@example.invalid> {seconds} +0000").into_bytes()
450}
451
452pub fn receive_pack_reachable_pack_into_local_repository(
459 remote_git_dir: &Path,
460 format: ObjectFormat,
461 request: &ReceivePackPushRequest,
462 source_db: &FileObjectDatabase,
463 starts: Vec<ObjectId>,
464 excluded: HashSet<ObjectId>,
465) -> Result<ReceivePackReportStatus> {
466 let remote_store = FileRefStore::new(remote_git_dir, format);
467 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
468 let mut starts = Some(starts);
469 let deletes_applied_with_updates = std::cell::RefCell::new(HashSet::<String>::new());
470 apply_receive_pack_push_request(
471 &receive_pack_features(format),
472 request,
473 |name| match remote_store.read_ref(name)? {
474 Some(RefTarget::Direct(oid)) => Ok(Some(oid)),
475 Some(RefTarget::Symbolic(_)) | None => Ok(None),
476 },
477 |_| {
478 let starts = starts.take().ok_or_else(|| {
479 GitError::InvalidFormat("receive-pack attempted to install pack twice".into())
480 })?;
481 build_and_install_reachable_pack(
482 source_db,
483 &remote_db,
484 format,
485 starts,
486 &excluded,
487 RawPackInstallOptions { promisor: false },
488 )?;
489 Ok(())
490 },
491 |oid| remote_db.contains(oid),
492 |commands| {
493 let applied = apply_receive_pack_ref_transaction(
494 remote_git_dir,
495 format,
496 &remote_store,
497 commands,
498 &request.commands.commands,
499 )?;
500 deletes_applied_with_updates.borrow_mut().extend(applied);
501 Ok(())
502 },
503 |command| {
504 if deletes_applied_with_updates
505 .borrow()
506 .contains(command.name.as_str())
507 {
508 return Ok(());
509 }
510 remote_store
511 .delete_ref_checked(DeleteRef {
512 name: command.name.clone(),
513 expected_old: (!command.old_id.is_null()).then_some(command.old_id),
514 reflog: None,
515 })
516 .map(|_| ())
517 .map_err(|err| GitError::Transaction(err.to_string()))
518 },
519 )
520}
521
522fn apply_receive_pack_ref_transaction(
523 remote_git_dir: &Path,
524 format: ObjectFormat,
525 store: &FileRefStore,
526 updates: &[ReceivePackCommand],
527 all_commands: &[ReceivePackCommand],
528) -> Result<HashSet<String>> {
529 let updates = canonical_receive_pack_update_commands(store, updates)?;
530 let deletes = all_commands
531 .iter()
532 .filter(|command| command.new_id.is_null())
533 .collect::<Vec<_>>();
534 let mut tx = store.transaction();
535 for command in &deletes {
536 tx.delete_with_precondition(
537 command.name.clone(),
538 RefDeletePrecondition::Direct((!command.old_id.is_null()).then_some(command.old_id)),
539 None,
540 );
541 }
542 let log_updates = receive_pack_log_all_ref_updates(remote_git_dir);
543 for command in &updates {
544 let precondition = if command.old_id.is_null() {
545 RefPrecondition::MustNotExist
546 } else {
547 RefPrecondition::MustExistAndMatch(RefTarget::Direct(command.old_id))
548 };
549 let reflog = if log_updates && receive_pack_should_write_reflog(&command.name) {
550 Some(receive_pack_reflog_entry(
551 format,
552 command.old_id,
553 command.new_id,
554 ))
555 } else {
556 None
557 };
558 tx.update_to(
559 command.name.clone(),
560 RefTarget::Direct(command.new_id),
561 precondition,
562 reflog,
563 );
564 }
565 tx.commit()?;
566 Ok(deletes
567 .into_iter()
568 .map(|command| command.name.clone())
569 .collect())
570}
571
572fn canonical_receive_pack_update_commands(
573 store: &FileRefStore,
574 commands: &[ReceivePackCommand],
575) -> Result<Vec<ReceivePackCommand>> {
576 let mut by_actual = HashMap::<String, ObjectId>::new();
577 let mut canonical = Vec::with_capacity(commands.len());
578 for command in commands {
579 let name = match store.read_ref(&command.name)? {
580 Some(RefTarget::Symbolic(target)) => target,
581 Some(RefTarget::Direct(_)) | None => command.name.clone(),
582 };
583 if let Some(existing) = by_actual.get(&name) {
584 if existing != &command.new_id {
585 return Err(GitError::Command("refusing inconsistent update".into()));
586 }
587 } else {
588 by_actual.insert(name.clone(), command.new_id);
589 }
590 canonical.push(ReceivePackCommand {
591 old_id: command.old_id,
592 new_id: command.new_id,
593 name,
594 });
595 }
596 Ok(canonical)
597}
598
599pub fn local_fetch_advertisements(
602 git_dir: &Path,
603 format: ObjectFormat,
604) -> Result<Vec<RefAdvertisement>> {
605 let store = FileRefStore::new(git_dir, format);
606 let mut advertisements = Vec::new();
607 if let Some(target) = store.read_ref("HEAD")? {
608 let reference = Ref {
609 name: "HEAD".to_string(),
610 target,
611 };
612 if let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? {
613 advertisements.push(RefAdvertisement {
614 oid,
615 name: reference.name,
616 capabilities: Vec::new(),
617 });
618 }
619 }
620 for reference in store.list_refs()? {
621 let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? else {
622 continue;
623 };
624 advertisements.push(RefAdvertisement {
625 oid,
626 name: reference.name,
627 capabilities: Vec::new(),
628 });
629 }
630 Ok(advertisements)
631}
632
633pub fn local_have_oids(git_dir: &Path, format: ObjectFormat) -> Result<Vec<ObjectId>> {
637 let mut seen = HashSet::new();
638 let mut haves = Vec::new();
639 for advertisement in local_fetch_advertisements(git_dir, format)? {
640 if seen.insert(advertisement.oid) {
641 haves.push(advertisement.oid);
642 }
643 }
644 let db = FileObjectDatabase::from_git_dir(git_dir, format);
645 for oid in db.object_ids()? {
646 if seen.insert(oid) {
647 haves.push(oid);
648 }
649 }
650 Ok(haves)
651}
652
653#[derive(Debug, Clone)]
660pub struct LocalDeepenPlan {
661 pub depth: u32,
665 pub deepen_since: bool,
667 pub deepen_not: usize,
669 pub client_shallow: Vec<ObjectId>,
672 pub shallow_info: Vec<ProtocolV2FetchShallowInfo>,
675 pub excluded: HashSet<ObjectId>,
680 pub extra_wants: Vec<ObjectId>,
684}
685
686fn peel_to_commit<R: ObjectReader>(
690 remote_db: &R,
691 format: ObjectFormat,
692 oid: &ObjectId,
693) -> Result<Option<ObjectId>> {
694 let mut oid = *oid;
695 loop {
696 let object = remote_db.read_object(&oid)?;
697 match object.object_type {
698 ObjectType::Commit => return Ok(Some(oid)),
699 ObjectType::Tag => oid = Tag::parse_ref(format, &object.body)?.object,
700 _ => return Ok(None),
701 }
702 }
703}
704
705pub fn compute_local_deepen<R: ObjectReader>(
718 remote_db: &R,
719 format: ObjectFormat,
720 heads: &[ObjectId],
721 client_shallow: Vec<ObjectId>,
722 depth: u32,
723 deepen_relative: bool,
724) -> Result<LocalDeepenPlan> {
725 let depth = if deepen_relative && depth < INFINITE_DEPTH {
728 depth.saturating_add(client_shallow_min_depth(
729 remote_db,
730 format,
731 heads,
732 &client_shallow,
733 )?)
734 } else {
735 depth
736 };
737 let mut min_depth: HashMap<ObjectId, u32> = HashMap::new();
738 let mut queue: VecDeque<ObjectId> = VecDeque::new();
739 for head in heads {
740 let Some(commit) = peel_to_commit(remote_db, format, head)? else {
741 continue;
742 };
743 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(commit) {
744 entry.insert(0);
745 queue.push_back(commit);
746 }
747 }
748 let mut boundary = Vec::new();
753 let mut boundary_parents = HashSet::new();
754 while let Some(oid) = queue.pop_front() {
755 let commit_depth = min_depth[&oid];
756 let object = remote_db.read_object(&oid)?;
757 let parents = sley_odb::grafted_parents(
758 remote_db,
759 &oid,
760 Commit::parse_ref(format, &object.body)?.parents,
761 );
762 if (depth != INFINITE_DEPTH && commit_depth + 1 >= depth)
766 || remote_db.is_shallow_graft(&oid)
767 {
768 boundary.push(oid);
769 boundary_parents.extend(parents);
770 continue;
771 }
772 for parent in parents {
773 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(parent) {
774 entry.insert(commit_depth + 1);
775 queue.push_back(parent);
776 }
777 }
778 }
779 let excluded = boundary_parents
783 .into_iter()
784 .filter(|parent| !min_depth.contains_key(parent))
785 .collect::<HashSet<_>>();
786
787 let client: HashSet<ObjectId> = client_shallow.iter().copied().collect();
788 let boundary_set: HashSet<ObjectId> = boundary.iter().copied().collect();
789 let mut shallow_info = Vec::new();
790 for oid in &boundary {
791 if !client.contains(oid) {
792 shallow_info.push(ProtocolV2FetchShallowInfo::Shallow(*oid));
793 }
794 }
795 let mut extra_wants = Vec::new();
796 for oid in &client_shallow {
797 let unshallowed = min_depth.contains_key(oid) && !boundary_set.contains(oid);
800 if !unshallowed {
801 continue;
802 }
803 shallow_info.push(ProtocolV2FetchShallowInfo::Unshallow(*oid));
804 let object = remote_db.read_object(oid)?;
805 extra_wants.extend(sley_odb::grafted_parents(
806 remote_db,
807 oid,
808 Commit::parse_ref(format, &object.body)?.parents,
809 ));
810 }
811 Ok(LocalDeepenPlan {
812 depth,
813 deepen_since: false,
814 deepen_not: 0,
815 client_shallow,
816 shallow_info,
817 excluded,
818 extra_wants,
819 })
820}
821
822pub const INFINITE_DEPTH: u32 = 0x7fff_ffff;
825
826fn client_shallow_min_depth<R: ObjectReader>(
830 remote_db: &R,
831 format: ObjectFormat,
832 heads: &[ObjectId],
833 client_shallow: &[ObjectId],
834) -> Result<u32> {
835 if client_shallow.is_empty() {
836 return Ok(0);
837 }
838 let client: HashSet<ObjectId> = client_shallow.iter().copied().collect();
839 let mut min_depth: HashMap<ObjectId, u32> = HashMap::new();
840 let mut queue: VecDeque<ObjectId> = VecDeque::new();
841 for head in heads {
842 let Some(commit) = peel_to_commit(remote_db, format, head)? else {
843 continue;
844 };
845 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(commit) {
846 entry.insert(1);
847 queue.push_back(commit);
848 }
849 }
850 let mut best: u32 = 0;
851 while let Some(oid) = queue.pop_front() {
852 let commit_depth = min_depth[&oid];
853 if client.contains(&oid) && (best == 0 || commit_depth < best) {
854 best = commit_depth;
855 }
856 let object = remote_db.read_object(&oid)?;
857 let parents = sley_odb::grafted_parents(
858 remote_db,
859 &oid,
860 Commit::parse_ref(format, &object.body)?.parents,
861 );
862 for parent in parents {
863 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(parent) {
864 entry.insert(commit_depth + 1);
865 queue.push_back(parent);
866 }
867 }
868 }
869 Ok(best)
870}
871
872pub fn compute_local_deepen_by_rev_list<R: ObjectReader>(
878 remote_db: &R,
879 format: ObjectFormat,
880 heads: &[ObjectId],
881 client_shallow: Vec<ObjectId>,
882 since: Option<i64>,
883 deepen_not: &[ObjectId],
884) -> Result<LocalDeepenPlan> {
885 let mut excluded_not: HashSet<ObjectId> = HashSet::new();
887 let mut queue: VecDeque<ObjectId> = VecDeque::new();
888 for tip in deepen_not {
889 if let Some(commit) = peel_to_commit(remote_db, format, tip)?
890 && excluded_not.insert(commit)
891 {
892 queue.push_back(commit);
893 }
894 }
895 while let Some(oid) = queue.pop_front() {
896 let object = remote_db.read_object(&oid)?;
897 for parent in sley_odb::grafted_parents(
898 remote_db,
899 &oid,
900 Commit::parse_ref(format, &object.body)?.parents,
901 ) {
902 if excluded_not.insert(parent) {
903 queue.push_back(parent);
904 }
905 }
906 }
907
908 let commit_time = |oid: &ObjectId| -> Result<i64> {
909 let object = remote_db.read_object(oid)?;
910 Ok(Commit::parse_ref(format, &object.body)?
911 .committer_signature()
912 .map(|signature| signature.time.seconds)
913 .unwrap_or(0))
914 };
915 let keeps = |oid: &ObjectId| -> Result<bool> {
916 if excluded_not.contains(oid) {
917 return Ok(false);
918 }
919 match since {
920 Some(since) => Ok(commit_time(oid)? >= since),
921 None => Ok(true),
922 }
923 };
924
925 let mut kept: HashSet<ObjectId> = HashSet::new();
928 let mut kept_order: Vec<ObjectId> = Vec::new();
929 let mut queue: VecDeque<ObjectId> = VecDeque::new();
930 for head in heads {
931 let Some(commit) = peel_to_commit(remote_db, format, head)? else {
932 continue;
933 };
934 if keeps(&commit)? && kept.insert(commit) {
935 kept_order.push(commit);
936 queue.push_back(commit);
937 }
938 }
939 while let Some(oid) = queue.pop_front() {
940 let object = remote_db.read_object(&oid)?;
941 for parent in sley_odb::grafted_parents(
942 remote_db,
943 &oid,
944 Commit::parse_ref(format, &object.body)?.parents,
945 ) {
946 if !kept.contains(&parent) && keeps(&parent)? {
947 kept.insert(parent);
948 kept_order.push(parent);
949 queue.push_back(parent);
950 }
951 }
952 }
953 if kept.is_empty() {
954 return Err(GitError::Command(
956 "no commits selected for shallow requests".into(),
957 ));
958 }
959
960 let mut boundary = Vec::new();
962 let mut boundary_set: HashSet<ObjectId> = HashSet::new();
963 let mut excluded: HashSet<ObjectId> = HashSet::new();
964 for oid in &kept_order {
965 let object = remote_db.read_object(oid)?;
966 let parents = sley_odb::grafted_parents(
967 remote_db,
968 oid,
969 Commit::parse_ref(format, &object.body)?.parents,
970 );
971 let mut is_boundary = false;
972 for parent in parents {
973 if !kept.contains(&parent) {
974 is_boundary = true;
975 excluded.insert(parent);
976 }
977 }
978 if is_boundary && boundary_set.insert(*oid) {
979 boundary.push(*oid);
980 }
981 }
982
983 let client: HashSet<ObjectId> = client_shallow.iter().copied().collect();
984 let mut shallow_info = Vec::new();
985 for oid in &boundary {
986 if !client.contains(oid) {
987 shallow_info.push(ProtocolV2FetchShallowInfo::Shallow(*oid));
988 }
989 }
990 let mut extra_wants = Vec::new();
991 for oid in &client_shallow {
992 let unshallowed = kept.contains(oid) && !boundary_set.contains(oid);
993 if !unshallowed {
994 continue;
995 }
996 shallow_info.push(ProtocolV2FetchShallowInfo::Unshallow(*oid));
997 let object = remote_db.read_object(oid)?;
998 extra_wants.extend(sley_odb::grafted_parents(
999 remote_db,
1000 oid,
1001 Commit::parse_ref(format, &object.body)?.parents,
1002 ));
1003 }
1004 Ok(LocalDeepenPlan {
1005 depth: 0,
1006 deepen_since: since.is_some(),
1007 deepen_not: deepen_not.len(),
1008 client_shallow,
1009 shallow_info,
1010 excluded,
1011 extra_wants,
1012 })
1013}
1014
1015#[allow(clippy::too_many_arguments)]
1028pub fn install_fetch_pack_via_local_upload_pack(
1029 git_dir: &Path,
1030 remote_git_dir: &Path,
1031 format: ObjectFormat,
1032 wants: Vec<ObjectId>,
1033 deepen: Option<&LocalDeepenPlan>,
1034 promisor: bool,
1035 record_promisor_refs: bool,
1036 filter: Option<sley_odb::PackObjectFilter>,
1037 refetch: bool,
1038 unpack_limit: Option<usize>,
1039) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
1040 if wants.is_empty() {
1041 return Ok(Vec::new());
1042 }
1043 let local_db = FileObjectDatabase::from_git_dir(git_dir, format);
1044 let all_wants_present = wants
1045 .iter()
1046 .map(|want| local_db.contains(want))
1047 .collect::<Result<Vec<_>>>()?
1048 .into_iter()
1049 .all(|contains| contains);
1050 let deepen_noop = match deepen {
1051 Some(plan) => plan.shallow_info.is_empty() && plan.extra_wants.is_empty(),
1052 None => true,
1053 };
1054 if all_wants_present && deepen_noop && !refetch {
1055 sley_protocol::trace_packet_write_payload(b"0000");
1056 return Ok(Vec::new());
1057 }
1058
1059 let request = UploadPackRequest {
1060 wants,
1061 capabilities: deepen
1064 .map(|_| {
1065 vec![Capability {
1066 name: "shallow".into(),
1067 value: None,
1068 }]
1069 })
1070 .unwrap_or_default(),
1071 shallow: deepen
1072 .map(|plan| plan.client_shallow.clone())
1073 .unwrap_or_default(),
1074 deepen: deepen.and_then(|plan| (plan.depth > 0).then_some(plan.depth)),
1075 ..UploadPackRequest::default()
1076 };
1077 let mut encoded_request = Vec::new();
1078 write_upload_pack_request(&mut encoded_request, Some(&request))?;
1079 let decoded_request = read_upload_pack_request(format, &mut encoded_request.as_slice())?
1080 .ok_or_else(|| GitError::InvalidFormat("encoded upload-pack request was empty".into()))?;
1081
1082 let haves = if refetch {
1083 Vec::new()
1084 } else {
1085 local_have_oids(git_dir, format)?
1086 };
1087 let negotiation = UploadPackNegotiationRequest { haves, done: true };
1088 let mut encoded_negotiation = Vec::new();
1089 write_upload_pack_negotiation_request(&mut encoded_negotiation, &negotiation)?;
1090 let decoded_negotiation =
1091 read_upload_pack_negotiation_request(format, &mut encoded_negotiation.as_slice())?;
1092 sley_core::trace2::data("negotiation_v2", "total_rounds", 1);
1093
1094 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
1095 for want in &decoded_request.wants {
1096 if !remote_db.contains(want)? {
1097 return Err(GitError::InvalidObject(format!(
1098 "upload-pack requested missing object {want}"
1099 )));
1100 }
1101 }
1102 let known_haves = decoded_negotiation
1103 .haves
1104 .into_iter()
1105 .filter_map(|oid| match remote_db.contains(&oid) {
1106 Ok(true) => Some(Ok(oid)),
1107 Ok(false) => None,
1108 Err(err) => Some(Err(err)),
1109 })
1110 .collect::<Result<Vec<_>>>()?;
1111 trace2_fetch_info(
1115 known_haves.len(),
1116 decoded_request.wants.len(),
1117 deepen.map(|plan| plan.depth).unwrap_or(0),
1118 deepen.map(|plan| plan.client_shallow.len()).unwrap_or(0),
1119 deepen.is_some_and(|plan| plan.deepen_since),
1120 deepen.map(|plan| plan.deepen_not).unwrap_or(0),
1121 filter.as_ref(),
1122 );
1123 let mut excluded = match deepen {
1128 Some(plan) => {
1129 let cut: HashSet<ObjectId> = plan.client_shallow.iter().copied().collect();
1130 sley_odb::collect_reachable_object_ids_with_cut(&remote_db, format, known_haves, &cut)?
1131 }
1132 None => collect_reachable_object_ids(&remote_db, format, known_haves)?,
1133 };
1134 let mut starts = decoded_request.wants;
1135 let promisor_ref_wants = starts.iter().copied().collect::<HashSet<_>>();
1136 for want in &starts {
1137 excluded.remove(want);
1138 }
1139 if let Some(plan) = deepen {
1140 excluded.extend(plan.excluded.iter().copied());
1143 starts.extend(plan.extra_wants.iter().copied());
1144 }
1145 let install = build_and_install_reachable_pack_filtered(
1146 &remote_db,
1147 &local_db,
1148 format,
1149 starts,
1150 &excluded,
1151 RawPackInstallOptions { promisor },
1152 filter.clone(),
1153 unpack_limit,
1154 )?;
1155 if promisor
1156 && record_promisor_refs
1157 && let Some(result) = install
1158 && let Some(promisor_path) = result.promisor_path
1159 {
1160 append_promisor_ref_lines(&promisor_path, remote_git_dir, format, &promisor_ref_wants)?;
1161 }
1162 Ok(deepen
1163 .map(|plan| plan.shallow_info.clone())
1164 .unwrap_or_default())
1165}
1166
1167fn append_promisor_ref_lines(
1168 promisor_path: &Path,
1169 remote_git_dir: &Path,
1170 format: ObjectFormat,
1171 wanted: &HashSet<ObjectId>,
1172) -> Result<()> {
1173 if wanted.is_empty() {
1174 return Ok(());
1175 }
1176 let store = FileRefStore::new(remote_git_dir, format);
1177 let mut lines = Vec::new();
1178 if let Some(head_target) = store.read_ref("HEAD")? {
1179 let head = Ref {
1180 name: "HEAD".into(),
1181 target: head_target,
1182 };
1183 if let Some((oid, _)) = resolve_for_each_ref_target(&store, &head)?
1184 && wanted.contains(&oid)
1185 {
1186 lines.push(format!("{oid} HEAD\n"));
1187 }
1188 }
1189 for reference in store.list_refs()? {
1190 let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? else {
1191 continue;
1192 };
1193 if wanted.contains(&oid) {
1194 lines.push(format!("{oid} {}\n", reference.name));
1195 }
1196 }
1197 if lines.is_empty() {
1198 return Ok(());
1199 }
1200 lines.sort();
1201 let mut file = fs::OpenOptions::new().append(true).open(promisor_path)?;
1202 use std::io::Write as _;
1203 for line in lines {
1204 file.write_all(line.as_bytes())?;
1205 }
1206 Ok(())
1207}
1208
1209fn trace2_fetch_info(
1213 haves: usize,
1214 wants: usize,
1215 depth: u32,
1216 shallows: usize,
1217 deepen_since: bool,
1218 deepen_not: usize,
1219 filter: Option<&sley_odb::PackObjectFilter>,
1220) {
1221 let Some(path) = std::env::var_os("GIT_TRACE2_EVENT") else {
1222 return;
1223 };
1224 if path.is_empty() {
1225 return;
1226 }
1227 let filter_json = match filter {
1228 Some(sley_odb::PackObjectFilter::BlobNone) => "\"blob:none\"".to_string(),
1229 Some(sley_odb::PackObjectFilter::BlobLimit(limit)) => {
1230 format!("\"blob:limit={limit}\"")
1231 }
1232 Some(sley_odb::PackObjectFilter::TreeDepth(depth)) => {
1233 format!("\"tree:{depth}\"")
1234 }
1235 Some(sley_odb::PackObjectFilter::SparsePathSet(_)) => "\"sparse:oid\"".to_string(),
1236 None => "null".to_string(),
1237 };
1238 let line = format!(
1239 "{{\"event\":\"data_json\",\"thread\":\"main\",\"category\":\"upload-pack\",\"key\":\"fetch-info\",\"value\":{{\"haves\":{haves},\"wants\":{wants},\"want-refs\":0,\"depth\":{depth},\"shallows\":{shallows},\"deepen-since\":{deepen_since},\"deepen-not\":{deepen_not},\"deepen-relative\":false,\"filter\":{filter_json}}}}}\n"
1240 );
1241 if let Ok(mut file) = std::fs::OpenOptions::new()
1242 .create(true)
1243 .append(true)
1244 .open(&path)
1245 {
1246 use std::io::Write as _;
1247 let _ = file.write_all(line.as_bytes());
1248 }
1249}
1250
1251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1262enum LsRefsUnbornConfig {
1263 Ignore,
1264 Allow,
1265 Advertise,
1266}
1267
1268fn lsrefs_unborn_config(config: &GitConfig) -> LsRefsUnbornConfig {
1269 match config.get("lsrefs", None, "unborn") {
1270 Some("ignore") => LsRefsUnbornConfig::Ignore,
1271 Some("allow") => LsRefsUnbornConfig::Allow,
1272 Some("advertise") | None => LsRefsUnbornConfig::Advertise,
1273 Some(_) => LsRefsUnbornConfig::Advertise,
1274 }
1275}
1276
1277fn upload_pack_blob_packfile_uri_configured(config: &GitConfig) -> bool {
1278 config
1279 .get_all("uploadpack", None, "blobpackfileuri")
1280 .into_iter()
1281 .any(|value| value.is_some_and(|value| !value.is_empty()))
1282}
1283
1284fn upload_pack_v2_capabilities(
1288 format: ObjectFormat,
1289 config: &GitConfig,
1290) -> Result<Vec<Capability>> {
1291 let mut capabilities = vec![
1292 Capability {
1293 name: "agent".into(),
1294 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
1295 },
1296 encode_protocol_v2_ls_refs_capability(&ProtocolV2LsRefsFeatures {
1297 unborn: lsrefs_unborn_config(config) == LsRefsUnbornConfig::Advertise,
1298 unknown: Vec::new(),
1299 })?,
1300 encode_protocol_v2_fetch_capability(&ProtocolV2FetchFeatures {
1301 shallow: true,
1302 wait_for_done: true,
1303 filter: config
1304 .get_bool("uploadpack", None, "allowfilter")
1305 .unwrap_or(false),
1306 packfile_uris: upload_pack_blob_packfile_uri_configured(config),
1307 ..ProtocolV2FetchFeatures::default()
1308 })?,
1309 Capability {
1310 name: "server-option".into(),
1311 value: None,
1312 },
1313 Capability {
1314 name: "object-format".into(),
1315 value: Some(format.name().into()),
1316 },
1317 ];
1318 if config
1319 .get_bool("transfer", None, "advertisesid")
1320 .unwrap_or(false)
1321 {
1322 capabilities.push(Capability {
1323 name: "session-id".into(),
1324 value: Some("sley".into()),
1325 });
1326 }
1327 Ok(capabilities)
1328}
1329
1330fn head_symref_target(store: &FileRefStore) -> Result<Option<String>> {
1334 match store.read_ref("HEAD")? {
1335 Some(RefTarget::Symbolic(name)) => Ok(Some(name)),
1336 _ => Ok(None),
1337 }
1338}
1339
1340fn local_ls_refs_v2_records(
1344 git_dir: &Path,
1345 format: ObjectFormat,
1346 request: &ProtocolV2LsRefsRequest,
1347 config: &GitConfig,
1348) -> Result<Vec<ProtocolV2LsRefsRecord>> {
1349 let store = FileRefStore::new(git_dir, format);
1350 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1351 let head_symref = head_symref_target(&store)?;
1352
1353 let mut entries: Vec<(String, ObjectId, Option<String>)> = Vec::new();
1356 if let Some(target) = store.read_ref("HEAD")? {
1357 let reference = Ref {
1358 name: "HEAD".to_string(),
1359 target,
1360 };
1361 if let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? {
1362 entries.push(("HEAD".to_string(), oid, head_symref.clone()));
1363 } else if request.unborn && lsrefs_unborn_config(config) != LsRefsUnbornConfig::Ignore {
1364 entries.push((
1367 "HEAD".to_string(),
1368 ObjectId::null(format),
1369 head_symref.clone(),
1370 ));
1371 }
1372 }
1373 for reference in store.list_refs()? {
1374 let name = reference.name.clone();
1375 let Some((oid, symref)) = resolve_for_each_ref_target(&store, &reference)? else {
1376 continue;
1377 };
1378 entries.push((name, oid, symref));
1379 }
1380
1381 let matches_prefix = |name: &str| -> bool {
1382 if request.ref_prefixes.is_empty() {
1383 return true;
1384 }
1385 request
1386 .ref_prefixes
1387 .iter()
1388 .any(|prefix| name.starts_with(prefix.as_str()))
1389 };
1390
1391 let mut records = Vec::new();
1392 for (name, oid, symref) in entries {
1393 if !matches_prefix(&name) {
1394 continue;
1395 }
1396 if name == "HEAD" && oid == ObjectId::null(format) {
1398 records.push(ProtocolV2LsRefsRecord::Unborn {
1399 name,
1400 symref_target: if request.symrefs { symref } else { None },
1401 attributes: Vec::new(),
1402 });
1403 continue;
1404 }
1405 let peeled = if request.peel {
1406 let object = db.read_object(&oid)?;
1407 if object.object_type == ObjectType::Tag {
1408 Some(sley_rev::peel_tags(&db, format, &oid)?)
1409 } else {
1410 None
1411 }
1412 } else {
1413 None
1414 };
1415 let symref_target = if request.symrefs { symref } else { None };
1416 records.push(ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
1417 oid,
1418 name,
1419 peeled,
1420 symref_target,
1421 attributes: Vec::new(),
1422 }));
1423 }
1424 Ok(records)
1425}
1426
1427fn packfile_section_lines(pack: &[u8]) -> Vec<Vec<u8>> {
1433 let chunk = PKT_LINE_MAX_PAYLOAD_LEN - 1;
1434 let mut lines = Vec::new();
1435 for slice in pack.chunks(chunk) {
1436 let mut payload = Vec::with_capacity(slice.len() + 1);
1437 payload.push(1u8); payload.extend_from_slice(slice);
1439 lines.push(payload);
1440 }
1441 lines
1442}
1443
1444fn local_fetch_v2_sections(
1450 git_dir: &Path,
1451 format: ObjectFormat,
1452 request: &ProtocolV2FetchRequest,
1453) -> Result<Vec<ProtocolV2FetchResponseSection>> {
1454 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1455
1456 let mut sections = Vec::new();
1457
1458 if !request.done {
1464 let mut acks: Vec<ProtocolV2FetchAcknowledgment> = Vec::new();
1465 for have in &request.haves {
1466 if db.contains(have)? {
1467 acks.push(ProtocolV2FetchAcknowledgment::Ack(*have));
1468 }
1469 }
1470 if acks.is_empty() {
1471 acks.push(ProtocolV2FetchAcknowledgment::Nak);
1472 }
1473 sections.push(ProtocolV2FetchResponseSection::Acknowledgments(acks));
1474 if !request.wait_for_done {
1477 return Ok(sections);
1478 }
1479 }
1480
1481 if !request.want_refs.is_empty() {
1483 let store = FileRefStore::new(git_dir, format);
1484 let mut wanted = Vec::new();
1485 for name in &request.want_refs {
1486 let reference = Ref {
1487 name: name.clone(),
1488 target: store
1489 .read_ref(name)?
1490 .ok_or_else(|| GitError::not_found(format!("want-ref {name}")))?,
1491 };
1492 let (oid, _) = resolve_for_each_ref_target(&store, &reference)?
1493 .ok_or_else(|| GitError::not_found(format!("want-ref {name}")))?;
1494 wanted.push(sley_protocol::ProtocolV2FetchWantedRef {
1495 oid,
1496 name: name.clone(),
1497 });
1498 }
1499 sections.push(ProtocolV2FetchResponseSection::WantedRefs(wanted));
1500 }
1501
1502 let mut wants: Vec<ObjectId> = request.wants.clone();
1504 if !request.want_refs.is_empty()
1505 && let Some(ProtocolV2FetchResponseSection::WantedRefs(wanted)) = sections
1506 .iter()
1507 .find(|s| matches!(s, ProtocolV2FetchResponseSection::WantedRefs(_)))
1508 {
1509 for w in wanted {
1510 wants.push(w.oid);
1511 }
1512 }
1513
1514 let mut known_haves: Vec<ObjectId> = Vec::new();
1516 for have in &request.haves {
1517 if db.contains(have)? {
1518 known_haves.push(*have);
1519 }
1520 }
1521 let excluded = collect_reachable_object_ids(&db, format, known_haves)?;
1522 let pack = build_reachable_pack(&db, format, wants, &excluded)?
1523 .map(|pack| pack.pack)
1524 .unwrap_or_default();
1525
1526 sections.push(ProtocolV2FetchResponseSection::Packfile(
1527 packfile_section_lines(&pack),
1528 ));
1529 Ok(sections)
1530}
1531
1532pub fn serve_upload_pack_v2(
1538 git_dir: &Path,
1539 format: ObjectFormat,
1540 reader: &mut impl std::io::Read,
1541 writer: &mut impl std::io::Write,
1542) -> Result<()> {
1543 let config = sley_config::read_repo_config(git_dir, None).unwrap_or_default();
1544 serve_upload_pack_v2_with_config(git_dir, format, &config, reader, writer)
1545}
1546
1547pub fn serve_upload_pack_v2_with_config(
1548 git_dir: &Path,
1549 format: ObjectFormat,
1550 config: &GitConfig,
1551 reader: &mut impl std::io::Read,
1552 writer: &mut impl std::io::Write,
1553) -> Result<()> {
1554 let handshake = TransportHandshake {
1555 protocol: ProtocolVersion::V2,
1556 capabilities: upload_pack_v2_capabilities(format, config)?,
1557 };
1558 write_protocol_v2_advertisement(writer, &handshake)?;
1559 writer.flush()?;
1560
1561 loop {
1566 let request = match read_protocol_v2_command_request(reader) {
1567 Ok(request) => request,
1568 Err(GitError::InvalidFormat(message))
1569 if message == "pkt-line stream ended before control packet"
1570 || message == "protocol v2 command request must start with a command line" =>
1571 {
1572 break;
1573 }
1574 Err(err) => return Err(err),
1575 };
1576 match classify_protocol_v2_command_request(&handshake, format, &request)? {
1577 sley_protocol::ProtocolV2Command::LsRefs(ls_refs) => {
1578 let records = local_ls_refs_v2_records(git_dir, format, &ls_refs, config)?;
1579 write_protocol_v2_ls_refs_response(writer, &records)?;
1580 writer.flush()?;
1581 }
1582 sley_protocol::ProtocolV2Command::Fetch(fetch) => {
1583 let sections = local_fetch_v2_sections(git_dir, format, &fetch)?;
1584 write_protocol_v2_fetch_response(writer, §ions)?;
1585 writer.flush()?;
1586 }
1587 sley_protocol::ProtocolV2Command::ObjectInfo(_)
1588 | sley_protocol::ProtocolV2Command::Unknown(_) => {
1589 return Err(GitError::InvalidFormat(format!(
1590 "unsupported protocol v2 command {}",
1591 request.command
1592 )));
1593 }
1594 }
1595 }
1596 Ok(())
1597}