1use std::collections::{HashMap, HashSet, VecDeque};
13use std::path::Path;
14
15use sley_core::{
16 Capability, GitError, ObjectFormat, ObjectId, Result, UPSTREAM_GIT_COMPAT_VERSION,
17};
18use sley_object::{Commit, ObjectType, Tag};
19use sley_odb::{
20 FileObjectDatabase, ObjectReader, RawPackInstallOptions, build_and_install_reachable_pack,
21 build_and_install_reachable_pack_filtered, build_reachable_pack, collect_reachable_object_ids,
22};
23use sley_protocol::{
24 PKT_LINE_MAX_PAYLOAD_LEN, ProtocolV2FetchAcknowledgment,
25 ProtocolV2FetchRequest, ProtocolV2FetchResponseSection, ProtocolV2FetchShallowInfo,
26 ProtocolV2LsRefsRecord, ProtocolV2LsRefsRef, ProtocolV2LsRefsRequest, ProtocolVersion,
27 ReceivePackFeatures, ReceivePackPushRequest, ReceivePackReportStatus, ReceivePackRequest,
28 RefAdvertisement, SideBandChannel, SideBandPacket, TransportHandshake, UploadPackFeatures,
29 UploadPackNegotiationRequest, UploadPackPackfileResponse, UploadPackRawPackfileResponse,
30 UploadPackRequest, apply_receive_pack_push_request, build_upload_pack_raw_packfile_response,
31 encode_receive_pack_features, encode_upload_pack_features,
32 read_protocol_v2_command_request, read_upload_pack_negotiation_request, read_upload_pack_request,
33 write_protocol_v2_advertisement, write_protocol_v2_fetch_response,
34 write_protocol_v2_ls_refs_response, write_upload_pack_negotiation_request,
35 write_upload_pack_request,
36};
37use sley_refs::{DeleteRef, FileRefStore, Ref, RefPrecondition, RefTarget};
38
39fn zero_oid(format: ObjectFormat) -> Result<ObjectId> {
42 Ok(ObjectId::null(format))
43}
44
45fn resolve_for_each_ref_target(
48 store: &FileRefStore,
49 reference: &Ref,
50) -> Result<Option<(ObjectId, Option<String>)>> {
51 let mut target = reference.target.clone();
52 let mut symref = None;
53 for _ in 0..5 {
54 match target {
55 RefTarget::Direct(oid) => return Ok(Some((oid, symref))),
56 RefTarget::Symbolic(name) => {
57 symref.get_or_insert_with(|| name.clone());
58 let Some(next) = store.read_ref(&name)? else {
59 return Ok(None);
60 };
61 target = next;
62 }
63 }
64 }
65 Ok(None)
66}
67
68pub fn upload_pack_features(git_dir: &Path, format: ObjectFormat) -> Result<UploadPackFeatures> {
71 let store = FileRefStore::new(git_dir, format);
72 let mut symrefs = Vec::new();
73 if let Some(RefTarget::Symbolic(target)) = store.read_ref("HEAD")? {
74 symrefs.push(format!("HEAD:{target}"));
75 }
76 Ok(UploadPackFeatures {
77 object_format: Some(format),
78 side_band_64k: true,
79 symrefs,
80 ..UploadPackFeatures::default()
81 })
82}
83
84pub fn upload_pack_request_uses_sideband(request: &UploadPackRequest) -> bool {
86 request
87 .capabilities
88 .iter()
89 .any(|capability| matches!(capability.name.as_str(), "side-band" | "side-band-64k"))
90}
91
92pub fn upload_pack_sideband_response(
95 response: UploadPackRawPackfileResponse,
96) -> UploadPackPackfileResponse {
97 let mut sideband = Vec::new();
98 let chunk_len = PKT_LINE_MAX_PAYLOAD_LEN - 1;
99 for chunk in response.packfile.chunks(chunk_len) {
100 sideband.push(SideBandPacket {
101 channel: SideBandChannel::Data,
102 data: chunk.to_vec(),
103 });
104 }
105 UploadPackPackfileResponse {
106 acknowledgments: response.acknowledgments,
107 sideband,
108 }
109}
110
111pub fn attach_upload_pack_capabilities(
114 advertisements: &mut Vec<RefAdvertisement>,
115 format: ObjectFormat,
116 features: &UploadPackFeatures,
117) -> Result<()> {
118 let capabilities = encode_upload_pack_features(features)?;
119 if let Some(first) = advertisements.first_mut() {
120 first.capabilities = capabilities;
121 } else {
122 advertisements.push(RefAdvertisement {
123 oid: zero_oid(format)?,
124 name: "capabilities^{}".into(),
125 capabilities,
126 });
127 }
128 Ok(())
129}
130
131pub fn upload_pack_from_local_repository(
135 git_dir: &Path,
136 format: ObjectFormat,
137 features: &UploadPackFeatures,
138 request: UploadPackRequest,
139 haves: HashSet<ObjectId>,
140) -> Result<UploadPackRawPackfileResponse> {
141 let db = FileObjectDatabase::from_git_dir(git_dir, format);
142 build_upload_pack_raw_packfile_response(
143 features,
144 request,
145 haves,
146 |oid| db.contains(oid),
147 |wants, known_haves| {
148 let excluded = collect_reachable_object_ids(&db, format, known_haves)?;
149 build_reachable_pack(&db, format, wants, &excluded)
150 .map(|pack| pack.map(|pack| pack.pack))
151 },
152 )
153}
154
155pub fn receive_pack_features(format: ObjectFormat) -> ReceivePackFeatures {
158 ReceivePackFeatures {
159 report_status: true,
160 delete_refs: true,
161 ofs_delta: true,
162 push_options: true,
163 quiet: true,
164 object_format: Some(format),
165 ..ReceivePackFeatures::default()
166 }
167}
168
169pub fn receive_pack_request_uses_push_options(request: &ReceivePackRequest) -> bool {
172 request
173 .capabilities
174 .iter()
175 .any(|capability| capability.name == "push-options")
176}
177
178pub fn attach_receive_pack_capabilities(
181 advertisements: &mut Vec<RefAdvertisement>,
182 format: ObjectFormat,
183 features: &ReceivePackFeatures,
184) -> Result<()> {
185 let capabilities = encode_receive_pack_features(features)?;
186 if let Some(first) = advertisements.first_mut() {
187 first.capabilities = capabilities;
188 } else {
189 advertisements.push(RefAdvertisement {
190 oid: zero_oid(format)?,
191 name: "capabilities^{}".into(),
192 capabilities,
193 });
194 }
195 Ok(())
196}
197
198pub fn receive_pack_into_local_repository(
202 remote_git_dir: &Path,
203 format: ObjectFormat,
204 request: &ReceivePackPushRequest,
205) -> Result<ReceivePackReportStatus> {
206 let remote_store = FileRefStore::new(remote_git_dir, format);
207 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
208 apply_receive_pack_push_request(
209 &receive_pack_features(format),
210 request,
211 |name| match remote_store.read_ref(name)? {
212 Some(RefTarget::Direct(oid)) => Ok(Some(oid)),
213 Some(RefTarget::Symbolic(_)) | None => Ok(None),
214 },
215 |packfile| remote_db.install_raw_pack(packfile).map(|_| ()),
216 |oid| remote_db.contains(oid),
217 |commands| {
218 let mut tx = remote_store.transaction();
219 for command in commands {
220 let precondition = if command.old_id.is_null() {
221 RefPrecondition::MustNotExist
222 } else {
223 RefPrecondition::MustExistAndMatch(RefTarget::Direct(command.old_id))
224 };
225 tx.update_to(
226 command.name.clone(),
227 RefTarget::Direct(command.new_id),
228 precondition,
229 None,
230 );
231 }
232 tx.commit()
233 },
234 |command| {
235 remote_store
236 .delete_ref_checked(DeleteRef {
237 name: command.name.clone(),
238 expected_old: (!command.old_id.is_null()).then_some(command.old_id),
239 reflog: None,
240 })
241 .map(|_| ())
242 .map_err(|err| GitError::Transaction(err.to_string()))
243 },
244 )
245}
246
247pub fn receive_pack_reachable_pack_into_local_repository(
254 remote_git_dir: &Path,
255 format: ObjectFormat,
256 request: &ReceivePackPushRequest,
257 source_db: &FileObjectDatabase,
258 starts: Vec<ObjectId>,
259 excluded: HashSet<ObjectId>,
260) -> Result<ReceivePackReportStatus> {
261 let remote_store = FileRefStore::new(remote_git_dir, format);
262 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
263 let mut starts = Some(starts);
264 apply_receive_pack_push_request(
265 &receive_pack_features(format),
266 request,
267 |name| match remote_store.read_ref(name)? {
268 Some(RefTarget::Direct(oid)) => Ok(Some(oid)),
269 Some(RefTarget::Symbolic(_)) | None => Ok(None),
270 },
271 |_| {
272 let starts = starts.take().ok_or_else(|| {
273 GitError::InvalidFormat("receive-pack attempted to install pack twice".into())
274 })?;
275 build_and_install_reachable_pack(
276 source_db,
277 &remote_db,
278 format,
279 starts,
280 &excluded,
281 RawPackInstallOptions { promisor: false },
282 )?;
283 Ok(())
284 },
285 |oid| remote_db.contains(oid),
286 |commands| {
287 let mut tx = remote_store.transaction();
288 for command in commands {
289 let precondition = if command.old_id.is_null() {
290 RefPrecondition::MustNotExist
291 } else {
292 RefPrecondition::MustExistAndMatch(RefTarget::Direct(command.old_id))
293 };
294 tx.update_to(
295 command.name.clone(),
296 RefTarget::Direct(command.new_id),
297 precondition,
298 None,
299 );
300 }
301 tx.commit()
302 },
303 |command| {
304 remote_store
305 .delete_ref_checked(DeleteRef {
306 name: command.name.clone(),
307 expected_old: (!command.old_id.is_null()).then_some(command.old_id),
308 reflog: None,
309 })
310 .map(|_| ())
311 .map_err(|err| GitError::Transaction(err.to_string()))
312 },
313 )
314}
315
316pub fn local_fetch_advertisements(
319 git_dir: &Path,
320 format: ObjectFormat,
321) -> Result<Vec<RefAdvertisement>> {
322 let store = FileRefStore::new(git_dir, format);
323 let mut advertisements = Vec::new();
324 if let Some(target) = store.read_ref("HEAD")? {
325 let reference = Ref {
326 name: "HEAD".to_string(),
327 target,
328 };
329 if let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? {
330 advertisements.push(RefAdvertisement {
331 oid,
332 name: reference.name,
333 capabilities: Vec::new(),
334 });
335 }
336 }
337 for reference in store.list_refs()? {
338 let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? else {
339 continue;
340 };
341 advertisements.push(RefAdvertisement {
342 oid,
343 name: reference.name,
344 capabilities: Vec::new(),
345 });
346 }
347 Ok(advertisements)
348}
349
350pub fn local_have_oids(git_dir: &Path, format: ObjectFormat) -> Result<Vec<ObjectId>> {
353 let mut seen = HashSet::new();
354 let mut haves = Vec::new();
355 for advertisement in local_fetch_advertisements(git_dir, format)? {
356 if seen.insert(advertisement.oid) {
357 haves.push(advertisement.oid);
358 }
359 }
360 Ok(haves)
361}
362
363#[derive(Debug, Clone)]
370pub struct LocalDeepenPlan {
371 pub depth: u32,
375 pub deepen_since: bool,
377 pub deepen_not: usize,
379 pub client_shallow: Vec<ObjectId>,
382 pub shallow_info: Vec<ProtocolV2FetchShallowInfo>,
385 pub excluded: HashSet<ObjectId>,
390 pub extra_wants: Vec<ObjectId>,
394}
395
396fn peel_to_commit<R: ObjectReader>(
400 remote_db: &R,
401 format: ObjectFormat,
402 oid: &ObjectId,
403) -> Result<Option<ObjectId>> {
404 let mut oid = *oid;
405 loop {
406 let object = remote_db.read_object(&oid)?;
407 match object.object_type {
408 ObjectType::Commit => return Ok(Some(oid)),
409 ObjectType::Tag => oid = Tag::parse_ref(format, &object.body)?.object,
410 _ => return Ok(None),
411 }
412 }
413}
414
415pub fn compute_local_deepen<R: ObjectReader>(
428 remote_db: &R,
429 format: ObjectFormat,
430 heads: &[ObjectId],
431 client_shallow: Vec<ObjectId>,
432 depth: u32,
433 deepen_relative: bool,
434) -> Result<LocalDeepenPlan> {
435 let depth = if deepen_relative && depth < INFINITE_DEPTH {
438 depth.saturating_add(client_shallow_min_depth(
439 remote_db,
440 format,
441 heads,
442 &client_shallow,
443 )?)
444 } else {
445 depth
446 };
447 let mut min_depth: HashMap<ObjectId, u32> = HashMap::new();
448 let mut queue: VecDeque<ObjectId> = VecDeque::new();
449 for head in heads {
450 let Some(commit) = peel_to_commit(remote_db, format, head)? else {
451 continue;
452 };
453 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(commit) {
454 entry.insert(0);
455 queue.push_back(commit);
456 }
457 }
458 let mut boundary = Vec::new();
463 let mut boundary_parents = HashSet::new();
464 while let Some(oid) = queue.pop_front() {
465 let commit_depth = min_depth[&oid];
466 let object = remote_db.read_object(&oid)?;
467 let parents = sley_odb::grafted_parents(
468 remote_db,
469 &oid,
470 Commit::parse_ref(format, &object.body)?.parents,
471 );
472 if (depth != INFINITE_DEPTH && commit_depth + 1 >= depth)
476 || remote_db.is_shallow_graft(&oid)
477 {
478 boundary.push(oid);
479 boundary_parents.extend(parents);
480 continue;
481 }
482 for parent in parents {
483 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(parent) {
484 entry.insert(commit_depth + 1);
485 queue.push_back(parent);
486 }
487 }
488 }
489 let excluded = boundary_parents
493 .into_iter()
494 .filter(|parent| !min_depth.contains_key(parent))
495 .collect::<HashSet<_>>();
496
497 let client: HashSet<ObjectId> = client_shallow.iter().copied().collect();
498 let boundary_set: HashSet<ObjectId> = boundary.iter().copied().collect();
499 let mut shallow_info = Vec::new();
500 for oid in &boundary {
501 if !client.contains(oid) {
502 shallow_info.push(ProtocolV2FetchShallowInfo::Shallow(*oid));
503 }
504 }
505 let mut extra_wants = Vec::new();
506 for oid in &client_shallow {
507 let unshallowed = min_depth.contains_key(oid) && !boundary_set.contains(oid);
510 if !unshallowed {
511 continue;
512 }
513 shallow_info.push(ProtocolV2FetchShallowInfo::Unshallow(*oid));
514 let object = remote_db.read_object(oid)?;
515 extra_wants.extend(sley_odb::grafted_parents(
516 remote_db,
517 oid,
518 Commit::parse_ref(format, &object.body)?.parents,
519 ));
520 }
521 Ok(LocalDeepenPlan {
522 depth,
523 deepen_since: false,
524 deepen_not: 0,
525 client_shallow,
526 shallow_info,
527 excluded,
528 extra_wants,
529 })
530}
531
532pub const INFINITE_DEPTH: u32 = 0x7fff_ffff;
535
536fn client_shallow_min_depth<R: ObjectReader>(
540 remote_db: &R,
541 format: ObjectFormat,
542 heads: &[ObjectId],
543 client_shallow: &[ObjectId],
544) -> Result<u32> {
545 if client_shallow.is_empty() {
546 return Ok(0);
547 }
548 let client: HashSet<ObjectId> = client_shallow.iter().copied().collect();
549 let mut min_depth: HashMap<ObjectId, u32> = HashMap::new();
550 let mut queue: VecDeque<ObjectId> = VecDeque::new();
551 for head in heads {
552 let Some(commit) = peel_to_commit(remote_db, format, head)? else {
553 continue;
554 };
555 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(commit) {
556 entry.insert(1);
557 queue.push_back(commit);
558 }
559 }
560 let mut best: u32 = 0;
561 while let Some(oid) = queue.pop_front() {
562 let commit_depth = min_depth[&oid];
563 if client.contains(&oid) && (best == 0 || commit_depth < best) {
564 best = commit_depth;
565 }
566 let object = remote_db.read_object(&oid)?;
567 let parents = sley_odb::grafted_parents(
568 remote_db,
569 &oid,
570 Commit::parse_ref(format, &object.body)?.parents,
571 );
572 for parent in parents {
573 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(parent) {
574 entry.insert(commit_depth + 1);
575 queue.push_back(parent);
576 }
577 }
578 }
579 Ok(best)
580}
581
582pub fn compute_local_deepen_by_rev_list<R: ObjectReader>(
588 remote_db: &R,
589 format: ObjectFormat,
590 heads: &[ObjectId],
591 client_shallow: Vec<ObjectId>,
592 since: Option<i64>,
593 deepen_not: &[ObjectId],
594) -> Result<LocalDeepenPlan> {
595 let mut excluded_not: HashSet<ObjectId> = HashSet::new();
597 let mut queue: VecDeque<ObjectId> = VecDeque::new();
598 for tip in deepen_not {
599 if let Some(commit) = peel_to_commit(remote_db, format, tip)?
600 && excluded_not.insert(commit)
601 {
602 queue.push_back(commit);
603 }
604 }
605 while let Some(oid) = queue.pop_front() {
606 let object = remote_db.read_object(&oid)?;
607 for parent in sley_odb::grafted_parents(
608 remote_db,
609 &oid,
610 Commit::parse_ref(format, &object.body)?.parents,
611 ) {
612 if excluded_not.insert(parent) {
613 queue.push_back(parent);
614 }
615 }
616 }
617
618 let commit_time = |oid: &ObjectId| -> Result<i64> {
619 let object = remote_db.read_object(oid)?;
620 Ok(Commit::parse_ref(format, &object.body)?
621 .committer_signature()
622 .map(|signature| signature.time.seconds)
623 .unwrap_or(0))
624 };
625 let keeps = |oid: &ObjectId| -> Result<bool> {
626 if excluded_not.contains(oid) {
627 return Ok(false);
628 }
629 match since {
630 Some(since) => Ok(commit_time(oid)? >= since),
631 None => Ok(true),
632 }
633 };
634
635 let mut kept: HashSet<ObjectId> = HashSet::new();
638 let mut kept_order: Vec<ObjectId> = Vec::new();
639 let mut queue: VecDeque<ObjectId> = VecDeque::new();
640 for head in heads {
641 let Some(commit) = peel_to_commit(remote_db, format, head)? else {
642 continue;
643 };
644 if keeps(&commit)? && kept.insert(commit) {
645 kept_order.push(commit);
646 queue.push_back(commit);
647 }
648 }
649 while let Some(oid) = queue.pop_front() {
650 let object = remote_db.read_object(&oid)?;
651 for parent in sley_odb::grafted_parents(
652 remote_db,
653 &oid,
654 Commit::parse_ref(format, &object.body)?.parents,
655 ) {
656 if !kept.contains(&parent) && keeps(&parent)? {
657 kept.insert(parent);
658 kept_order.push(parent);
659 queue.push_back(parent);
660 }
661 }
662 }
663 if kept.is_empty() {
664 return Err(GitError::Command(
666 "no commits selected for shallow requests".into(),
667 ));
668 }
669
670 let mut boundary = Vec::new();
672 let mut boundary_set: HashSet<ObjectId> = HashSet::new();
673 let mut excluded: HashSet<ObjectId> = HashSet::new();
674 for oid in &kept_order {
675 let object = remote_db.read_object(oid)?;
676 let parents = sley_odb::grafted_parents(
677 remote_db,
678 oid,
679 Commit::parse_ref(format, &object.body)?.parents,
680 );
681 let mut is_boundary = false;
682 for parent in parents {
683 if !kept.contains(&parent) {
684 is_boundary = true;
685 excluded.insert(parent);
686 }
687 }
688 if is_boundary && boundary_set.insert(*oid) {
689 boundary.push(*oid);
690 }
691 }
692
693 let client: HashSet<ObjectId> = client_shallow.iter().copied().collect();
694 let mut shallow_info = Vec::new();
695 for oid in &boundary {
696 if !client.contains(oid) {
697 shallow_info.push(ProtocolV2FetchShallowInfo::Shallow(*oid));
698 }
699 }
700 let mut extra_wants = Vec::new();
701 for oid in &client_shallow {
702 let unshallowed = kept.contains(oid) && !boundary_set.contains(oid);
703 if !unshallowed {
704 continue;
705 }
706 shallow_info.push(ProtocolV2FetchShallowInfo::Unshallow(*oid));
707 let object = remote_db.read_object(oid)?;
708 extra_wants.extend(sley_odb::grafted_parents(
709 remote_db,
710 oid,
711 Commit::parse_ref(format, &object.body)?.parents,
712 ));
713 }
714 Ok(LocalDeepenPlan {
715 depth: 0,
716 deepen_since: since.is_some(),
717 deepen_not: deepen_not.len(),
718 client_shallow,
719 shallow_info,
720 excluded,
721 extra_wants,
722 })
723}
724
725#[allow(clippy::too_many_arguments)]
738pub fn install_fetch_pack_via_local_upload_pack(
739 git_dir: &Path,
740 remote_git_dir: &Path,
741 format: ObjectFormat,
742 wants: Vec<ObjectId>,
743 deepen: Option<&LocalDeepenPlan>,
744 promisor: bool,
745 filter: Option<sley_odb::PackObjectFilter>,
746 unpack_limit: Option<usize>,
747) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
748 if wants.is_empty() {
749 return Ok(Vec::new());
750 }
751 let local_db = FileObjectDatabase::from_git_dir(git_dir, format);
752 if deepen.is_none()
755 && wants
756 .iter()
757 .map(|want| local_db.contains(want))
758 .collect::<Result<Vec<_>>>()?
759 .into_iter()
760 .all(|contains| contains)
761 {
762 return Ok(Vec::new());
763 }
764
765 let request = UploadPackRequest {
766 wants,
767 capabilities: deepen
770 .map(|_| {
771 vec![Capability {
772 name: "shallow".into(),
773 value: None,
774 }]
775 })
776 .unwrap_or_default(),
777 shallow: deepen
778 .map(|plan| plan.client_shallow.clone())
779 .unwrap_or_default(),
780 deepen: deepen.and_then(|plan| (plan.depth > 0).then_some(plan.depth)),
781 ..UploadPackRequest::default()
782 };
783 let mut encoded_request = Vec::new();
784 write_upload_pack_request(&mut encoded_request, Some(&request))?;
785 let decoded_request = read_upload_pack_request(format, &mut encoded_request.as_slice())?
786 .ok_or_else(|| GitError::InvalidFormat("encoded upload-pack request was empty".into()))?;
787
788 let haves = local_have_oids(git_dir, format)?;
789 let negotiation = UploadPackNegotiationRequest { haves, done: true };
790 let mut encoded_negotiation = Vec::new();
791 write_upload_pack_negotiation_request(&mut encoded_negotiation, &negotiation)?;
792 let decoded_negotiation =
793 read_upload_pack_negotiation_request(format, &mut encoded_negotiation.as_slice())?;
794
795 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
796 for want in &decoded_request.wants {
797 if !remote_db.contains(want)? {
798 return Err(GitError::InvalidObject(format!(
799 "upload-pack requested missing object {want}"
800 )));
801 }
802 }
803 let known_haves = decoded_negotiation
804 .haves
805 .into_iter()
806 .filter_map(|oid| match remote_db.contains(&oid) {
807 Ok(true) => Some(Ok(oid)),
808 Ok(false) => None,
809 Err(err) => Some(Err(err)),
810 })
811 .collect::<Result<Vec<_>>>()?;
812 trace2_fetch_info(
816 known_haves.len(),
817 decoded_request.wants.len(),
818 deepen.map(|plan| plan.depth).unwrap_or(0),
819 deepen.map(|plan| plan.client_shallow.len()).unwrap_or(0),
820 deepen.is_some_and(|plan| plan.deepen_since),
821 deepen.map(|plan| plan.deepen_not).unwrap_or(0),
822 filter,
823 );
824 let mut excluded = match deepen {
829 Some(plan) => {
830 let cut: HashSet<ObjectId> = plan.client_shallow.iter().copied().collect();
831 sley_odb::collect_reachable_object_ids_with_cut(&remote_db, format, known_haves, &cut)?
832 }
833 None => collect_reachable_object_ids(&remote_db, format, known_haves)?,
834 };
835 let mut starts = decoded_request.wants;
836 if let Some(plan) = deepen {
837 excluded.extend(plan.excluded.iter().copied());
840 starts.extend(plan.extra_wants.iter().copied());
841 }
842 build_and_install_reachable_pack_filtered(
843 &remote_db,
844 &local_db,
845 format,
846 starts,
847 &excluded,
848 RawPackInstallOptions { promisor },
849 filter,
850 unpack_limit,
851 )?;
852 Ok(deepen
853 .map(|plan| plan.shallow_info.clone())
854 .unwrap_or_default())
855}
856
857fn trace2_fetch_info(
861 haves: usize,
862 wants: usize,
863 depth: u32,
864 shallows: usize,
865 deepen_since: bool,
866 deepen_not: usize,
867 filter: Option<sley_odb::PackObjectFilter>,
868) {
869 let Some(path) = std::env::var_os("GIT_TRACE2_EVENT") else {
870 return;
871 };
872 if path.is_empty() {
873 return;
874 }
875 let filter_json = match filter {
876 Some(sley_odb::PackObjectFilter::BlobNone) => "\"blob:none\"".to_string(),
877 None => "null".to_string(),
878 };
879 let line = format!(
880 "{{\"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"
881 );
882 if let Ok(mut file) = std::fs::OpenOptions::new()
883 .create(true)
884 .append(true)
885 .open(&path)
886 {
887 use std::io::Write as _;
888 let _ = file.write_all(line.as_bytes());
889 }
890}
891
892fn upload_pack_v2_capabilities(format: ObjectFormat) -> Vec<Capability> {
906 vec![
907 Capability {
908 name: "agent".into(),
909 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
910 },
911 Capability {
912 name: "ls-refs".into(),
913 value: Some("unborn".into()),
914 },
915 Capability {
916 name: "fetch".into(),
917 value: Some("shallow wait-for-done".into()),
918 },
919 Capability {
920 name: "server-option".into(),
921 value: None,
922 },
923 Capability {
924 name: "object-format".into(),
925 value: Some(format.name().into()),
926 },
927 ]
928}
929
930fn head_symref_target(store: &FileRefStore) -> Result<Option<String>> {
934 match store.read_ref("HEAD")? {
935 Some(RefTarget::Symbolic(name)) => Ok(Some(name)),
936 _ => Ok(None),
937 }
938}
939
940fn local_ls_refs_v2_records(
944 git_dir: &Path,
945 format: ObjectFormat,
946 request: &ProtocolV2LsRefsRequest,
947) -> Result<Vec<ProtocolV2LsRefsRecord>> {
948 let store = FileRefStore::new(git_dir, format);
949 let db = FileObjectDatabase::from_git_dir(git_dir, format);
950 let head_symref = head_symref_target(&store)?;
951
952 let mut entries: Vec<(String, ObjectId, Option<String>)> = Vec::new();
955 if let Some(target) = store.read_ref("HEAD")? {
956 let reference = Ref {
957 name: "HEAD".to_string(),
958 target,
959 };
960 if let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? {
961 entries.push(("HEAD".to_string(), oid, head_symref.clone()));
962 } else if request.unborn {
963 entries.push(("HEAD".to_string(), ObjectId::null(format), head_symref.clone()));
966 }
967 }
968 for reference in store.list_refs()? {
969 let name = reference.name.clone();
970 let Some((oid, symref)) = resolve_for_each_ref_target(&store, &reference)? else {
971 continue;
972 };
973 entries.push((name, oid, symref));
974 }
975
976 let matches_prefix = |name: &str| -> bool {
977 if request.ref_prefixes.is_empty() {
978 return true;
979 }
980 request
981 .ref_prefixes
982 .iter()
983 .any(|prefix| name.starts_with(prefix.as_str()))
984 };
985
986 let mut records = Vec::new();
987 for (name, oid, symref) in entries {
988 if !matches_prefix(&name) {
989 continue;
990 }
991 if name == "HEAD" && oid == ObjectId::null(format) {
993 records.push(ProtocolV2LsRefsRecord::Unborn {
994 name,
995 symref_target: if request.symrefs { symref } else { None },
996 attributes: Vec::new(),
997 });
998 continue;
999 }
1000 let peeled = if request.peel {
1001 let object = db.read_object(&oid)?;
1002 if object.object_type == ObjectType::Tag {
1003 Some(sley_rev::peel_tags(&db, format, &oid)?)
1004 } else {
1005 None
1006 }
1007 } else {
1008 None
1009 };
1010 let symref_target = if request.symrefs { symref } else { None };
1011 records.push(ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
1012 oid,
1013 name,
1014 peeled,
1015 symref_target,
1016 attributes: Vec::new(),
1017 }));
1018 }
1019 Ok(records)
1020}
1021
1022fn packfile_section_lines(pack: &[u8]) -> Vec<Vec<u8>> {
1028 let chunk = PKT_LINE_MAX_PAYLOAD_LEN - 1;
1029 let mut lines = Vec::new();
1030 for slice in pack.chunks(chunk) {
1031 let mut payload = Vec::with_capacity(slice.len() + 1);
1032 payload.push(1u8); payload.extend_from_slice(slice);
1034 lines.push(payload);
1035 }
1036 lines
1037}
1038
1039fn local_fetch_v2_sections(
1045 git_dir: &Path,
1046 format: ObjectFormat,
1047 request: &ProtocolV2FetchRequest,
1048) -> Result<Vec<ProtocolV2FetchResponseSection>> {
1049 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1050
1051 let mut sections = Vec::new();
1052
1053 if !request.done {
1059 let mut acks: Vec<ProtocolV2FetchAcknowledgment> = Vec::new();
1060 for have in &request.haves {
1061 if db.contains(have)? {
1062 acks.push(ProtocolV2FetchAcknowledgment::Ack(*have));
1063 }
1064 }
1065 if acks.is_empty() {
1066 acks.push(ProtocolV2FetchAcknowledgment::Nak);
1067 }
1068 sections.push(ProtocolV2FetchResponseSection::Acknowledgments(acks));
1069 if !request.wait_for_done {
1072 return Ok(sections);
1073 }
1074 }
1075
1076 if !request.want_refs.is_empty() {
1078 let store = FileRefStore::new(git_dir, format);
1079 let mut wanted = Vec::new();
1080 for name in &request.want_refs {
1081 let reference = Ref {
1082 name: name.clone(),
1083 target: store
1084 .read_ref(name)?
1085 .ok_or_else(|| GitError::not_found(format!("want-ref {name}")))?,
1086 };
1087 let (oid, _) = resolve_for_each_ref_target(&store, &reference)?
1088 .ok_or_else(|| GitError::not_found(format!("want-ref {name}")))?;
1089 wanted.push(sley_protocol::ProtocolV2FetchWantedRef {
1090 oid,
1091 name: name.clone(),
1092 });
1093 }
1094 sections.push(ProtocolV2FetchResponseSection::WantedRefs(wanted));
1095 }
1096
1097 let mut wants: Vec<ObjectId> = request.wants.clone();
1099 if !request.want_refs.is_empty() {
1100 if let Some(ProtocolV2FetchResponseSection::WantedRefs(wanted)) = sections
1101 .iter()
1102 .find(|s| matches!(s, ProtocolV2FetchResponseSection::WantedRefs(_)))
1103 {
1104 for w in wanted {
1105 wants.push(w.oid);
1106 }
1107 }
1108 }
1109
1110 let mut known_haves: Vec<ObjectId> = Vec::new();
1112 for have in &request.haves {
1113 if db.contains(have)? {
1114 known_haves.push(*have);
1115 }
1116 }
1117 let excluded = collect_reachable_object_ids(&db, format, known_haves)?;
1118 let pack = build_reachable_pack(&db, format, wants, &excluded)?
1119 .map(|pack| pack.pack)
1120 .unwrap_or_default();
1121
1122 sections.push(ProtocolV2FetchResponseSection::Packfile(
1123 packfile_section_lines(&pack),
1124 ));
1125 Ok(sections)
1126}
1127
1128pub fn serve_upload_pack_v2(
1134 git_dir: &Path,
1135 format: ObjectFormat,
1136 reader: &mut impl std::io::Read,
1137 writer: &mut impl std::io::Write,
1138) -> Result<()> {
1139 let handshake = TransportHandshake {
1140 protocol: ProtocolVersion::V2,
1141 capabilities: upload_pack_v2_capabilities(format),
1142 };
1143 write_protocol_v2_advertisement(writer, &handshake)?;
1144 writer.flush()?;
1145
1146 loop {
1147 let request = match read_protocol_v2_command_request(reader) {
1148 Ok(request) => request,
1149 Err(_) => break,
1152 };
1153 match request.command.as_str() {
1154 "ls-refs" => {
1155 let ls_refs = ProtocolV2LsRefsRequest::from_command_request(&request)?;
1156 let records = local_ls_refs_v2_records(git_dir, format, &ls_refs)?;
1157 write_protocol_v2_ls_refs_response(writer, &records)?;
1158 writer.flush()?;
1159 }
1160 "fetch" => {
1161 let fetch = ProtocolV2FetchRequest::from_command_request(format, &request)?;
1162 let sections = local_fetch_v2_sections(git_dir, format, &fetch)?;
1163 write_protocol_v2_fetch_response(writer, §ions)?;
1164 writer.flush()?;
1165 }
1166 other => {
1167 return Err(GitError::InvalidFormat(format!(
1168 "unsupported protocol v2 command {other}"
1169 )));
1170 }
1171 }
1172 }
1173 Ok(())
1174}