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 {
169 ReceivePackFeatures {
170 report_status: true,
171 delete_refs: true,
172 ofs_delta: true,
173 push_options: true,
174 quiet: true,
175 no_thin: true,
176 object_format: Some(format),
177 ..ReceivePackFeatures::default()
178 }
179}
180
181pub fn receive_pack_request_uses_push_options(request: &ReceivePackRequest) -> bool {
184 request
185 .capabilities
186 .iter()
187 .any(|capability| capability.name == "push-options")
188}
189
190pub fn attach_receive_pack_capabilities(
193 advertisements: &mut Vec<RefAdvertisement>,
194 format: ObjectFormat,
195 features: &ReceivePackFeatures,
196) -> Result<()> {
197 let capabilities = encode_receive_pack_features(features)?;
198 if let Some(first) = advertisements.first_mut() {
199 first.capabilities = capabilities;
200 } else {
201 advertisements.push(RefAdvertisement {
202 oid: zero_oid(format)?,
203 name: "capabilities^{}".into(),
204 capabilities,
205 });
206 }
207 Ok(())
208}
209
210pub fn receive_pack_into_local_repository(
214 remote_git_dir: &Path,
215 format: ObjectFormat,
216 request: &ReceivePackPushRequest,
217) -> Result<ReceivePackReportStatus> {
218 let remote_store = FileRefStore::new(remote_git_dir, format);
219 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
220 let deletes_applied_with_updates = std::cell::RefCell::new(HashSet::<String>::new());
221 apply_receive_pack_push_request(
222 &receive_pack_features(format),
223 request,
224 |name| match remote_store.read_ref(name)? {
225 Some(RefTarget::Direct(oid)) => Ok(Some(oid)),
226 Some(RefTarget::Symbolic(_)) | None => Ok(None),
227 },
228 |packfile| {
229 let mut reader = packfile;
230 remote_db
231 .install_raw_pack_from_reader(&mut reader)
232 .map(|_| ())
233 },
234 |oid| remote_db.contains(oid),
235 |commands| {
236 let applied = apply_receive_pack_ref_transaction(
237 remote_git_dir,
238 format,
239 &remote_store,
240 commands,
241 &request.commands.commands,
242 )?;
243 deletes_applied_with_updates.borrow_mut().extend(applied);
244 Ok(())
245 },
246 |command| {
247 if deletes_applied_with_updates
248 .borrow()
249 .contains(command.name.as_str())
250 {
251 return Ok(());
252 }
253 remote_store
254 .delete_ref_checked(DeleteRef {
255 name: command.name.clone(),
256 expected_old: (!command.old_id.is_null()).then_some(command.old_id),
257 reflog: None,
258 })
259 .map(|_| ())
260 .map_err(|err| GitError::Transaction(err.to_string()))
261 },
262 )
263}
264
265pub fn receive_pack_stream_into_local_repository<R: Read>(
270 remote_git_dir: &Path,
271 format: ObjectFormat,
272 header: &ReceivePackPushRequestHeader,
273 pack_reader: &mut R,
274) -> Result<ReceivePackReportStatus> {
275 let remote_store = FileRefStore::new(remote_git_dir, format);
276 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
277 let pack_prefix = read_optional_pack_prefix(pack_reader)?;
278 let validation_request = ReceivePackPushRequest {
279 commands: header.commands.clone(),
280 push_options: header.push_options.clone(),
281 packfile: pack_prefix.clone().unwrap_or_default(),
282 };
283 validate_receive_pack_push_request_features(
284 &receive_pack_features(format),
285 &validation_request,
286 )?;
287
288 let deletes_applied_with_updates = std::cell::RefCell::new(HashSet::<String>::new());
289 for command in header
290 .commands
291 .commands
292 .iter()
293 .filter(|command| command.new_id.is_null())
294 {
295 let current = match remote_store.read_ref(&command.name)? {
296 Some(RefTarget::Direct(oid)) => Some(oid),
297 Some(RefTarget::Symbolic(_)) | None => None,
298 };
299 if !command.old_id.is_null() && current != Some(command.old_id.clone()) {
300 return Err(GitError::Transaction(format!(
301 "expected ref {} to match",
302 command.name
303 )));
304 }
305 }
306
307 let updates = header
308 .commands
309 .commands
310 .iter()
311 .filter(|command| !command.new_id.is_null())
312 .cloned()
313 .collect::<Vec<_>>();
314 if !updates.is_empty() {
315 if let Some(prefix) = pack_prefix {
316 let mut stream = Cursor::new(prefix).chain(pack_reader);
317 remote_db
318 .install_raw_pack_from_reader(&mut stream)
319 .map(|_| ())?;
320 }
321 for command in &updates {
322 if !remote_db.contains(&command.new_id)? {
323 return Err(GitError::InvalidObject(format!(
324 "receive-pack packfile did not provide {}",
325 command.new_id
326 )));
327 }
328 }
329 let applied = apply_receive_pack_ref_transaction(
330 remote_git_dir,
331 format,
332 &remote_store,
333 &updates,
334 &header.commands.commands,
335 )?;
336 deletes_applied_with_updates.borrow_mut().extend(applied);
337 }
338
339 for command in header
340 .commands
341 .commands
342 .iter()
343 .filter(|command| command.new_id.is_null())
344 {
345 if deletes_applied_with_updates
346 .borrow()
347 .contains(command.name.as_str())
348 {
349 continue;
350 }
351 remote_store
352 .delete_ref_checked(DeleteRef {
353 name: command.name.clone(),
354 expected_old: (!command.old_id.is_null()).then_some(command.old_id),
355 reflog: None,
356 })
357 .map(|_| ())
358 .map_err(|err| GitError::Transaction(err.to_string()))?;
359 }
360
361 Ok(ReceivePackReportStatus {
362 unpack: ReceivePackUnpackStatus::Ok,
363 commands: header
364 .commands
365 .commands
366 .iter()
367 .map(|command| ReceivePackCommandStatus::Ok {
368 name: command.name.clone(),
369 })
370 .collect(),
371 })
372}
373
374fn read_optional_pack_prefix(reader: &mut impl Read) -> Result<Option<Vec<u8>>> {
375 let mut prefix = [0u8; 4];
376 loop {
377 match reader.read(&mut prefix[..1]) {
378 Ok(0) => return Ok(None),
379 Ok(1) => break,
380 Ok(_) => unreachable!("one-byte read returned more than one byte"),
381 Err(err) if err.kind() == ErrorKind::Interrupted => {}
382 Err(err) => return Err(err.into()),
383 }
384 }
385 reader.read_exact(&mut prefix[1..])?;
386 if &prefix != b"PACK" {
387 return Err(GitError::InvalidFormat(
388 "receive-pack packfile must start with PACK".into(),
389 ));
390 }
391 Ok(Some(prefix.to_vec()))
392}
393
394fn receive_pack_log_all_ref_updates(git_dir: &Path) -> bool {
395 let Ok(config) = fs::read_to_string(git_dir.join("config")) else {
396 return false;
397 };
398 let mut in_core = false;
399 for raw_line in config.lines() {
400 let line = raw_line.trim();
401 if line.starts_with('[') && line.ends_with(']') {
402 in_core = line.eq_ignore_ascii_case("[core]");
403 continue;
404 }
405 if !in_core || line.starts_with('#') || line.starts_with(';') {
406 continue;
407 }
408 let Some((name, value)) = line.split_once('=') else {
409 continue;
410 };
411 if name.trim().eq_ignore_ascii_case("logallrefupdates") {
412 return matches!(
413 value.trim().trim_matches('"').to_ascii_lowercase().as_str(),
414 "true" | "yes" | "on" | "1" | "always"
415 );
416 }
417 }
418 false
419}
420
421fn receive_pack_should_write_reflog(refname: &str) -> bool {
422 refname == "HEAD"
423 || refname.starts_with("refs/heads/")
424 || refname.starts_with("refs/remotes/")
425 || refname.starts_with("refs/notes/")
426}
427
428fn receive_pack_reflog_entry(
429 format: ObjectFormat,
430 old_oid: ObjectId,
431 new_oid: ObjectId,
432) -> ReflogEntry {
433 let old_oid = if old_oid.is_null() {
434 ObjectId::null(format)
435 } else {
436 old_oid
437 };
438 ReflogEntry {
439 old_oid,
440 new_oid,
441 committer: receive_pack_reflog_committer(),
442 message: b"push".to_vec(),
443 }
444}
445
446fn receive_pack_reflog_committer() -> Vec<u8> {
447 let seconds = SystemTime::now()
448 .duration_since(UNIX_EPOCH)
449 .map(|duration| duration.as_secs())
450 .unwrap_or(0);
451 format!("Git Rs <sley@example.invalid> {seconds} +0000").into_bytes()
452}
453
454pub fn receive_pack_reachable_pack_into_local_repository(
461 remote_git_dir: &Path,
462 format: ObjectFormat,
463 request: &ReceivePackPushRequest,
464 source_db: &FileObjectDatabase,
465 starts: Vec<ObjectId>,
466 excluded: HashSet<ObjectId>,
467) -> Result<ReceivePackReportStatus> {
468 let remote_store = FileRefStore::new(remote_git_dir, format);
469 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
470 let mut starts = Some(starts);
471 let deletes_applied_with_updates = std::cell::RefCell::new(HashSet::<String>::new());
472 apply_receive_pack_push_request(
473 &receive_pack_features(format),
474 request,
475 |name| match remote_store.read_ref(name)? {
476 Some(RefTarget::Direct(oid)) => Ok(Some(oid)),
477 Some(RefTarget::Symbolic(_)) | None => Ok(None),
478 },
479 |_| {
480 let starts = starts.take().ok_or_else(|| {
481 GitError::InvalidFormat("receive-pack attempted to install pack twice".into())
482 })?;
483 build_and_install_reachable_pack(
484 source_db,
485 &remote_db,
486 format,
487 starts,
488 &excluded,
489 RawPackInstallOptions { promisor: false },
490 )?;
491 Ok(())
492 },
493 |oid| remote_db.contains(oid),
494 |commands| {
495 let applied = apply_receive_pack_ref_transaction(
496 remote_git_dir,
497 format,
498 &remote_store,
499 commands,
500 &request.commands.commands,
501 )?;
502 deletes_applied_with_updates.borrow_mut().extend(applied);
503 Ok(())
504 },
505 |command| {
506 if deletes_applied_with_updates
507 .borrow()
508 .contains(command.name.as_str())
509 {
510 return Ok(());
511 }
512 remote_store
513 .delete_ref_checked(DeleteRef {
514 name: command.name.clone(),
515 expected_old: (!command.old_id.is_null()).then_some(command.old_id),
516 reflog: None,
517 })
518 .map(|_| ())
519 .map_err(|err| GitError::Transaction(err.to_string()))
520 },
521 )
522}
523
524fn apply_receive_pack_ref_transaction(
525 remote_git_dir: &Path,
526 format: ObjectFormat,
527 store: &FileRefStore,
528 updates: &[ReceivePackCommand],
529 all_commands: &[ReceivePackCommand],
530) -> Result<HashSet<String>> {
531 let updates = canonical_receive_pack_update_commands(store, updates)?;
532 let deletes = all_commands
533 .iter()
534 .filter(|command| command.new_id.is_null())
535 .collect::<Vec<_>>();
536 let mut tx = store.transaction();
537 for command in &deletes {
538 tx.delete_with_precondition(
539 command.name.clone(),
540 RefDeletePrecondition::Direct((!command.old_id.is_null()).then_some(command.old_id)),
541 None,
542 );
543 }
544 let log_updates = receive_pack_log_all_ref_updates(remote_git_dir);
545 for command in &updates {
546 let precondition = if command.old_id.is_null() {
547 RefPrecondition::MustNotExist
548 } else {
549 RefPrecondition::MustExistAndMatch(RefTarget::Direct(command.old_id))
550 };
551 let reflog = if log_updates && receive_pack_should_write_reflog(&command.name) {
552 Some(receive_pack_reflog_entry(
553 format,
554 command.old_id,
555 command.new_id,
556 ))
557 } else {
558 None
559 };
560 tx.update_to(
561 command.name.clone(),
562 RefTarget::Direct(command.new_id),
563 precondition,
564 reflog,
565 );
566 }
567 tx.commit()?;
568 Ok(deletes
569 .into_iter()
570 .map(|command| command.name.clone())
571 .collect())
572}
573
574fn canonical_receive_pack_update_commands(
575 store: &FileRefStore,
576 commands: &[ReceivePackCommand],
577) -> Result<Vec<ReceivePackCommand>> {
578 let mut by_actual = HashMap::<String, ObjectId>::new();
579 let mut canonical = Vec::with_capacity(commands.len());
580 for command in commands {
581 let name = match store.read_ref(&command.name)? {
582 Some(RefTarget::Symbolic(target)) => target,
583 Some(RefTarget::Direct(_)) | None => command.name.clone(),
584 };
585 if let Some(existing) = by_actual.get(&name) {
586 if existing != &command.new_id {
587 return Err(GitError::Command("refusing inconsistent update".into()));
588 }
589 } else {
590 by_actual.insert(name.clone(), command.new_id);
591 }
592 canonical.push(ReceivePackCommand {
593 old_id: command.old_id,
594 new_id: command.new_id,
595 name,
596 });
597 }
598 Ok(canonical)
599}
600
601pub fn local_fetch_advertisements(
604 git_dir: &Path,
605 format: ObjectFormat,
606) -> Result<Vec<RefAdvertisement>> {
607 let store = FileRefStore::new(git_dir, format);
608 let mut advertisements = Vec::new();
609 if let Some(target) = store.read_ref("HEAD")? {
610 let reference = Ref {
611 name: "HEAD".to_string(),
612 target,
613 };
614 if let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? {
615 advertisements.push(RefAdvertisement {
616 oid,
617 name: reference.name,
618 capabilities: Vec::new(),
619 });
620 }
621 }
622 for reference in store.list_refs()? {
623 let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? else {
624 continue;
625 };
626 advertisements.push(RefAdvertisement {
627 oid,
628 name: reference.name,
629 capabilities: Vec::new(),
630 });
631 }
632 Ok(advertisements)
633}
634
635pub fn local_have_oids(git_dir: &Path, format: ObjectFormat) -> Result<Vec<ObjectId>> {
639 let mut seen = HashSet::new();
640 let mut haves = Vec::new();
641 for advertisement in local_fetch_advertisements(git_dir, format)? {
642 if seen.insert(advertisement.oid) {
643 haves.push(advertisement.oid);
644 }
645 }
646 let db = FileObjectDatabase::from_git_dir(git_dir, format);
647 for oid in db.object_ids()? {
648 if seen.insert(oid) {
649 haves.push(oid);
650 }
651 }
652 Ok(haves)
653}
654
655#[derive(Debug, Clone)]
662pub struct LocalDeepenPlan {
663 pub depth: u32,
667 pub deepen_since: bool,
669 pub deepen_not: usize,
671 pub client_shallow: Vec<ObjectId>,
674 pub shallow_info: Vec<ProtocolV2FetchShallowInfo>,
677 pub excluded: HashSet<ObjectId>,
682 pub extra_wants: Vec<ObjectId>,
686}
687
688fn peel_to_commit<R: ObjectReader>(
692 remote_db: &R,
693 format: ObjectFormat,
694 oid: &ObjectId,
695) -> Result<Option<ObjectId>> {
696 let mut oid = *oid;
697 loop {
698 let object = remote_db.read_object(&oid)?;
699 match object.object_type {
700 ObjectType::Commit => return Ok(Some(oid)),
701 ObjectType::Tag => oid = Tag::parse_ref(format, &object.body)?.object,
702 _ => return Ok(None),
703 }
704 }
705}
706
707pub fn compute_local_deepen<R: ObjectReader>(
720 remote_db: &R,
721 format: ObjectFormat,
722 heads: &[ObjectId],
723 client_shallow: Vec<ObjectId>,
724 depth: u32,
725 deepen_relative: bool,
726) -> Result<LocalDeepenPlan> {
727 let depth = if deepen_relative && depth < INFINITE_DEPTH {
730 depth.saturating_add(client_shallow_min_depth(
731 remote_db,
732 format,
733 heads,
734 &client_shallow,
735 )?)
736 } else {
737 depth
738 };
739 let mut min_depth: HashMap<ObjectId, u32> = HashMap::new();
740 let mut queue: VecDeque<ObjectId> = VecDeque::new();
741 for head in heads {
742 let Some(commit) = peel_to_commit(remote_db, format, head)? else {
743 continue;
744 };
745 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(commit) {
746 entry.insert(0);
747 queue.push_back(commit);
748 }
749 }
750 let mut boundary = Vec::new();
755 let mut boundary_parents = HashSet::new();
756 while let Some(oid) = queue.pop_front() {
757 let commit_depth = min_depth[&oid];
758 let object = remote_db.read_object(&oid)?;
759 let parents = sley_odb::grafted_parents(
760 remote_db,
761 &oid,
762 Commit::parse_ref(format, &object.body)?.parents,
763 );
764 if (depth != INFINITE_DEPTH && commit_depth + 1 >= depth)
768 || remote_db.is_shallow_graft(&oid)
769 {
770 boundary.push(oid);
771 boundary_parents.extend(parents);
772 continue;
773 }
774 for parent in parents {
775 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(parent) {
776 entry.insert(commit_depth + 1);
777 queue.push_back(parent);
778 }
779 }
780 }
781 let excluded = boundary_parents
785 .into_iter()
786 .filter(|parent| !min_depth.contains_key(parent))
787 .collect::<HashSet<_>>();
788
789 let client: HashSet<ObjectId> = client_shallow.iter().copied().collect();
790 let boundary_set: HashSet<ObjectId> = boundary.iter().copied().collect();
791 let mut shallow_info = Vec::new();
792 for oid in &boundary {
793 if !client.contains(oid) {
794 shallow_info.push(ProtocolV2FetchShallowInfo::Shallow(*oid));
795 }
796 }
797 let mut extra_wants = Vec::new();
798 for oid in &client_shallow {
799 let unshallowed = min_depth.contains_key(oid) && !boundary_set.contains(oid);
802 if !unshallowed {
803 continue;
804 }
805 shallow_info.push(ProtocolV2FetchShallowInfo::Unshallow(*oid));
806 let object = remote_db.read_object(oid)?;
807 extra_wants.extend(sley_odb::grafted_parents(
808 remote_db,
809 oid,
810 Commit::parse_ref(format, &object.body)?.parents,
811 ));
812 }
813 Ok(LocalDeepenPlan {
814 depth,
815 deepen_since: false,
816 deepen_not: 0,
817 client_shallow,
818 shallow_info,
819 excluded,
820 extra_wants,
821 })
822}
823
824pub const INFINITE_DEPTH: u32 = 0x7fff_ffff;
827
828fn client_shallow_min_depth<R: ObjectReader>(
832 remote_db: &R,
833 format: ObjectFormat,
834 heads: &[ObjectId],
835 client_shallow: &[ObjectId],
836) -> Result<u32> {
837 if client_shallow.is_empty() {
838 return Ok(0);
839 }
840 let client: HashSet<ObjectId> = client_shallow.iter().copied().collect();
841 let mut min_depth: HashMap<ObjectId, u32> = HashMap::new();
842 let mut queue: VecDeque<ObjectId> = VecDeque::new();
843 for head in heads {
844 let Some(commit) = peel_to_commit(remote_db, format, head)? else {
845 continue;
846 };
847 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(commit) {
848 entry.insert(1);
849 queue.push_back(commit);
850 }
851 }
852 let mut best: u32 = 0;
853 while let Some(oid) = queue.pop_front() {
854 let commit_depth = min_depth[&oid];
855 if client.contains(&oid) && (best == 0 || commit_depth < best) {
856 best = commit_depth;
857 }
858 let object = remote_db.read_object(&oid)?;
859 let parents = sley_odb::grafted_parents(
860 remote_db,
861 &oid,
862 Commit::parse_ref(format, &object.body)?.parents,
863 );
864 for parent in parents {
865 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(parent) {
866 entry.insert(commit_depth + 1);
867 queue.push_back(parent);
868 }
869 }
870 }
871 Ok(best)
872}
873
874pub fn compute_local_deepen_by_rev_list<R: ObjectReader>(
880 remote_db: &R,
881 format: ObjectFormat,
882 heads: &[ObjectId],
883 client_shallow: Vec<ObjectId>,
884 since: Option<i64>,
885 deepen_not: &[ObjectId],
886) -> Result<LocalDeepenPlan> {
887 let mut excluded_not: HashSet<ObjectId> = HashSet::new();
889 let mut queue: VecDeque<ObjectId> = VecDeque::new();
890 for tip in deepen_not {
891 if let Some(commit) = peel_to_commit(remote_db, format, tip)?
892 && excluded_not.insert(commit)
893 {
894 queue.push_back(commit);
895 }
896 }
897 while let Some(oid) = queue.pop_front() {
898 let object = remote_db.read_object(&oid)?;
899 for parent in sley_odb::grafted_parents(
900 remote_db,
901 &oid,
902 Commit::parse_ref(format, &object.body)?.parents,
903 ) {
904 if excluded_not.insert(parent) {
905 queue.push_back(parent);
906 }
907 }
908 }
909
910 let commit_time = |oid: &ObjectId| -> Result<i64> {
911 let object = remote_db.read_object(oid)?;
912 Ok(Commit::parse_ref(format, &object.body)?
913 .committer_signature()
914 .map(|signature| signature.time.seconds)
915 .unwrap_or(0))
916 };
917 let keeps = |oid: &ObjectId| -> Result<bool> {
918 if excluded_not.contains(oid) {
919 return Ok(false);
920 }
921 match since {
922 Some(since) => Ok(commit_time(oid)? >= since),
923 None => Ok(true),
924 }
925 };
926
927 let mut kept: HashSet<ObjectId> = HashSet::new();
930 let mut kept_order: Vec<ObjectId> = Vec::new();
931 let mut queue: VecDeque<ObjectId> = VecDeque::new();
932 for head in heads {
933 let Some(commit) = peel_to_commit(remote_db, format, head)? else {
934 continue;
935 };
936 if keeps(&commit)? && kept.insert(commit) {
937 kept_order.push(commit);
938 queue.push_back(commit);
939 }
940 }
941 while let Some(oid) = queue.pop_front() {
942 let object = remote_db.read_object(&oid)?;
943 for parent in sley_odb::grafted_parents(
944 remote_db,
945 &oid,
946 Commit::parse_ref(format, &object.body)?.parents,
947 ) {
948 if !kept.contains(&parent) && keeps(&parent)? {
949 kept.insert(parent);
950 kept_order.push(parent);
951 queue.push_back(parent);
952 }
953 }
954 }
955 if kept.is_empty() {
956 return Err(GitError::Command(
958 "no commits selected for shallow requests".into(),
959 ));
960 }
961
962 let mut boundary = Vec::new();
964 let mut boundary_set: HashSet<ObjectId> = HashSet::new();
965 let mut excluded: HashSet<ObjectId> = HashSet::new();
966 for oid in &kept_order {
967 let object = remote_db.read_object(oid)?;
968 let parents = sley_odb::grafted_parents(
969 remote_db,
970 oid,
971 Commit::parse_ref(format, &object.body)?.parents,
972 );
973 let mut is_boundary = false;
974 for parent in parents {
975 if !kept.contains(&parent) {
976 is_boundary = true;
977 excluded.insert(parent);
978 }
979 }
980 if is_boundary && boundary_set.insert(*oid) {
981 boundary.push(*oid);
982 }
983 }
984
985 let client: HashSet<ObjectId> = client_shallow.iter().copied().collect();
986 let mut shallow_info = Vec::new();
987 for oid in &boundary {
988 if !client.contains(oid) {
989 shallow_info.push(ProtocolV2FetchShallowInfo::Shallow(*oid));
990 }
991 }
992 let mut extra_wants = Vec::new();
993 for oid in &client_shallow {
994 let unshallowed = kept.contains(oid) && !boundary_set.contains(oid);
995 if !unshallowed {
996 continue;
997 }
998 shallow_info.push(ProtocolV2FetchShallowInfo::Unshallow(*oid));
999 let object = remote_db.read_object(oid)?;
1000 extra_wants.extend(sley_odb::grafted_parents(
1001 remote_db,
1002 oid,
1003 Commit::parse_ref(format, &object.body)?.parents,
1004 ));
1005 }
1006 Ok(LocalDeepenPlan {
1007 depth: 0,
1008 deepen_since: since.is_some(),
1009 deepen_not: deepen_not.len(),
1010 client_shallow,
1011 shallow_info,
1012 excluded,
1013 extra_wants,
1014 })
1015}
1016
1017#[allow(clippy::too_many_arguments)]
1030pub fn install_fetch_pack_via_local_upload_pack(
1031 git_dir: &Path,
1032 remote_git_dir: &Path,
1033 format: ObjectFormat,
1034 wants: Vec<ObjectId>,
1035 deepen: Option<&LocalDeepenPlan>,
1036 promisor: bool,
1037 record_promisor_refs: bool,
1038 filter: Option<sley_odb::PackObjectFilter>,
1039 refetch: bool,
1040 unpack_limit: Option<usize>,
1041) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
1042 if wants.is_empty() {
1043 return Ok(Vec::new());
1044 }
1045 let local_db = FileObjectDatabase::from_git_dir(git_dir, format)
1046 .with_promisor_remote_present(repo_has_promisor_remote(git_dir));
1047 let all_wants_present = wants
1048 .iter()
1049 .map(|want| local_db.contains(want))
1050 .collect::<Result<Vec<_>>>()?
1051 .into_iter()
1052 .all(|contains| contains);
1053 let deepen_noop = match deepen {
1054 Some(plan) => plan.shallow_info.is_empty() && plan.extra_wants.is_empty(),
1055 None => true,
1056 };
1057 if all_wants_present && deepen_noop && !refetch {
1058 sley_protocol::trace_packet_write_payload(b"0000");
1059 return Ok(Vec::new());
1060 }
1061
1062 let request = UploadPackRequest {
1063 wants,
1064 filter: filter
1065 .as_ref()
1066 .and_then(local_upload_pack_filter_protocol_spec),
1067 capabilities: deepen
1070 .map(|_| {
1071 vec![Capability {
1072 name: "shallow".into(),
1073 value: None,
1074 }]
1075 })
1076 .unwrap_or_default(),
1077 shallow: deepen
1078 .map(|plan| plan.client_shallow.clone())
1079 .unwrap_or_default(),
1080 deepen: deepen.and_then(|plan| (plan.depth > 0).then_some(plan.depth)),
1081 ..UploadPackRequest::default()
1082 };
1083 let mut encoded_request = Vec::new();
1084 write_upload_pack_request(&mut encoded_request, Some(&request))?;
1085 let decoded_request = read_upload_pack_request(format, &mut encoded_request.as_slice())?
1086 .ok_or_else(|| GitError::InvalidFormat("encoded upload-pack request was empty".into()))?;
1087
1088 let direct_promisor_object_fetch = promisor && deepen.is_none() && !record_promisor_refs;
1091 if direct_promisor_object_fetch && local_upload_pack_client_wants_v2(git_dir) {
1092 trace_local_upload_pack_v2_capabilities(remote_git_dir, format);
1093 }
1094 let haves = if refetch || direct_promisor_object_fetch {
1095 Vec::new()
1096 } else {
1097 local_have_oids(git_dir, format)?
1098 };
1099 let negotiation = UploadPackNegotiationRequest { haves, done: true };
1100 let mut encoded_negotiation = Vec::new();
1101 write_upload_pack_negotiation_request(&mut encoded_negotiation, &negotiation)?;
1102 let decoded_negotiation =
1103 read_upload_pack_negotiation_request(format, &mut encoded_negotiation.as_slice())?;
1104 sley_core::trace2::data("negotiation_v2", "total_rounds", 1);
1105
1106 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
1107 for want in &decoded_request.wants {
1108 if !remote_db.contains(want)? {
1109 return Err(GitError::InvalidObject(format!(
1110 "upload-pack requested missing object {want}"
1111 )));
1112 }
1113 }
1114 let known_haves = decoded_negotiation
1115 .haves
1116 .into_iter()
1117 .filter_map(|oid| match remote_db.contains(&oid) {
1118 Ok(true) => Some(Ok(oid)),
1119 Ok(false) => None,
1120 Err(err) => Some(Err(err)),
1121 })
1122 .collect::<Result<Vec<_>>>()?;
1123 trace2_fetch_info(
1127 known_haves.len(),
1128 decoded_request.wants.len(),
1129 deepen.map(|plan| plan.depth).unwrap_or(0),
1130 deepen.map(|plan| plan.client_shallow.len()).unwrap_or(0),
1131 deepen.is_some_and(|plan| plan.deepen_since),
1132 deepen.map(|plan| plan.deepen_not).unwrap_or(0),
1133 filter.as_ref(),
1134 );
1135 let mut excluded = match deepen {
1140 Some(plan) => {
1141 let cut: HashSet<ObjectId> = plan.client_shallow.iter().copied().collect();
1142 sley_odb::collect_reachable_object_ids_with_cut(&remote_db, format, known_haves, &cut)?
1143 }
1144 None => {
1145 sley_odb::collect_reachable_object_ids_tolerating_promised_missing(
1150 &local_db,
1151 format,
1152 known_haves,
1153 )?
1154 }
1155 };
1156 let mut starts = decoded_request.wants;
1157 let promisor_ref_wants = starts.iter().copied().collect::<HashSet<_>>();
1158 for want in &starts {
1159 excluded.remove(want);
1160 }
1161 if let Some(plan) = deepen {
1162 excluded.extend(plan.excluded.iter().copied());
1165 starts.extend(plan.extra_wants.iter().copied());
1166 }
1167 let install = build_and_install_reachable_pack_filtered(
1168 &remote_db,
1169 &local_db,
1170 format,
1171 starts,
1172 &excluded,
1173 RawPackInstallOptions { promisor },
1174 filter.clone(),
1175 unpack_limit,
1176 )?;
1177 if promisor
1178 && record_promisor_refs
1179 && let Some(result) = install
1180 && let Some(promisor_path) = result.promisor_path
1181 {
1182 append_promisor_ref_lines(&promisor_path, remote_git_dir, format, &promisor_ref_wants)?;
1183 }
1184 Ok(deepen
1185 .map(|plan| plan.shallow_info.clone())
1186 .unwrap_or_default())
1187}
1188
1189fn local_upload_pack_client_wants_v2(git_dir: &Path) -> bool {
1190 sley_config::read_repo_config(git_dir, None)
1191 .ok()
1192 .and_then(|config| config.get("protocol", None, "version").map(str::to_string))
1193 .as_deref()
1194 == Some("2")
1195}
1196
1197fn repo_has_promisor_remote(git_dir: &Path) -> bool {
1198 let Ok(config) = sley_config::read_repo_config(git_dir, None) else {
1199 return false;
1200 };
1201 if config
1202 .get("extensions", None, "partialclone")
1203 .is_some_and(|value| !value.is_empty())
1204 {
1205 return true;
1206 }
1207 config.sections.iter().any(|section| {
1208 section.name.eq_ignore_ascii_case("remote")
1209 && section
1210 .subsection
1211 .as_deref()
1212 .is_some_and(|name| config.get_bool("remote", Some(name), "promisor") == Some(true))
1213 })
1214}
1215
1216fn trace_local_upload_pack_v2_capabilities(remote_git_dir: &Path, format: ObjectFormat) {
1217 sley_protocol::set_packet_trace_identity("fetch");
1218 let config = sley_config::read_repo_config(remote_git_dir, None).unwrap_or_default();
1219 sley_protocol::trace_packet_read_payload(b"version 2\n");
1220 sley_protocol::trace_packet_read_payload(
1221 format!("agent={UPSTREAM_GIT_COMPAT_VERSION}\n").as_bytes(),
1222 );
1223 sley_protocol::trace_packet_read_payload(b"ls-refs=unborn\n");
1224 let mut fetch = "fetch=shallow wait-for-done".to_string();
1225 if config
1226 .get_bool("uploadpack", None, "allowfilter")
1227 .unwrap_or(false)
1228 {
1229 fetch.push_str(" filter");
1230 }
1231 if config
1232 .get_bool("uploadpack", None, "allowrefinwant")
1233 .unwrap_or(false)
1234 {
1235 fetch.push_str(" ref-in-want");
1236 }
1237 fetch.push('\n');
1238 sley_protocol::trace_packet_read_payload(fetch.as_bytes());
1239 sley_protocol::trace_packet_read_payload(
1240 format!("object-format={}\n", format.name()).as_bytes(),
1241 );
1242 sley_protocol::trace_packet_read_payload(b"0000");
1243}
1244
1245fn local_upload_pack_filter_protocol_spec(filter: &sley_odb::PackObjectFilter) -> Option<String> {
1246 match filter {
1247 sley_odb::PackObjectFilter::BlobNone => Some("blob:none".to_string()),
1248 sley_odb::PackObjectFilter::BlobLimit(limit) => Some(format!("blob:limit={limit}")),
1249 sley_odb::PackObjectFilter::TreeDepth(depth) => Some(format!("tree:{depth}")),
1250 sley_odb::PackObjectFilter::SparsePathSet(_) => None,
1251 }
1252}
1253
1254fn append_promisor_ref_lines(
1255 promisor_path: &Path,
1256 remote_git_dir: &Path,
1257 format: ObjectFormat,
1258 wanted: &HashSet<ObjectId>,
1259) -> Result<()> {
1260 if wanted.is_empty() {
1261 return Ok(());
1262 }
1263 let store = FileRefStore::new(remote_git_dir, format);
1264 let mut lines = Vec::new();
1265 if let Some(head_target) = store.read_ref("HEAD")? {
1266 let head = Ref {
1267 name: "HEAD".into(),
1268 target: head_target,
1269 };
1270 if let Some((oid, _)) = resolve_for_each_ref_target(&store, &head)?
1271 && wanted.contains(&oid)
1272 {
1273 lines.push(format!("{oid} HEAD\n"));
1274 }
1275 }
1276 for reference in store.list_refs()? {
1277 let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? else {
1278 continue;
1279 };
1280 if wanted.contains(&oid) {
1281 lines.push(format!("{oid} {}\n", reference.name));
1282 }
1283 }
1284 if lines.is_empty() {
1285 return Ok(());
1286 }
1287 lines.sort();
1288 let mut file = fs::OpenOptions::new().append(true).open(promisor_path)?;
1289 use std::io::Write as _;
1290 for line in lines {
1291 file.write_all(line.as_bytes())?;
1292 }
1293 Ok(())
1294}
1295
1296fn trace2_fetch_info(
1300 haves: usize,
1301 wants: usize,
1302 depth: u32,
1303 shallows: usize,
1304 deepen_since: bool,
1305 deepen_not: usize,
1306 filter: Option<&sley_odb::PackObjectFilter>,
1307) {
1308 let Some(path) = std::env::var_os("GIT_TRACE2_EVENT") else {
1309 return;
1310 };
1311 if path.is_empty() {
1312 return;
1313 }
1314 let filter_json = match filter {
1315 Some(sley_odb::PackObjectFilter::BlobNone) => "\"blob:none\"".to_string(),
1316 Some(sley_odb::PackObjectFilter::BlobLimit(limit)) => {
1317 format!("\"blob:limit={limit}\"")
1318 }
1319 Some(sley_odb::PackObjectFilter::TreeDepth(depth)) => {
1320 format!("\"tree:{depth}\"")
1321 }
1322 Some(sley_odb::PackObjectFilter::SparsePathSet(_)) => "\"sparse:oid\"".to_string(),
1323 None => "null".to_string(),
1324 };
1325 let line = format!(
1326 "{{\"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"
1327 );
1328 if let Ok(mut file) = std::fs::OpenOptions::new()
1329 .create(true)
1330 .append(true)
1331 .open(&path)
1332 {
1333 use std::io::Write as _;
1334 let _ = file.write_all(line.as_bytes());
1335 }
1336}
1337
1338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1349enum LsRefsUnbornConfig {
1350 Ignore,
1351 Allow,
1352 Advertise,
1353}
1354
1355fn lsrefs_unborn_config(config: &GitConfig) -> LsRefsUnbornConfig {
1356 match config.get("lsrefs", None, "unborn") {
1357 Some("ignore") => LsRefsUnbornConfig::Ignore,
1358 Some("allow") => LsRefsUnbornConfig::Allow,
1359 Some("advertise") | None => LsRefsUnbornConfig::Advertise,
1360 Some(_) => LsRefsUnbornConfig::Advertise,
1361 }
1362}
1363
1364fn upload_pack_blob_packfile_uri_configured(config: &GitConfig) -> bool {
1365 config
1366 .get_all("uploadpack", None, "blobpackfileuri")
1367 .into_iter()
1368 .any(|value| value.is_some_and(|value| !value.is_empty()))
1369}
1370
1371fn upload_pack_v2_capabilities(
1375 format: ObjectFormat,
1376 config: &GitConfig,
1377) -> Result<Vec<Capability>> {
1378 let mut capabilities = vec![
1379 Capability {
1380 name: "agent".into(),
1381 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
1382 },
1383 encode_protocol_v2_ls_refs_capability(&ProtocolV2LsRefsFeatures {
1384 unborn: lsrefs_unborn_config(config) == LsRefsUnbornConfig::Advertise,
1385 unknown: Vec::new(),
1386 })?,
1387 encode_protocol_v2_fetch_capability(&ProtocolV2FetchFeatures {
1388 shallow: true,
1389 wait_for_done: true,
1390 filter: config
1391 .get_bool("uploadpack", None, "allowfilter")
1392 .unwrap_or(false),
1393 ref_in_want: config
1394 .get_bool("uploadpack", None, "allowrefinwant")
1395 .unwrap_or(false),
1396 packfile_uris: upload_pack_blob_packfile_uri_configured(config),
1397 ..ProtocolV2FetchFeatures::default()
1398 })?,
1399 Capability {
1400 name: "server-option".into(),
1401 value: None,
1402 },
1403 Capability {
1404 name: "object-format".into(),
1405 value: Some(format.name().into()),
1406 },
1407 ];
1408 if config
1409 .get_bool("transfer", None, "advertisesid")
1410 .unwrap_or(false)
1411 {
1412 capabilities.push(Capability {
1413 name: "session-id".into(),
1414 value: Some("sley".into()),
1415 });
1416 }
1417 Ok(capabilities)
1418}
1419
1420fn head_symref_target(store: &FileRefStore) -> Result<Option<String>> {
1424 match store.read_ref("HEAD")? {
1425 Some(RefTarget::Symbolic(name)) => Ok(Some(name)),
1426 _ => Ok(None),
1427 }
1428}
1429
1430fn local_ls_refs_v2_records(
1434 git_dir: &Path,
1435 format: ObjectFormat,
1436 request: &ProtocolV2LsRefsRequest,
1437 config: &GitConfig,
1438) -> Result<Vec<ProtocolV2LsRefsRecord>> {
1439 let store = FileRefStore::new(git_dir, format);
1440 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1441 let head_symref = head_symref_target(&store)?;
1442
1443 let mut entries: Vec<(String, ObjectId, Option<String>)> = Vec::new();
1446 if let Some(target) = store.read_ref("HEAD")? {
1447 let reference = Ref {
1448 name: "HEAD".to_string(),
1449 target,
1450 };
1451 if let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? {
1452 entries.push(("HEAD".to_string(), oid, head_symref.clone()));
1453 } else if request.unborn && lsrefs_unborn_config(config) != LsRefsUnbornConfig::Ignore {
1454 entries.push((
1457 "HEAD".to_string(),
1458 ObjectId::null(format),
1459 head_symref.clone(),
1460 ));
1461 }
1462 }
1463 for reference in store.list_refs()? {
1464 let name = reference.name.clone();
1465 let Some((oid, symref)) = resolve_for_each_ref_target(&store, &reference)? else {
1466 continue;
1467 };
1468 entries.push((name, oid, symref));
1469 }
1470
1471 let matches_prefix = |name: &str| -> bool {
1472 if request.ref_prefixes.is_empty() {
1473 return true;
1474 }
1475 request
1476 .ref_prefixes
1477 .iter()
1478 .any(|prefix| name.starts_with(prefix.as_str()))
1479 };
1480
1481 let mut records = Vec::new();
1482 for (name, oid, symref) in entries {
1483 if !matches_prefix(&name) {
1484 continue;
1485 }
1486 if name == "HEAD" && oid == ObjectId::null(format) {
1488 records.push(ProtocolV2LsRefsRecord::Unborn {
1489 name,
1490 symref_target: if request.symrefs { symref } else { None },
1491 attributes: Vec::new(),
1492 });
1493 continue;
1494 }
1495 let peeled = if request.peel {
1496 let object = db.read_object(&oid)?;
1497 if object.object_type == ObjectType::Tag {
1498 Some(sley_rev::peel_tags(&db, format, &oid)?)
1499 } else {
1500 None
1501 }
1502 } else {
1503 None
1504 };
1505 let symref_target = if request.symrefs { symref } else { None };
1506 records.push(ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
1507 oid,
1508 name,
1509 peeled,
1510 symref_target,
1511 attributes: Vec::new(),
1512 }));
1513 }
1514 Ok(records)
1515}
1516
1517fn packfile_section_lines(pack: &[u8]) -> Vec<Vec<u8>> {
1523 let chunk = PKT_LINE_MAX_PAYLOAD_LEN - 1;
1524 let mut lines = Vec::new();
1525 for slice in pack.chunks(chunk) {
1526 let mut payload = Vec::with_capacity(slice.len() + 1);
1527 payload.push(1u8); payload.extend_from_slice(slice);
1529 lines.push(payload);
1530 }
1531 lines
1532}
1533
1534fn local_fetch_v2_sections(
1540 git_dir: &Path,
1541 format: ObjectFormat,
1542 request: &ProtocolV2FetchRequest,
1543) -> Result<Vec<ProtocolV2FetchResponseSection>> {
1544 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1545
1546 let mut sections = Vec::new();
1547
1548 if !request.done {
1554 let mut acks: Vec<ProtocolV2FetchAcknowledgment> = Vec::new();
1555 for have in &request.haves {
1556 if db.contains(have)? {
1557 acks.push(ProtocolV2FetchAcknowledgment::Ack(*have));
1558 }
1559 }
1560 if acks.is_empty() {
1561 acks.push(ProtocolV2FetchAcknowledgment::Nak);
1562 }
1563 sections.push(ProtocolV2FetchResponseSection::Acknowledgments(acks));
1564 if !request.wait_for_done {
1567 return Ok(sections);
1568 }
1569 }
1570
1571 if !request.want_refs.is_empty() {
1573 let store = FileRefStore::new(git_dir, format);
1574 let mut wanted = Vec::new();
1575 for name in &request.want_refs {
1576 let reference = Ref {
1577 name: name.clone(),
1578 target: store
1579 .read_ref(name)?
1580 .ok_or_else(|| GitError::not_found(format!("want-ref {name}")))?,
1581 };
1582 let (oid, _) = resolve_for_each_ref_target(&store, &reference)?
1583 .ok_or_else(|| GitError::not_found(format!("want-ref {name}")))?;
1584 wanted.push(sley_protocol::ProtocolV2FetchWantedRef {
1585 oid,
1586 name: name.clone(),
1587 });
1588 }
1589 sections.push(ProtocolV2FetchResponseSection::WantedRefs(wanted));
1590 }
1591
1592 let mut wants: Vec<ObjectId> = request.wants.clone();
1594 if !request.want_refs.is_empty()
1595 && let Some(ProtocolV2FetchResponseSection::WantedRefs(wanted)) = sections
1596 .iter()
1597 .find(|s| matches!(s, ProtocolV2FetchResponseSection::WantedRefs(_)))
1598 {
1599 for w in wanted {
1600 wants.push(w.oid);
1601 }
1602 }
1603
1604 let mut known_haves: Vec<ObjectId> = Vec::new();
1606 for have in &request.haves {
1607 if db.contains(have)? {
1608 known_haves.push(*have);
1609 }
1610 }
1611 let excluded = collect_reachable_object_ids(&db, format, known_haves)?;
1612 let pack = build_reachable_pack(&db, format, wants, &excluded)?
1613 .map(|pack| pack.pack)
1614 .unwrap_or_default();
1615
1616 sections.push(ProtocolV2FetchResponseSection::Packfile(
1617 packfile_section_lines(&pack),
1618 ));
1619 Ok(sections)
1620}
1621
1622pub fn serve_upload_pack_v2(
1628 git_dir: &Path,
1629 format: ObjectFormat,
1630 reader: &mut impl std::io::Read,
1631 writer: &mut impl std::io::Write,
1632) -> Result<()> {
1633 let config = sley_config::read_repo_config(git_dir, None).unwrap_or_default();
1634 serve_upload_pack_v2_with_config(git_dir, format, &config, reader, writer)
1635}
1636
1637pub fn serve_upload_pack_v2_with_config(
1638 git_dir: &Path,
1639 format: ObjectFormat,
1640 config: &GitConfig,
1641 reader: &mut impl std::io::Read,
1642 writer: &mut impl std::io::Write,
1643) -> Result<()> {
1644 let handshake = TransportHandshake {
1645 protocol: ProtocolVersion::V2,
1646 capabilities: upload_pack_v2_capabilities(format, config)?,
1647 };
1648 write_protocol_v2_advertisement(writer, &handshake)?;
1649 writer.flush()?;
1650
1651 loop {
1656 let request = match read_protocol_v2_command_request(reader) {
1657 Ok(request) => request,
1658 Err(GitError::InvalidFormat(message))
1659 if message == "pkt-line stream ended before control packet"
1660 || message == "protocol v2 command request must start with a command line" =>
1661 {
1662 break;
1663 }
1664 Err(err) => return Err(err),
1665 };
1666 match classify_protocol_v2_command_request(&handshake, format, &request)? {
1667 sley_protocol::ProtocolV2Command::LsRefs(ls_refs) => {
1668 let records = local_ls_refs_v2_records(git_dir, format, &ls_refs, config)?;
1669 write_protocol_v2_ls_refs_response(writer, &records)?;
1670 writer.flush()?;
1671 }
1672 sley_protocol::ProtocolV2Command::Fetch(fetch) => {
1673 let sections = local_fetch_v2_sections(git_dir, format, &fetch)?;
1674 write_protocol_v2_fetch_response(writer, §ions)?;
1675 writer.flush()?;
1676 }
1677 sley_protocol::ProtocolV2Command::ObjectInfo(_)
1678 | sley_protocol::ProtocolV2Command::Unknown(_) => {
1679 return Err(GitError::InvalidFormat(format!(
1680 "unsupported protocol v2 command {}",
1681 request.command
1682 )));
1683 }
1684 }
1685 }
1686 Ok(())
1687}
1688
1689#[cfg(test)]
1690mod tests {
1691 use super::*;
1692 use sley_object::{BString, EncodedObject, Tree, TreeEntry};
1693 use sley_odb::ObjectWriter;
1694
1695 #[test]
1696 fn receive_pack_advertises_no_thin_until_server_fixes_thin_packs() {
1697 let features = receive_pack_features(ObjectFormat::Sha1);
1698 assert!(features.no_thin);
1699
1700 let capabilities =
1701 encode_receive_pack_features(&features).expect("test operation should succeed");
1702 assert!(
1703 capabilities
1704 .iter()
1705 .any(|capability| capability.name == "no-thin")
1706 );
1707 }
1708
1709 #[test]
1710 fn local_fetch_from_incomplete_remote_excludes_client_have_closure() {
1711 let root = unique_local_test_dir("incomplete-local-fetch");
1712 let base_git = root.join("base.git");
1713 let patch_git = root.join("patch.git");
1714 let user_git = root.join("user.git");
1715 let direct_git = root.join("direct.git");
1716 for git_dir in [&base_git, &patch_git, &user_git, &direct_git] {
1717 fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1718 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/main\n")
1719 .expect("test operation should succeed");
1720 }
1721
1722 let format = ObjectFormat::Sha1;
1723 let base_db = FileObjectDatabase::from_git_dir(&base_git, format);
1724 let patch_db = FileObjectDatabase::from_git_dir(&patch_git, format);
1725
1726 let text_a = EncodedObject::new(ObjectType::Blob, b"a\nb\nc\nd\ne\nf\ng\nh\ni\n".to_vec());
1727 let text_a_oid = write_test_object(&base_db, &text_a);
1728 let side = EncodedObject::new(ObjectType::Blob, b"side\n".to_vec());
1729 let side_oid = write_test_object(&base_db, &side);
1730 let tree_a = test_tree(&[
1731 (0o100644, b"side", side_oid),
1732 (0o100644, b"text", text_a_oid),
1733 ]);
1734 let tree_a_oid = write_test_object(&base_db, &tree_a);
1735 let commit_a = test_commit(tree_a_oid, &[], b"A\n");
1736 let commit_a_oid = write_test_object(&base_db, &commit_a);
1737
1738 let text_b =
1739 EncodedObject::new(ObjectType::Blob, b"a\nb\nc\nd\ne\nf\ng\nh\ni\nm\n".to_vec());
1740 let text_b_oid = write_test_object(&base_db, &text_b);
1741 let tree_b = test_tree(&[
1742 (0o100644, b"side", side_oid),
1743 (0o100644, b"text", text_b_oid),
1744 ]);
1745 let tree_b_oid = write_test_object(&base_db, &tree_b);
1746 let commit_b = test_commit(tree_b_oid, &[commit_a_oid], b"B\n");
1747 let commit_b_oid = write_test_object(&base_db, &commit_b);
1748
1749 let text_c = EncodedObject::new(
1750 ObjectType::Blob,
1751 b"a\nb\nc\nd\ne\nf\ng\nh\ni\nm\nq\n".to_vec(),
1752 );
1753 let text_c_oid = write_test_object(&patch_db, &text_c);
1754 let tree_c = test_tree(&[
1755 (0o100644, b"side", side_oid),
1756 (0o100644, b"text", text_c_oid),
1757 ]);
1758 let tree_c_oid = write_test_object(&patch_db, &tree_c);
1759 let commit_c = test_commit(tree_c_oid, &[commit_b_oid], b"C\n");
1760 let commit_c_oid = write_test_object(&patch_db, &commit_c);
1761 write_test_object(&patch_db, &tree_b);
1762 write_test_object(&patch_db, &commit_b);
1763 assert!(
1764 !patch_db
1765 .contains(&text_b_oid)
1766 .expect("test operation should succeed"),
1767 "patch repo must be missing the best delta base"
1768 );
1769
1770 install_fetch_pack_via_local_upload_pack(
1771 &user_git,
1772 &base_git,
1773 format,
1774 vec![commit_b_oid],
1775 None,
1776 false,
1777 false,
1778 None,
1779 false,
1780 None,
1781 )
1782 .expect("base fetch should succeed");
1783 assert!(
1784 FileObjectDatabase::from_git_dir(&user_git, format)
1785 .contains(&text_b_oid)
1786 .expect("test operation should succeed"),
1787 "user clone should have the missing base before fetching C"
1788 );
1789
1790 install_fetch_pack_via_local_upload_pack(
1791 &user_git,
1792 &patch_git,
1793 format,
1794 vec![commit_c_oid],
1795 None,
1796 false,
1797 false,
1798 None,
1799 false,
1800 None,
1801 )
1802 .expect("fetch from incomplete remote should succeed when client has the base");
1803 assert!(
1804 FileObjectDatabase::from_git_dir(&user_git, format)
1805 .contains(&commit_c_oid)
1806 .expect("test operation should succeed")
1807 );
1808
1809 let direct = install_fetch_pack_via_local_upload_pack(
1810 &direct_git,
1811 &patch_git,
1812 format,
1813 vec![commit_c_oid],
1814 None,
1815 false,
1816 false,
1817 None,
1818 false,
1819 None,
1820 );
1821 assert!(
1822 direct.is_err(),
1823 "direct fetch from the incomplete patch repo must still fail"
1824 );
1825
1826 fs::remove_dir_all(root).expect("test operation should succeed");
1827 }
1828
1829 #[test]
1830 fn direct_promisor_object_fetch_does_not_walk_missing_local_blobs() {
1831 let root = unique_local_test_dir("direct-promisor-object-fetch");
1832 let remote_git = root.join("remote.git");
1833 let client_git = root.join("client.git");
1834 for git_dir in [&remote_git, &client_git] {
1835 fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1836 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/main\n")
1837 .expect("test operation should succeed");
1838 }
1839
1840 let format = ObjectFormat::Sha1;
1841 let remote_db = FileObjectDatabase::from_git_dir(&remote_git, format);
1842 let client_db = FileObjectDatabase::from_git_dir(&client_git, format);
1843
1844 let blob = EncodedObject::new(ObjectType::Blob, b"promised\n".to_vec());
1845 let blob_oid = write_test_object(&remote_db, &blob);
1846 let tree = test_tree(&[(0o100644, b"file.txt", blob_oid)]);
1847 let tree_oid = write_test_object(&remote_db, &tree);
1848 let commit = test_commit(tree_oid, &[], b"main\n");
1849 let commit_oid = write_test_object(&remote_db, &commit);
1850
1851 write_test_object(&client_db, &tree);
1852 write_test_object(&client_db, &commit);
1853 assert!(
1854 !client_db
1855 .contains(&blob_oid)
1856 .expect("test operation should succeed"),
1857 "client starts with the promised blob missing"
1858 );
1859
1860 install_fetch_pack_via_local_upload_pack(
1861 &client_git,
1862 &remote_git,
1863 format,
1864 vec![blob_oid],
1865 None,
1866 true,
1867 false,
1868 None,
1869 false,
1870 None,
1871 )
1872 .expect("direct promisor blob fetch should not traverse missing local blobs");
1873
1874 assert!(
1875 FileObjectDatabase::from_git_dir(&client_git, format)
1876 .contains(&blob_oid)
1877 .expect("test operation should succeed"),
1878 "directly wanted promised blob should be installed"
1879 );
1880 assert_eq!(
1881 FileObjectDatabase::from_git_dir(&client_git, format)
1882 .read_object(&commit_oid)
1883 .expect("test operation should succeed")
1884 .object_type,
1885 ObjectType::Commit
1886 );
1887
1888 fs::remove_dir_all(root).expect("test operation should succeed");
1889 }
1890
1891 fn unique_local_test_dir(name: &str) -> std::path::PathBuf {
1892 let nanos = SystemTime::now()
1893 .duration_since(UNIX_EPOCH)
1894 .expect("test operation should succeed")
1895 .as_nanos();
1896 let root =
1897 std::env::temp_dir().join(format!("sley-remote-{name}-{}-{nanos}", std::process::id()));
1898 fs::create_dir_all(&root).expect("test operation should succeed");
1899 root
1900 }
1901
1902 fn write_test_object(db: &FileObjectDatabase, object: &EncodedObject) -> ObjectId {
1903 db.write_object(object.clone())
1904 .expect("test operation should succeed")
1905 }
1906
1907 fn test_tree(entries: &[(u32, &[u8], ObjectId)]) -> EncodedObject {
1908 EncodedObject::new(
1909 ObjectType::Tree,
1910 Tree {
1911 entries: entries
1912 .iter()
1913 .map(|(mode, name, oid)| TreeEntry {
1914 mode: *mode,
1915 name: BString::from(*name),
1916 oid: *oid,
1917 })
1918 .collect(),
1919 }
1920 .write(),
1921 )
1922 }
1923
1924 fn test_commit(tree: ObjectId, parents: &[ObjectId], message: &[u8]) -> EncodedObject {
1925 let identity = b"Example <example@example.invalid> 0 +0000".to_vec();
1926 EncodedObject::new(
1927 ObjectType::Commit,
1928 Commit {
1929 tree,
1930 parents: parents.to_vec(),
1931 author: identity.clone(),
1932 committer: identity,
1933 encoding: None,
1934 message: message.to_vec(),
1935 }
1936 .write(),
1937 )
1938 }
1939}