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, build_reachable_pack_filtered,
26 collect_reachable_object_ids,
27};
28use sley_protocol::{
29 PKT_LINE_MAX_PAYLOAD_LEN, PktLineFrame, ProtocolV2FetchAcknowledgment, ProtocolV2FetchFeatures,
30 ProtocolV2FetchRequest, ProtocolV2FetchResponseSection, ProtocolV2FetchShallowInfo,
31 ProtocolV2LsRefsFeatures, ProtocolV2LsRefsRecord, ProtocolV2LsRefsRef, ProtocolV2LsRefsRequest,
32 ProtocolVersion, ReceivePackCommand, ReceivePackCommandStatus, ReceivePackFeatures,
33 ReceivePackPushRequest, ReceivePackPushRequestHeader, ReceivePackReportStatus,
34 ReceivePackRequest, ReceivePackUnpackStatus, RefAdvertisement, SideBandChannel, SideBandPacket,
35 TransportHandshake, UploadPackFeatures, UploadPackNegotiationRequest,
36 UploadPackPackfileResponse, UploadPackRawPackfileResponse, UploadPackRequest,
37 apply_receive_pack_push_request, build_upload_pack_raw_packfile_response,
38 classify_protocol_v2_command_request, encode_protocol_v2_fetch_capability,
39 encode_protocol_v2_ls_refs_capability, encode_receive_pack_features,
40 encode_upload_pack_features, read_protocol_v2_command_request,
41 read_upload_pack_negotiation_request, read_upload_pack_request,
42 validate_receive_pack_push_request_features, write_pkt_line_frame, write_pkt_line_payload,
43 write_protocol_v2_advertisement, write_protocol_v2_fetch_response,
44 write_protocol_v2_ls_refs_response, write_upload_pack_negotiation_request,
45 write_upload_pack_request,
46};
47use sley_refs::{
48 DeleteRef, FileRefStore, Ref, RefDeletePrecondition, RefPrecondition, RefTarget, ReflogEntry,
49};
50
51fn zero_oid(format: ObjectFormat) -> Result<ObjectId> {
54 Ok(ObjectId::null(format))
55}
56
57fn resolve_for_each_ref_target(
60 store: &FileRefStore,
61 reference: &Ref,
62) -> Result<Option<(ObjectId, Option<String>)>> {
63 let mut target = reference.target.clone();
64 let mut symref = None;
65 for _ in 0..5 {
66 match target {
67 RefTarget::Direct(oid) => return Ok(Some((oid, symref))),
68 RefTarget::Symbolic(name) => {
69 symref.get_or_insert_with(|| name.clone());
70 let Some(next) = store.read_ref(&name)? else {
71 return Ok(None);
72 };
73 target = next;
74 }
75 }
76 }
77 Ok(None)
78}
79
80pub fn upload_pack_features(git_dir: &Path, format: ObjectFormat) -> Result<UploadPackFeatures> {
83 let store = FileRefStore::new(git_dir, format);
84 let mut symrefs = Vec::new();
85 if let Some(RefTarget::Symbolic(target)) = store.read_ref("HEAD")? {
86 symrefs.push(format!("HEAD:{target}"));
87 }
88 Ok(UploadPackFeatures {
89 object_format: Some(format),
90 side_band_64k: true,
91 symrefs,
92 ..UploadPackFeatures::default()
93 })
94}
95
96pub fn upload_pack_request_uses_sideband(request: &UploadPackRequest) -> bool {
98 request
99 .capabilities
100 .iter()
101 .any(|capability| matches!(capability.name.as_str(), "side-band" | "side-band-64k"))
102}
103
104pub fn upload_pack_sideband_response(
107 response: UploadPackRawPackfileResponse,
108) -> UploadPackPackfileResponse {
109 let mut sideband = Vec::new();
110 let chunk_len = PKT_LINE_MAX_PAYLOAD_LEN - 1;
111 for chunk in response.packfile.chunks(chunk_len) {
112 sideband.push(SideBandPacket {
113 channel: SideBandChannel::Data,
114 data: chunk.to_vec(),
115 });
116 }
117 UploadPackPackfileResponse {
118 acknowledgments: response.acknowledgments,
119 sideband,
120 }
121}
122
123pub fn attach_upload_pack_capabilities(
126 advertisements: &mut Vec<RefAdvertisement>,
127 format: ObjectFormat,
128 features: &UploadPackFeatures,
129) -> Result<()> {
130 let capabilities = encode_upload_pack_features(features)?;
131 if let Some(first) = advertisements.first_mut() {
132 first.capabilities = capabilities;
133 } else {
134 advertisements.push(RefAdvertisement {
135 oid: zero_oid(format)?,
136 name: "capabilities^{}".into(),
137 capabilities,
138 });
139 }
140 Ok(())
141}
142
143pub fn upload_pack_from_local_repository(
147 git_dir: &Path,
148 format: ObjectFormat,
149 features: &UploadPackFeatures,
150 request: UploadPackRequest,
151 haves: HashSet<ObjectId>,
152) -> Result<UploadPackRawPackfileResponse> {
153 let db = FileObjectDatabase::from_git_dir(git_dir, format);
154 build_upload_pack_raw_packfile_response(
155 features,
156 request,
157 haves,
158 |oid| db.contains(oid),
159 |wants, known_haves| {
160 let excluded = collect_reachable_object_ids(&db, format, known_haves)?;
161 build_reachable_pack(&db, format, wants, &excluded)
162 .map(|pack| pack.map(|pack| pack.pack))
163 },
164 )
165}
166
167pub fn receive_pack_features(format: ObjectFormat) -> ReceivePackFeatures {
171 ReceivePackFeatures {
172 report_status: true,
173 report_status_v2: true,
174 delete_refs: true,
175 ofs_delta: true,
176 push_options: true,
177 quiet: true,
178 no_thin: true,
179 atomic: true,
180 side_band_64k: true,
181 object_format: Some(format),
182 ..ReceivePackFeatures::default()
183 }
184}
185
186pub fn receive_pack_request_uses_push_options(request: &ReceivePackRequest) -> bool {
189 request
190 .capabilities
191 .iter()
192 .any(|capability| capability.name == "push-options")
193}
194
195pub fn attach_receive_pack_capabilities(
198 advertisements: &mut Vec<RefAdvertisement>,
199 format: ObjectFormat,
200 features: &ReceivePackFeatures,
201) -> Result<()> {
202 let capabilities = encode_receive_pack_features(features)?;
203 if let Some(first) = advertisements.first_mut() {
204 first.capabilities = capabilities;
205 } else {
206 advertisements.push(RefAdvertisement {
207 oid: zero_oid(format)?,
208 name: "capabilities^{}".into(),
209 capabilities,
210 });
211 }
212 Ok(())
213}
214
215pub fn receive_pack_into_local_repository(
219 remote_git_dir: &Path,
220 format: ObjectFormat,
221 request: &ReceivePackPushRequest,
222) -> Result<ReceivePackReportStatus> {
223 let remote_store = FileRefStore::new(remote_git_dir, format);
224 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
225 let deletes_applied_with_updates = std::cell::RefCell::new(HashSet::<String>::new());
226 apply_receive_pack_push_request(
227 &receive_pack_features(format),
228 request,
229 |name| match remote_store.read_ref(name)? {
230 Some(RefTarget::Direct(oid)) => Ok(Some(oid)),
231 Some(RefTarget::Symbolic(_)) | None => Ok(None),
232 },
233 |packfile| {
234 let mut reader = packfile;
235 remote_db
236 .install_raw_pack_from_reader(&mut reader)
237 .map(|_| ())
238 },
239 |oid| remote_db.contains(oid),
240 |commands| {
241 let applied = apply_receive_pack_ref_transaction(
242 remote_git_dir,
243 format,
244 &remote_store,
245 commands,
246 &request.commands.commands,
247 )?;
248 deletes_applied_with_updates.borrow_mut().extend(applied);
249 Ok(())
250 },
251 |command| {
252 if deletes_applied_with_updates
253 .borrow()
254 .contains(command.name.as_str())
255 {
256 return Ok(());
257 }
258 remote_store
259 .delete_ref_checked(DeleteRef {
260 name: command.name.clone(),
261 expected_old: (!command.old_id.is_null()).then_some(command.old_id),
262 reflog: None,
263 })
264 .map(|_| ())
265 .map_err(|err| GitError::Transaction(err.to_string()))
266 },
267 )
268}
269
270pub fn receive_pack_stream_into_local_repository<R: Read>(
275 remote_git_dir: &Path,
276 format: ObjectFormat,
277 header: &ReceivePackPushRequestHeader,
278 pack_reader: &mut R,
279) -> Result<ReceivePackReportStatus> {
280 let remote_store = FileRefStore::new(remote_git_dir, format);
281 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
282 let pack_prefix = read_optional_pack_prefix(pack_reader)?;
283 let validation_request = ReceivePackPushRequest {
284 commands: header.commands.clone(),
285 push_options: header.push_options.clone(),
286 packfile: pack_prefix.clone().unwrap_or_default(),
287 };
288 validate_receive_pack_push_request_features(
289 &receive_pack_features(format),
290 &validation_request,
291 )?;
292
293 let deletes_applied_with_updates = std::cell::RefCell::new(HashSet::<String>::new());
294 for command in header
295 .commands
296 .commands
297 .iter()
298 .filter(|command| command.new_id.is_null())
299 {
300 let current = match remote_store.read_ref(&command.name)? {
301 Some(RefTarget::Direct(oid)) => Some(oid),
302 Some(RefTarget::Symbolic(_)) | None => None,
303 };
304 if !command.old_id.is_null() && current != Some(command.old_id.clone()) {
305 return Err(GitError::Transaction(format!(
306 "expected ref {} to match",
307 command.name
308 )));
309 }
310 }
311
312 let updates = header
313 .commands
314 .commands
315 .iter()
316 .filter(|command| !command.new_id.is_null())
317 .cloned()
318 .collect::<Vec<_>>();
319 if !updates.is_empty() {
320 if let Some(prefix) = pack_prefix {
321 let mut stream = Cursor::new(prefix).chain(pack_reader);
322 remote_db
323 .install_raw_pack_from_reader(&mut stream)
324 .map(|_| ())?;
325 }
326 for command in &updates {
327 if !remote_db.contains(&command.new_id)? {
328 return Err(GitError::InvalidObject(format!(
329 "receive-pack packfile did not provide {}",
330 command.new_id
331 )));
332 }
333 }
334 let applied = apply_receive_pack_ref_transaction(
335 remote_git_dir,
336 format,
337 &remote_store,
338 &updates,
339 &header.commands.commands,
340 )?;
341 deletes_applied_with_updates.borrow_mut().extend(applied);
342 }
343
344 for command in header
345 .commands
346 .commands
347 .iter()
348 .filter(|command| command.new_id.is_null())
349 {
350 if deletes_applied_with_updates
351 .borrow()
352 .contains(command.name.as_str())
353 {
354 continue;
355 }
356 remote_store
357 .delete_ref_checked(DeleteRef {
358 name: command.name.clone(),
359 expected_old: (!command.old_id.is_null()).then_some(command.old_id),
360 reflog: None,
361 })
362 .map(|_| ())
363 .map_err(|err| GitError::Transaction(err.to_string()))?;
364 }
365
366 Ok(ReceivePackReportStatus {
367 unpack: ReceivePackUnpackStatus::Ok,
368 commands: header
369 .commands
370 .commands
371 .iter()
372 .map(|command| ReceivePackCommandStatus::Ok {
373 name: command.name.clone(),
374 })
375 .collect(),
376 })
377}
378
379fn read_optional_pack_prefix(reader: &mut impl Read) -> Result<Option<Vec<u8>>> {
380 let mut prefix = [0u8; 4];
381 loop {
382 match reader.read(&mut prefix[..1]) {
383 Ok(0) => return Ok(None),
384 Ok(1) => break,
385 Ok(_) => unreachable!("one-byte read returned more than one byte"),
386 Err(err) if err.kind() == ErrorKind::Interrupted => {}
387 Err(err) => return Err(err.into()),
388 }
389 }
390 reader.read_exact(&mut prefix[1..])?;
391 if &prefix != b"PACK" {
392 return Err(GitError::InvalidFormat(
393 "receive-pack packfile must start with PACK".into(),
394 ));
395 }
396 Ok(Some(prefix.to_vec()))
397}
398
399fn receive_pack_log_all_ref_updates(git_dir: &Path) -> bool {
400 let Ok(config) = fs::read_to_string(git_dir.join("config")) else {
401 return false;
402 };
403 let mut in_core = false;
404 for raw_line in config.lines() {
405 let line = raw_line.trim();
406 if line.starts_with('[') && line.ends_with(']') {
407 in_core = line.eq_ignore_ascii_case("[core]");
408 continue;
409 }
410 if !in_core || line.starts_with('#') || line.starts_with(';') {
411 continue;
412 }
413 let Some((name, value)) = line.split_once('=') else {
414 continue;
415 };
416 if name.trim().eq_ignore_ascii_case("logallrefupdates") {
417 return matches!(
418 value.trim().trim_matches('"').to_ascii_lowercase().as_str(),
419 "true" | "yes" | "on" | "1" | "always"
420 );
421 }
422 }
423 false
424}
425
426fn receive_pack_should_write_reflog(refname: &str) -> bool {
427 refname == "HEAD"
428 || refname.starts_with("refs/heads/")
429 || refname.starts_with("refs/remotes/")
430 || refname.starts_with("refs/notes/")
431}
432
433fn receive_pack_reflog_entry(
434 format: ObjectFormat,
435 old_oid: ObjectId,
436 new_oid: ObjectId,
437) -> ReflogEntry {
438 let old_oid = if old_oid.is_null() {
439 ObjectId::null(format)
440 } else {
441 old_oid
442 };
443 ReflogEntry {
444 old_oid,
445 new_oid,
446 committer: receive_pack_reflog_committer(),
447 message: b"push".to_vec(),
448 }
449}
450
451fn receive_pack_reflog_committer() -> Vec<u8> {
452 let seconds = SystemTime::now()
453 .duration_since(UNIX_EPOCH)
454 .map(|duration| duration.as_secs())
455 .unwrap_or(0);
456 format!("Git Rs <sley@example.invalid> {seconds} +0000").into_bytes()
457}
458
459pub fn receive_pack_reachable_pack_into_local_repository(
466 remote_git_dir: &Path,
467 format: ObjectFormat,
468 request: &ReceivePackPushRequest,
469 source_db: &FileObjectDatabase,
470 starts: Vec<ObjectId>,
471 excluded: HashSet<ObjectId>,
472) -> Result<ReceivePackReportStatus> {
473 let remote_store = FileRefStore::new(remote_git_dir, format);
474 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
475 let mut starts = Some(starts);
476 let deletes_applied_with_updates = std::cell::RefCell::new(HashSet::<String>::new());
477 apply_receive_pack_push_request(
478 &receive_pack_features(format),
479 request,
480 |name| match remote_store.read_ref(name)? {
481 Some(RefTarget::Direct(oid)) => Ok(Some(oid)),
482 Some(RefTarget::Symbolic(_)) | None => Ok(None),
483 },
484 |_| {
485 let starts = starts.take().ok_or_else(|| {
486 GitError::InvalidFormat("receive-pack attempted to install pack twice".into())
487 })?;
488 build_and_install_reachable_pack(
489 source_db,
490 &remote_db,
491 format,
492 starts,
493 &excluded,
494 RawPackInstallOptions {
495 promisor: false,
496 ..Default::default()
497 },
498 )?;
499 Ok(())
500 },
501 |oid| remote_db.contains(oid),
502 |commands| {
503 let applied = apply_receive_pack_ref_transaction(
504 remote_git_dir,
505 format,
506 &remote_store,
507 commands,
508 &request.commands.commands,
509 )?;
510 deletes_applied_with_updates.borrow_mut().extend(applied);
511 Ok(())
512 },
513 |command| {
514 if deletes_applied_with_updates
515 .borrow()
516 .contains(command.name.as_str())
517 {
518 return Ok(());
519 }
520 remote_store
521 .delete_ref_checked(DeleteRef {
522 name: command.name.clone(),
523 expected_old: (!command.old_id.is_null()).then_some(command.old_id),
524 reflog: None,
525 })
526 .map(|_| ())
527 .map_err(|err| GitError::Transaction(err.to_string()))
528 },
529 )
530}
531
532pub(crate) fn apply_receive_pack_ref_transaction(
533 remote_git_dir: &Path,
534 format: ObjectFormat,
535 store: &FileRefStore,
536 updates: &[ReceivePackCommand],
537 all_commands: &[ReceivePackCommand],
538) -> Result<HashSet<String>> {
539 let updates = canonical_receive_pack_update_commands(store, updates)?;
540 let deletes = all_commands
541 .iter()
542 .filter(|command| command.new_id.is_null())
543 .collect::<Vec<_>>();
544 let mut tx = store.transaction();
545 for command in &deletes {
546 tx.delete_with_precondition(
547 command.name.clone(),
548 RefDeletePrecondition::Direct((!command.old_id.is_null()).then_some(command.old_id)),
549 None,
550 );
551 }
552 let log_updates = receive_pack_log_all_ref_updates(remote_git_dir);
553 for command in &updates {
554 let precondition = if command.old_id.is_null() {
555 RefPrecondition::MustNotExist
556 } else {
557 RefPrecondition::MustExistAndMatch(RefTarget::Direct(command.old_id))
558 };
559 let reflog = if log_updates && receive_pack_should_write_reflog(&command.name) {
560 Some(receive_pack_reflog_entry(
561 format,
562 command.old_id,
563 command.new_id,
564 ))
565 } else {
566 None
567 };
568 tx.update_to(
569 command.name.clone(),
570 RefTarget::Direct(command.new_id),
571 precondition,
572 reflog,
573 );
574 }
575 tx.commit()?;
576 Ok(deletes
577 .into_iter()
578 .map(|command| command.name.clone())
579 .collect())
580}
581
582fn canonical_receive_pack_update_commands(
583 store: &FileRefStore,
584 commands: &[ReceivePackCommand],
585) -> Result<Vec<ReceivePackCommand>> {
586 let mut by_actual = HashMap::<String, ObjectId>::new();
587 let mut canonical = Vec::with_capacity(commands.len());
588 for command in commands {
589 let name = match store.read_ref(&command.name)? {
590 Some(RefTarget::Symbolic(target)) => target,
591 Some(RefTarget::Direct(_)) | None => command.name.clone(),
592 };
593 if let Some(existing) = by_actual.get(&name) {
594 if existing != &command.new_id {
595 return Err(GitError::Command("refusing inconsistent update".into()));
596 }
597 } else {
598 by_actual.insert(name.clone(), command.new_id);
599 }
600 canonical.push(ReceivePackCommand {
601 old_id: command.old_id,
602 new_id: command.new_id,
603 name,
604 });
605 }
606 Ok(canonical)
607}
608
609pub fn local_fetch_advertisements(
612 git_dir: &Path,
613 format: ObjectFormat,
614) -> Result<Vec<RefAdvertisement>> {
615 let store = FileRefStore::new_without_reference_backend_env(git_dir, format);
616 let mut advertisements = Vec::new();
617 if let Some(target) = store.read_ref("HEAD")? {
618 let reference = Ref {
619 name: "HEAD".to_string(),
620 target,
621 };
622 if let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? {
623 advertisements.push(RefAdvertisement {
624 oid,
625 name: reference.name,
626 capabilities: Vec::new(),
627 });
628 }
629 }
630 for reference in store.list_refs()? {
631 let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? else {
632 continue;
633 };
634 advertisements.push(RefAdvertisement {
635 oid,
636 name: reference.name,
637 capabilities: Vec::new(),
638 });
639 }
640 Ok(advertisements)
641}
642
643pub fn local_have_oids(git_dir: &Path, format: ObjectFormat) -> Result<Vec<ObjectId>> {
647 let mut seen = HashSet::new();
648 let mut haves = Vec::new();
649 for advertisement in local_fetch_advertisements(git_dir, format)? {
650 if seen.insert(advertisement.oid) {
651 haves.push(advertisement.oid);
652 }
653 }
654 let db = FileObjectDatabase::from_git_dir(git_dir, format);
655 for oid in db.object_ids()? {
656 if seen.insert(oid) {
657 haves.push(oid);
658 }
659 }
660 Ok(haves)
661}
662
663#[derive(Debug, Clone)]
670pub struct LocalDeepenPlan {
671 pub depth: u32,
675 pub deepen_since: bool,
677 pub deepen_not: usize,
679 pub client_shallow: Vec<ObjectId>,
682 pub shallow_info: Vec<ProtocolV2FetchShallowInfo>,
685 pub excluded: HashSet<ObjectId>,
690 pub extra_wants: Vec<ObjectId>,
694}
695
696fn peel_to_commit<R: ObjectReader>(
700 remote_db: &R,
701 format: ObjectFormat,
702 oid: &ObjectId,
703) -> Result<Option<ObjectId>> {
704 let mut oid = *oid;
705 loop {
706 let object = remote_db.read_object(&oid)?;
707 match object.object_type {
708 ObjectType::Commit => return Ok(Some(oid)),
709 ObjectType::Tag => oid = Tag::parse_ref(format, &object.body)?.object,
710 _ => return Ok(None),
711 }
712 }
713}
714
715pub fn compute_local_deepen<R: ObjectReader>(
728 remote_db: &R,
729 format: ObjectFormat,
730 heads: &[ObjectId],
731 client_shallow: Vec<ObjectId>,
732 depth: u32,
733 deepen_relative: bool,
734) -> Result<LocalDeepenPlan> {
735 let depth = if deepen_relative && depth < INFINITE_DEPTH {
738 depth.saturating_add(client_shallow_min_depth(
739 remote_db,
740 format,
741 heads,
742 &client_shallow,
743 )?)
744 } else {
745 depth
746 };
747 let mut min_depth: HashMap<ObjectId, u32> = HashMap::new();
748 let mut queue: VecDeque<ObjectId> = VecDeque::new();
749 for head in heads {
750 let Some(commit) = peel_to_commit(remote_db, format, head)? else {
751 continue;
752 };
753 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(commit) {
754 entry.insert(0);
755 queue.push_back(commit);
756 }
757 }
758 let mut boundary = Vec::new();
763 let mut boundary_parents = HashSet::new();
764 while let Some(oid) = queue.pop_front() {
765 let commit_depth = min_depth[&oid];
766 let object = remote_db.read_object(&oid)?;
767 let parents = sley_odb::grafted_parents(
768 remote_db,
769 &oid,
770 Commit::parse_ref(format, &object.body)?.parents,
771 );
772 if (depth != INFINITE_DEPTH && commit_depth + 1 >= depth)
776 || remote_db.is_shallow_graft(&oid)
777 {
778 boundary.push(oid);
779 boundary_parents.extend(parents);
780 continue;
781 }
782 for parent in parents {
783 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(parent) {
784 entry.insert(commit_depth + 1);
785 queue.push_back(parent);
786 }
787 }
788 }
789 let excluded = boundary_parents
793 .into_iter()
794 .filter(|parent| !min_depth.contains_key(parent))
795 .collect::<HashSet<_>>();
796
797 let client: HashSet<ObjectId> = client_shallow.iter().copied().collect();
798 let boundary_set: HashSet<ObjectId> = boundary.iter().copied().collect();
799 let mut shallow_info = Vec::new();
800 for oid in &boundary {
801 if !client.contains(oid) {
802 shallow_info.push(ProtocolV2FetchShallowInfo::Shallow(*oid));
803 }
804 }
805 let mut extra_wants = Vec::new();
806 for oid in &client_shallow {
807 let unshallowed = min_depth.contains_key(oid) && !boundary_set.contains(oid);
810 if !unshallowed {
811 continue;
812 }
813 shallow_info.push(ProtocolV2FetchShallowInfo::Unshallow(*oid));
814 let object = remote_db.read_object(oid)?;
815 extra_wants.extend(sley_odb::grafted_parents(
816 remote_db,
817 oid,
818 Commit::parse_ref(format, &object.body)?.parents,
819 ));
820 }
821 Ok(LocalDeepenPlan {
822 depth,
823 deepen_since: false,
824 deepen_not: 0,
825 client_shallow,
826 shallow_info,
827 excluded,
828 extra_wants,
829 })
830}
831
832pub const INFINITE_DEPTH: u32 = 0x7fff_ffff;
835
836fn client_shallow_min_depth<R: ObjectReader>(
840 remote_db: &R,
841 format: ObjectFormat,
842 heads: &[ObjectId],
843 client_shallow: &[ObjectId],
844) -> Result<u32> {
845 if client_shallow.is_empty() {
846 return Ok(0);
847 }
848 let client: HashSet<ObjectId> = client_shallow.iter().copied().collect();
849 let mut min_depth: HashMap<ObjectId, u32> = HashMap::new();
850 let mut queue: VecDeque<ObjectId> = VecDeque::new();
851 for head in heads {
852 let Some(commit) = peel_to_commit(remote_db, format, head)? else {
853 continue;
854 };
855 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(commit) {
856 entry.insert(1);
857 queue.push_back(commit);
858 }
859 }
860 let mut best: u32 = 0;
861 while let Some(oid) = queue.pop_front() {
862 let commit_depth = min_depth[&oid];
863 if client.contains(&oid) && (best == 0 || commit_depth < best) {
864 best = commit_depth;
865 }
866 let object = remote_db.read_object(&oid)?;
867 let parents = sley_odb::grafted_parents(
868 remote_db,
869 &oid,
870 Commit::parse_ref(format, &object.body)?.parents,
871 );
872 for parent in parents {
873 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(parent) {
874 entry.insert(commit_depth + 1);
875 queue.push_back(parent);
876 }
877 }
878 }
879 Ok(best)
880}
881
882pub fn compute_local_deepen_by_rev_list<R: ObjectReader>(
888 remote_db: &R,
889 format: ObjectFormat,
890 heads: &[ObjectId],
891 client_shallow: Vec<ObjectId>,
892 since: Option<i64>,
893 deepen_not: &[ObjectId],
894) -> Result<LocalDeepenPlan> {
895 let mut excluded_not: HashSet<ObjectId> = HashSet::new();
897 let mut queue: VecDeque<ObjectId> = VecDeque::new();
898 for tip in deepen_not {
899 if let Some(commit) = peel_to_commit(remote_db, format, tip)?
900 && excluded_not.insert(commit)
901 {
902 queue.push_back(commit);
903 }
904 }
905 while let Some(oid) = queue.pop_front() {
906 let object = remote_db.read_object(&oid)?;
907 for parent in sley_odb::grafted_parents(
908 remote_db,
909 &oid,
910 Commit::parse_ref(format, &object.body)?.parents,
911 ) {
912 if excluded_not.insert(parent) {
913 queue.push_back(parent);
914 }
915 }
916 }
917
918 let commit_time = |oid: &ObjectId| -> Result<i64> {
919 let object = remote_db.read_object(oid)?;
920 Ok(Commit::parse_ref(format, &object.body)?
921 .committer_signature()
922 .map(|signature| signature.time.seconds)
923 .unwrap_or(0))
924 };
925 let keeps = |oid: &ObjectId| -> Result<bool> {
926 if excluded_not.contains(oid) {
927 return Ok(false);
928 }
929 match since {
930 Some(since) => Ok(commit_time(oid)? >= since),
931 None => Ok(true),
932 }
933 };
934
935 let mut kept: HashSet<ObjectId> = HashSet::new();
938 let mut kept_order: Vec<ObjectId> = Vec::new();
939 let mut queue: VecDeque<ObjectId> = VecDeque::new();
940 for head in heads {
941 let Some(commit) = peel_to_commit(remote_db, format, head)? else {
942 continue;
943 };
944 if keeps(&commit)? && kept.insert(commit) {
945 kept_order.push(commit);
946 queue.push_back(commit);
947 }
948 }
949 while let Some(oid) = queue.pop_front() {
950 let object = remote_db.read_object(&oid)?;
951 for parent in sley_odb::grafted_parents(
952 remote_db,
953 &oid,
954 Commit::parse_ref(format, &object.body)?.parents,
955 ) {
956 if !kept.contains(&parent) && keeps(&parent)? {
957 kept.insert(parent);
958 kept_order.push(parent);
959 queue.push_back(parent);
960 }
961 }
962 }
963 if kept.is_empty() {
964 return Err(GitError::Command(
966 "no commits selected for shallow requests".into(),
967 ));
968 }
969
970 let mut boundary = Vec::new();
972 let mut boundary_set: HashSet<ObjectId> = HashSet::new();
973 let mut excluded: HashSet<ObjectId> = HashSet::new();
974 for oid in &kept_order {
975 let object = remote_db.read_object(oid)?;
976 let parents = sley_odb::grafted_parents(
977 remote_db,
978 oid,
979 Commit::parse_ref(format, &object.body)?.parents,
980 );
981 let mut is_boundary = false;
982 for parent in parents {
983 if !kept.contains(&parent) {
984 is_boundary = true;
985 excluded.insert(parent);
986 }
987 }
988 if is_boundary && boundary_set.insert(*oid) {
989 boundary.push(*oid);
990 }
991 }
992
993 let client: HashSet<ObjectId> = client_shallow.iter().copied().collect();
994 let mut shallow_info = Vec::new();
995 for oid in &boundary {
996 if !client.contains(oid) {
997 shallow_info.push(ProtocolV2FetchShallowInfo::Shallow(*oid));
998 }
999 }
1000 let mut extra_wants = Vec::new();
1001 for oid in &client_shallow {
1002 let unshallowed = kept.contains(oid) && !boundary_set.contains(oid);
1003 if !unshallowed {
1004 continue;
1005 }
1006 shallow_info.push(ProtocolV2FetchShallowInfo::Unshallow(*oid));
1007 let object = remote_db.read_object(oid)?;
1008 extra_wants.extend(sley_odb::grafted_parents(
1009 remote_db,
1010 oid,
1011 Commit::parse_ref(format, &object.body)?.parents,
1012 ));
1013 }
1014 Ok(LocalDeepenPlan {
1015 depth: 0,
1016 deepen_since: since.is_some(),
1017 deepen_not: deepen_not.len(),
1018 client_shallow,
1019 shallow_info,
1020 excluded,
1021 extra_wants,
1022 })
1023}
1024
1025#[allow(clippy::too_many_arguments)]
1038pub fn install_fetch_pack_via_local_upload_pack(
1039 git_dir: &Path,
1040 remote_git_dir: &Path,
1041 format: ObjectFormat,
1042 wants: Vec<ObjectId>,
1043 deepen: Option<&LocalDeepenPlan>,
1044 promisor: bool,
1045 record_promisor_refs: bool,
1046 filter: Option<sley_odb::PackObjectFilter>,
1047 custom_haves: Option<Vec<ObjectId>>,
1048 refetch: bool,
1049 unpack_limit: Option<usize>,
1050) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
1051 if wants.is_empty() {
1052 return Ok(Vec::new());
1053 }
1054 let local_db = FileObjectDatabase::from_git_dir(git_dir, format)
1055 .with_promisor_remote_present(repo_has_promisor_remote(git_dir));
1056 let all_wants_present = wants
1057 .iter()
1058 .map(|want| local_db.contains(want))
1059 .collect::<Result<Vec<_>>>()?
1060 .into_iter()
1061 .all(|contains| contains);
1062 let deepen_noop = match deepen {
1063 Some(plan) => plan.shallow_info.is_empty() && plan.extra_wants.is_empty(),
1064 None => true,
1065 };
1066 if all_wants_present && deepen_noop && !refetch {
1067 sley_protocol::trace_packet_write_payload(b"0000");
1068 return Ok(Vec::new());
1069 }
1070
1071 let request = UploadPackRequest {
1072 wants,
1073 filter: filter
1074 .as_ref()
1075 .and_then(upload_pack_filter_protocol_spec),
1076 capabilities: deepen
1079 .map(|_| {
1080 vec![Capability {
1081 name: "shallow".into(),
1082 value: None,
1083 }]
1084 })
1085 .unwrap_or_default(),
1086 shallow: deepen
1087 .map(|plan| plan.client_shallow.clone())
1088 .unwrap_or_default(),
1089 deepen: deepen.and_then(|plan| (plan.depth > 0).then_some(plan.depth)),
1090 ..UploadPackRequest::default()
1091 };
1092 let mut encoded_request = Vec::new();
1093 write_upload_pack_request(&mut encoded_request, Some(&request))?;
1094 let decoded_request = read_upload_pack_request(format, &mut encoded_request.as_slice())?
1095 .ok_or_else(|| GitError::InvalidFormat("encoded upload-pack request was empty".into()))?;
1096
1097 let direct_promisor_object_fetch = promisor && deepen.is_none() && !record_promisor_refs;
1100 if direct_promisor_object_fetch && local_upload_pack_client_wants_v2(git_dir) {
1101 trace_local_upload_pack_v2_capabilities(remote_git_dir, format);
1102 }
1103 let haves = if refetch || direct_promisor_object_fetch {
1104 Vec::new()
1105 } else if let Some(haves) = custom_haves {
1106 haves
1107 } else {
1108 local_have_oids(git_dir, format)?
1109 };
1110 let negotiation = UploadPackNegotiationRequest { haves, done: true };
1111 let mut encoded_negotiation = Vec::new();
1112 write_upload_pack_negotiation_request(&mut encoded_negotiation, &negotiation)?;
1113 let decoded_negotiation =
1114 read_upload_pack_negotiation_request(format, &mut encoded_negotiation.as_slice())?;
1115 sley_core::trace2::data("negotiation_v2", "total_rounds", 1);
1116
1117 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
1118 for want in &decoded_request.wants {
1119 if !remote_db.contains(want)? {
1120 return Err(GitError::InvalidObject(format!(
1121 "upload-pack requested missing object {want}"
1122 )));
1123 }
1124 }
1125 let known_haves = decoded_negotiation
1126 .haves
1127 .into_iter()
1128 .filter_map(|oid| match remote_db.contains(&oid) {
1129 Ok(true) => Some(Ok(oid)),
1130 Ok(false) => None,
1131 Err(err) => Some(Err(err)),
1132 })
1133 .collect::<Result<Vec<_>>>()?;
1134 trace2_fetch_info(
1138 known_haves.len(),
1139 decoded_request.wants.len(),
1140 deepen.map(|plan| plan.depth).unwrap_or(0),
1141 deepen.map(|plan| plan.client_shallow.len()).unwrap_or(0),
1142 deepen.is_some_and(|plan| plan.deepen_since),
1143 deepen.map(|plan| plan.deepen_not).unwrap_or(0),
1144 filter.as_ref(),
1145 );
1146 let mut excluded = match deepen {
1151 Some(plan) => {
1152 let cut: HashSet<ObjectId> = plan.client_shallow.iter().copied().collect();
1153 sley_odb::collect_reachable_object_ids_with_cut(&remote_db, format, known_haves, &cut)?
1154 }
1155 None => {
1156 sley_odb::collect_reachable_object_ids_tolerating_promised_missing(
1161 &local_db,
1162 format,
1163 known_haves,
1164 )?
1165 }
1166 };
1167 let mut starts = decoded_request.wants;
1168 let promisor_ref_wants = starts.iter().copied().collect::<HashSet<_>>();
1169 for want in &starts {
1170 excluded.remove(want);
1171 }
1172 if let Some(plan) = deepen {
1173 excluded.extend(plan.excluded.iter().copied());
1176 starts.extend(plan.extra_wants.iter().copied());
1177 }
1178 let install = build_and_install_reachable_pack_filtered(
1179 &remote_db,
1180 &local_db,
1181 format,
1182 starts,
1183 &excluded,
1184 RawPackInstallOptions {
1185 promisor,
1186 ..Default::default()
1187 },
1188 filter.clone(),
1189 unpack_limit,
1190 )?;
1191 if promisor
1192 && record_promisor_refs
1193 && let Some(result) = install
1194 && let Some(promisor_path) = result.promisor_path
1195 {
1196 append_promisor_ref_lines(&promisor_path, remote_git_dir, format, &promisor_ref_wants)?;
1197 }
1198 Ok(deepen
1199 .map(|plan| plan.shallow_info.clone())
1200 .unwrap_or_default())
1201}
1202
1203fn local_upload_pack_client_wants_v2(git_dir: &Path) -> bool {
1204 sley_config::read_repo_config(git_dir, None)
1205 .ok()
1206 .and_then(|config| config.get("protocol", None, "version").map(str::to_string))
1207 .as_deref()
1208 == Some("2")
1209}
1210
1211fn repo_has_promisor_remote(git_dir: &Path) -> bool {
1212 let Ok(config) = sley_config::read_repo_config(git_dir, None) else {
1213 return false;
1214 };
1215 if config
1216 .get("extensions", None, "partialclone")
1217 .is_some_and(|value| !value.is_empty())
1218 {
1219 return true;
1220 }
1221 config.sections.iter().any(|section| {
1222 section.name.eq_ignore_ascii_case("remote")
1223 && section
1224 .subsection
1225 .as_deref()
1226 .is_some_and(|name| config.get_bool("remote", Some(name), "promisor") == Some(true))
1227 })
1228}
1229
1230fn trace_local_upload_pack_v2_capabilities(remote_git_dir: &Path, format: ObjectFormat) {
1231 sley_protocol::set_packet_trace_identity("fetch");
1232 let config = sley_config::read_repo_config(remote_git_dir, None).unwrap_or_default();
1233 sley_protocol::trace_packet_read_payload(b"version 2\n");
1234 sley_protocol::trace_packet_read_payload(
1235 format!("agent={UPSTREAM_GIT_COMPAT_VERSION}\n").as_bytes(),
1236 );
1237 sley_protocol::trace_packet_read_payload(b"ls-refs=unborn\n");
1238 let mut fetch = "fetch=shallow wait-for-done".to_string();
1239 if config
1240 .get_bool("uploadpack", None, "allowfilter")
1241 .unwrap_or(false)
1242 {
1243 fetch.push_str(" filter");
1244 }
1245 if config
1246 .get_bool("uploadpack", None, "allowrefinwant")
1247 .unwrap_or(false)
1248 {
1249 fetch.push_str(" ref-in-want");
1250 }
1251 fetch.push('\n');
1252 sley_protocol::trace_packet_read_payload(fetch.as_bytes());
1253 sley_protocol::trace_packet_read_payload(
1254 format!("object-format={}\n", format.name()).as_bytes(),
1255 );
1256 sley_protocol::trace_packet_read_payload(b"0000");
1257}
1258
1259pub(crate) fn upload_pack_filter_protocol_spec(filter: &sley_odb::PackObjectFilter) -> Option<String> {
1260 match filter {
1261 sley_odb::PackObjectFilter::BlobNone => Some("blob:none".to_string()),
1262 sley_odb::PackObjectFilter::BlobLimit(limit) => Some(format!("blob:limit={limit}")),
1263 sley_odb::PackObjectFilter::TreeDepth(depth) => Some(format!("tree:{depth}")),
1264 sley_odb::PackObjectFilter::SparsePathSet(_) => None,
1265 }
1266}
1267
1268fn append_promisor_ref_lines(
1269 promisor_path: &Path,
1270 remote_git_dir: &Path,
1271 format: ObjectFormat,
1272 wanted: &HashSet<ObjectId>,
1273) -> Result<()> {
1274 if wanted.is_empty() {
1275 return Ok(());
1276 }
1277 let store = FileRefStore::new(remote_git_dir, format);
1278 let mut lines = Vec::new();
1279 if let Some(head_target) = store.read_ref("HEAD")? {
1280 let head = Ref {
1281 name: "HEAD".into(),
1282 target: head_target,
1283 };
1284 if let Some((oid, _)) = resolve_for_each_ref_target(&store, &head)?
1285 && wanted.contains(&oid)
1286 {
1287 lines.push(format!("{oid} HEAD\n"));
1288 }
1289 }
1290 for reference in store.list_refs()? {
1291 let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? else {
1292 continue;
1293 };
1294 if wanted.contains(&oid) {
1295 lines.push(format!("{oid} {}\n", reference.name));
1296 }
1297 }
1298 if lines.is_empty() {
1299 return Ok(());
1300 }
1301 lines.sort();
1302 let mut file = fs::OpenOptions::new().append(true).open(promisor_path)?;
1303 use std::io::Write as _;
1304 for line in lines {
1305 file.write_all(line.as_bytes())?;
1306 }
1307 Ok(())
1308}
1309
1310fn trace2_fetch_info(
1314 haves: usize,
1315 wants: usize,
1316 depth: u32,
1317 shallows: usize,
1318 deepen_since: bool,
1319 deepen_not: usize,
1320 filter: Option<&sley_odb::PackObjectFilter>,
1321) {
1322 let Some(path) = std::env::var_os("GIT_TRACE2_EVENT") else {
1323 return;
1324 };
1325 if path.is_empty() {
1326 return;
1327 }
1328 let filter_json = match filter {
1329 Some(sley_odb::PackObjectFilter::BlobNone) => "\"blob:none\"".to_string(),
1330 Some(sley_odb::PackObjectFilter::BlobLimit(limit)) => {
1331 format!("\"blob:limit={limit}\"")
1332 }
1333 Some(sley_odb::PackObjectFilter::TreeDepth(depth)) => {
1334 format!("\"tree:{depth}\"")
1335 }
1336 Some(sley_odb::PackObjectFilter::SparsePathSet(_)) => "\"sparse:oid\"".to_string(),
1337 None => "null".to_string(),
1338 };
1339 let line = format!(
1340 "{{\"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"
1341 );
1342 if let Ok(mut file) = std::fs::OpenOptions::new()
1343 .create(true)
1344 .append(true)
1345 .open(&path)
1346 {
1347 use std::io::Write as _;
1348 let _ = file.write_all(line.as_bytes());
1349 }
1350}
1351
1352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1363enum LsRefsUnbornConfig {
1364 Ignore,
1365 Allow,
1366 Advertise,
1367}
1368
1369fn lsrefs_unborn_config(config: &GitConfig) -> LsRefsUnbornConfig {
1370 match config.get("lsrefs", None, "unborn") {
1371 Some("ignore") => LsRefsUnbornConfig::Ignore,
1372 Some("allow") => LsRefsUnbornConfig::Allow,
1373 Some("advertise") | None => LsRefsUnbornConfig::Advertise,
1374 Some(_) => LsRefsUnbornConfig::Advertise,
1375 }
1376}
1377
1378fn upload_pack_blob_packfile_uri_configured(config: &GitConfig) -> bool {
1379 config
1380 .get_all("uploadpack", None, "blobpackfileuri")
1381 .into_iter()
1382 .any(|value| value.is_some_and(|value| !value.is_empty()))
1383}
1384
1385fn upload_pack_v2_capabilities(
1389 format: ObjectFormat,
1390 config: &GitConfig,
1391) -> Result<Vec<Capability>> {
1392 let mut capabilities = vec![
1393 Capability {
1394 name: "agent".into(),
1395 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
1396 },
1397 encode_protocol_v2_ls_refs_capability(&ProtocolV2LsRefsFeatures {
1398 unborn: lsrefs_unborn_config(config) == LsRefsUnbornConfig::Advertise,
1399 unknown: Vec::new(),
1400 })?,
1401 encode_protocol_v2_fetch_capability(&ProtocolV2FetchFeatures {
1402 shallow: true,
1403 wait_for_done: true,
1404 filter: config
1405 .get_bool("uploadpack", None, "allowfilter")
1406 .unwrap_or(false),
1407 ref_in_want: config
1408 .get_bool("uploadpack", None, "allowrefinwant")
1409 .unwrap_or(false),
1410 packfile_uris: upload_pack_blob_packfile_uri_configured(config),
1411 ..ProtocolV2FetchFeatures::default()
1412 })?,
1413 Capability {
1414 name: "server-option".into(),
1415 value: None,
1416 },
1417 Capability {
1418 name: "object-format".into(),
1419 value: Some(format.name().into()),
1420 },
1421 ];
1422 if config
1423 .get_bool("transfer", None, "advertisesid")
1424 .unwrap_or(false)
1425 {
1426 capabilities.push(Capability {
1427 name: "session-id".into(),
1428 value: Some("sley".into()),
1429 });
1430 }
1431 if config
1436 .get_bool("uploadpack", None, "advertisebundleuris")
1437 .unwrap_or(false)
1438 {
1439 capabilities.push(Capability {
1440 name: "bundle-uri".into(),
1441 value: None,
1442 });
1443 }
1444 Ok(capabilities)
1445}
1446
1447fn head_symref_target(store: &FileRefStore) -> Result<Option<String>> {
1451 match store.read_ref("HEAD")? {
1452 Some(RefTarget::Symbolic(name)) => Ok(Some(name)),
1453 _ => Ok(None),
1454 }
1455}
1456
1457fn local_ls_refs_v2_records(
1461 git_dir: &Path,
1462 format: ObjectFormat,
1463 request: &ProtocolV2LsRefsRequest,
1464 config: &GitConfig,
1465) -> Result<Vec<ProtocolV2LsRefsRecord>> {
1466 let store = FileRefStore::new(git_dir, format);
1467 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1468 let head_symref = head_symref_target(&store)?;
1469
1470 let mut entries: Vec<(String, ObjectId, Option<String>)> = Vec::new();
1473 if let Some(target) = store.read_ref("HEAD")? {
1474 let reference = Ref {
1475 name: "HEAD".to_string(),
1476 target,
1477 };
1478 if let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? {
1479 entries.push(("HEAD".to_string(), oid, head_symref.clone()));
1480 } else if request.unborn && lsrefs_unborn_config(config) != LsRefsUnbornConfig::Ignore {
1481 entries.push((
1484 "HEAD".to_string(),
1485 ObjectId::null(format),
1486 head_symref.clone(),
1487 ));
1488 }
1489 }
1490 for reference in store.list_refs()? {
1491 let name = reference.name.clone();
1492 let Some((oid, symref)) = resolve_for_each_ref_target(&store, &reference)? else {
1493 continue;
1494 };
1495 entries.push((name, oid, symref));
1496 }
1497
1498 let matches_prefix = |name: &str| -> bool {
1499 if request.ref_prefixes.is_empty() {
1500 return true;
1501 }
1502 request
1503 .ref_prefixes
1504 .iter()
1505 .any(|prefix| name.starts_with(prefix.as_str()))
1506 };
1507
1508 let mut records = Vec::new();
1509 for (name, oid, symref) in entries {
1510 if !matches_prefix(&name) {
1511 continue;
1512 }
1513 if name == "HEAD" && oid == ObjectId::null(format) {
1515 records.push(ProtocolV2LsRefsRecord::Unborn {
1516 name,
1517 symref_target: if request.symrefs { symref } else { None },
1518 attributes: Vec::new(),
1519 });
1520 continue;
1521 }
1522 let peeled = if request.peel {
1523 let object = db.read_object(&oid)?;
1524 if object.object_type == ObjectType::Tag {
1525 Some(sley_rev::peel_tags(&db, format, &oid)?)
1526 } else {
1527 None
1528 }
1529 } else {
1530 None
1531 };
1532 let symref_target = if request.symrefs { symref } else { None };
1533 records.push(ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
1534 oid,
1535 name,
1536 peeled,
1537 symref_target,
1538 attributes: Vec::new(),
1539 }));
1540 }
1541 Ok(records)
1542}
1543
1544fn packfile_section_lines(pack: &[u8]) -> Vec<Vec<u8>> {
1550 let chunk = PKT_LINE_MAX_PAYLOAD_LEN - 1;
1551 let mut lines = Vec::new();
1552 for slice in pack.chunks(chunk) {
1553 let mut payload = Vec::with_capacity(slice.len() + 1);
1554 payload.push(1u8); payload.extend_from_slice(slice);
1556 lines.push(payload);
1557 }
1558 lines
1559}
1560
1561fn local_fetch_v2_sections(
1567 git_dir: &Path,
1568 format: ObjectFormat,
1569 request: &ProtocolV2FetchRequest,
1570) -> Result<Vec<ProtocolV2FetchResponseSection>> {
1571 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1572
1573 let mut sections = Vec::new();
1574
1575 if !request.done {
1581 let mut acks: Vec<ProtocolV2FetchAcknowledgment> = Vec::new();
1582 for have in &request.haves {
1583 if db.contains(have)? {
1584 acks.push(ProtocolV2FetchAcknowledgment::Ack(*have));
1585 }
1586 }
1587 if acks.is_empty() {
1588 acks.push(ProtocolV2FetchAcknowledgment::Nak);
1589 }
1590 sections.push(ProtocolV2FetchResponseSection::Acknowledgments(acks));
1591 if !request.wait_for_done {
1594 return Ok(sections);
1595 }
1596 }
1597
1598 if !request.want_refs.is_empty() {
1600 let store = FileRefStore::new(git_dir, format);
1601 let mut wanted = Vec::new();
1602 for name in &request.want_refs {
1603 let reference = Ref {
1604 name: name.clone(),
1605 target: store
1606 .read_ref(name)?
1607 .ok_or_else(|| GitError::not_found(format!("want-ref {name}")))?,
1608 };
1609 let (oid, _) = resolve_for_each_ref_target(&store, &reference)?
1610 .ok_or_else(|| GitError::not_found(format!("want-ref {name}")))?;
1611 wanted.push(sley_protocol::ProtocolV2FetchWantedRef {
1612 oid,
1613 name: name.clone(),
1614 });
1615 }
1616 sections.push(ProtocolV2FetchResponseSection::WantedRefs(wanted));
1617 }
1618
1619 let mut wants: Vec<ObjectId> = request.wants.clone();
1621 if !request.want_refs.is_empty()
1622 && let Some(ProtocolV2FetchResponseSection::WantedRefs(wanted)) = sections
1623 .iter()
1624 .find(|s| matches!(s, ProtocolV2FetchResponseSection::WantedRefs(_)))
1625 {
1626 for w in wanted {
1627 wants.push(w.oid);
1628 }
1629 }
1630
1631 let request_is_deepening =
1639 request.deepen.is_some() || request.deepen_since.is_some() || !request.deepen_not.is_empty();
1640 if !request_is_deepening {
1641 let server_shallow = crate::shallow::read_shallow(git_dir, format)?;
1642 if !server_shallow.is_empty() {
1643 let reachable = collect_reachable_object_ids(&db, format, wants.clone())?;
1644 let shallow_lines: Vec<ProtocolV2FetchShallowInfo> = server_shallow
1645 .iter()
1646 .filter(|oid| reachable.contains(*oid))
1647 .map(|oid| ProtocolV2FetchShallowInfo::Shallow(*oid))
1648 .collect();
1649 if !shallow_lines.is_empty() {
1650 sections.push(ProtocolV2FetchResponseSection::ShallowInfo(shallow_lines));
1651 }
1652 }
1653 }
1654
1655 let mut known_haves: Vec<ObjectId> = Vec::new();
1657 for have in &request.haves {
1658 if db.contains(have)? {
1659 known_haves.push(*have);
1660 }
1661 }
1662 let excluded = collect_reachable_object_ids(&db, format, known_haves)?;
1663 let filter = request
1668 .filter
1669 .as_deref()
1670 .and_then(crate::pack_filter_from_spec);
1671 let pack = if filter.is_some() {
1672 build_reachable_pack_filtered(&db, format, wants, &excluded, filter)?
1673 } else {
1674 build_reachable_pack(&db, format, wants, &excluded)?
1675 }
1676 .map(|pack| pack.pack)
1677 .unwrap_or_default();
1678
1679 sections.push(ProtocolV2FetchResponseSection::Packfile(
1680 packfile_section_lines(&pack),
1681 ));
1682 Ok(sections)
1683}
1684
1685fn write_bundle_uri_command_response(
1696 config: &GitConfig,
1697 writer: &mut impl std::io::Write,
1698) -> Result<()> {
1699 for section in &config.sections {
1700 if !section.name.eq_ignore_ascii_case("bundle") {
1701 continue;
1702 }
1703 for entry in §ion.entries {
1704 let Some(value) = entry.value.as_deref() else {
1705 continue;
1706 };
1707 let key = match §ion.subsection {
1708 Some(subsection) => {
1709 format!("bundle.{subsection}.{}", entry.key.to_ascii_lowercase())
1710 }
1711 None => format!("bundle.{}", entry.key.to_ascii_lowercase()),
1712 };
1713 write_pkt_line_payload(writer, format!("{key}={value}").as_bytes())?;
1714 }
1715 }
1716 write_pkt_line_frame(writer, &PktLineFrame::Flush)?;
1717 Ok(())
1718}
1719
1720pub fn serve_upload_pack_v2(
1721 git_dir: &Path,
1722 format: ObjectFormat,
1723 reader: &mut impl std::io::Read,
1724 writer: &mut impl std::io::Write,
1725) -> Result<()> {
1726 let config = sley_config::read_repo_config(git_dir, None).unwrap_or_default();
1727 serve_upload_pack_v2_with_config(git_dir, format, &config, reader, writer)
1728}
1729
1730pub fn serve_upload_pack_v2_with_config(
1731 git_dir: &Path,
1732 format: ObjectFormat,
1733 config: &GitConfig,
1734 reader: &mut impl std::io::Read,
1735 writer: &mut impl std::io::Write,
1736) -> Result<()> {
1737 let handshake = TransportHandshake {
1738 protocol: ProtocolVersion::V2,
1739 capabilities: upload_pack_v2_capabilities(format, config)?,
1740 };
1741 write_protocol_v2_advertisement(writer, &handshake)?;
1742 writer.flush()?;
1743
1744 loop {
1749 let request = match read_protocol_v2_command_request(reader) {
1750 Ok(request) => request,
1751 Err(GitError::InvalidFormat(message))
1752 if message == "pkt-line stream ended before control packet"
1753 || message == "protocol v2 command request must start with a command line" =>
1754 {
1755 break;
1756 }
1757 Err(err) => return Err(err),
1758 };
1759 if request.command == "bundle-uri" {
1763 write_bundle_uri_command_response(config, writer)?;
1764 writer.flush()?;
1765 continue;
1766 }
1767 match classify_protocol_v2_command_request(&handshake, format, &request)? {
1768 sley_protocol::ProtocolV2Command::LsRefs(ls_refs) => {
1769 let records = local_ls_refs_v2_records(git_dir, format, &ls_refs, config)?;
1770 write_protocol_v2_ls_refs_response(writer, &records)?;
1771 writer.flush()?;
1772 }
1773 sley_protocol::ProtocolV2Command::Fetch(fetch) => {
1774 let sections = local_fetch_v2_sections(git_dir, format, &fetch)?;
1775 write_protocol_v2_fetch_response(writer, §ions)?;
1776 writer.flush()?;
1777 }
1778 sley_protocol::ProtocolV2Command::ObjectInfo(_)
1779 | sley_protocol::ProtocolV2Command::Unknown(_) => {
1780 return Err(GitError::InvalidFormat(format!(
1781 "unsupported protocol v2 command {}",
1782 request.command
1783 )));
1784 }
1785 }
1786 }
1787 Ok(())
1788}
1789
1790#[cfg(test)]
1791mod tests {
1792 use super::*;
1793 use sley_object::{BString, EncodedObject, Tree, TreeEntry};
1794 use sley_odb::ObjectWriter;
1795
1796 #[test]
1797 fn receive_pack_advertises_no_thin_until_server_fixes_thin_packs() {
1798 let features = receive_pack_features(ObjectFormat::Sha1);
1799 assert!(features.no_thin);
1800
1801 let capabilities =
1802 encode_receive_pack_features(&features).expect("test operation should succeed");
1803 assert!(
1804 capabilities
1805 .iter()
1806 .any(|capability| capability.name == "no-thin")
1807 );
1808 }
1809
1810 #[test]
1811 fn local_fetch_from_incomplete_remote_excludes_client_have_closure() {
1812 let root = unique_local_test_dir("incomplete-local-fetch");
1813 let base_git = root.join("base.git");
1814 let patch_git = root.join("patch.git");
1815 let user_git = root.join("user.git");
1816 let direct_git = root.join("direct.git");
1817 for git_dir in [&base_git, &patch_git, &user_git, &direct_git] {
1818 fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1819 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/main\n")
1820 .expect("test operation should succeed");
1821 }
1822
1823 let format = ObjectFormat::Sha1;
1824 let base_db = FileObjectDatabase::from_git_dir(&base_git, format);
1825 let patch_db = FileObjectDatabase::from_git_dir(&patch_git, format);
1826
1827 let text_a = EncodedObject::new(ObjectType::Blob, b"a\nb\nc\nd\ne\nf\ng\nh\ni\n".to_vec());
1828 let text_a_oid = write_test_object(&base_db, &text_a);
1829 let side = EncodedObject::new(ObjectType::Blob, b"side\n".to_vec());
1830 let side_oid = write_test_object(&base_db, &side);
1831 let tree_a = test_tree(&[
1832 (0o100644, b"side", side_oid),
1833 (0o100644, b"text", text_a_oid),
1834 ]);
1835 let tree_a_oid = write_test_object(&base_db, &tree_a);
1836 let commit_a = test_commit(tree_a_oid, &[], b"A\n");
1837 let commit_a_oid = write_test_object(&base_db, &commit_a);
1838
1839 let text_b =
1840 EncodedObject::new(ObjectType::Blob, b"a\nb\nc\nd\ne\nf\ng\nh\ni\nm\n".to_vec());
1841 let text_b_oid = write_test_object(&base_db, &text_b);
1842 let tree_b = test_tree(&[
1843 (0o100644, b"side", side_oid),
1844 (0o100644, b"text", text_b_oid),
1845 ]);
1846 let tree_b_oid = write_test_object(&base_db, &tree_b);
1847 let commit_b = test_commit(tree_b_oid, &[commit_a_oid], b"B\n");
1848 let commit_b_oid = write_test_object(&base_db, &commit_b);
1849
1850 let text_c = EncodedObject::new(
1851 ObjectType::Blob,
1852 b"a\nb\nc\nd\ne\nf\ng\nh\ni\nm\nq\n".to_vec(),
1853 );
1854 let text_c_oid = write_test_object(&patch_db, &text_c);
1855 let tree_c = test_tree(&[
1856 (0o100644, b"side", side_oid),
1857 (0o100644, b"text", text_c_oid),
1858 ]);
1859 let tree_c_oid = write_test_object(&patch_db, &tree_c);
1860 let commit_c = test_commit(tree_c_oid, &[commit_b_oid], b"C\n");
1861 let commit_c_oid = write_test_object(&patch_db, &commit_c);
1862 write_test_object(&patch_db, &tree_b);
1863 write_test_object(&patch_db, &commit_b);
1864 assert!(
1865 !patch_db
1866 .contains(&text_b_oid)
1867 .expect("test operation should succeed"),
1868 "patch repo must be missing the best delta base"
1869 );
1870
1871 install_fetch_pack_via_local_upload_pack(
1872 &user_git,
1873 &base_git,
1874 format,
1875 vec![commit_b_oid],
1876 None,
1877 false,
1878 false,
1879 None,
1880 None,
1881 false,
1882 None,
1883 )
1884 .expect("base fetch should succeed");
1885 assert!(
1886 FileObjectDatabase::from_git_dir(&user_git, format)
1887 .contains(&text_b_oid)
1888 .expect("test operation should succeed"),
1889 "user clone should have the missing base before fetching C"
1890 );
1891
1892 install_fetch_pack_via_local_upload_pack(
1893 &user_git,
1894 &patch_git,
1895 format,
1896 vec![commit_c_oid],
1897 None,
1898 false,
1899 false,
1900 None,
1901 None,
1902 false,
1903 None,
1904 )
1905 .expect("fetch from incomplete remote should succeed when client has the base");
1906 assert!(
1907 FileObjectDatabase::from_git_dir(&user_git, format)
1908 .contains(&commit_c_oid)
1909 .expect("test operation should succeed")
1910 );
1911
1912 let direct = install_fetch_pack_via_local_upload_pack(
1913 &direct_git,
1914 &patch_git,
1915 format,
1916 vec![commit_c_oid],
1917 None,
1918 false,
1919 false,
1920 None,
1921 None,
1922 false,
1923 None,
1924 );
1925 assert!(
1926 direct.is_err(),
1927 "direct fetch from the incomplete patch repo must still fail"
1928 );
1929
1930 fs::remove_dir_all(root).expect("test operation should succeed");
1931 }
1932
1933 #[test]
1934 fn direct_promisor_object_fetch_does_not_walk_missing_local_blobs() {
1935 let root = unique_local_test_dir("direct-promisor-object-fetch");
1936 let remote_git = root.join("remote.git");
1937 let client_git = root.join("client.git");
1938 for git_dir in [&remote_git, &client_git] {
1939 fs::create_dir_all(git_dir.join("objects")).expect("test operation should succeed");
1940 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/main\n")
1941 .expect("test operation should succeed");
1942 }
1943
1944 let format = ObjectFormat::Sha1;
1945 let remote_db = FileObjectDatabase::from_git_dir(&remote_git, format);
1946 let client_db = FileObjectDatabase::from_git_dir(&client_git, format);
1947
1948 let blob = EncodedObject::new(ObjectType::Blob, b"promised\n".to_vec());
1949 let blob_oid = write_test_object(&remote_db, &blob);
1950 let tree = test_tree(&[(0o100644, b"file.txt", blob_oid)]);
1951 let tree_oid = write_test_object(&remote_db, &tree);
1952 let commit = test_commit(tree_oid, &[], b"main\n");
1953 let commit_oid = write_test_object(&remote_db, &commit);
1954
1955 write_test_object(&client_db, &tree);
1956 write_test_object(&client_db, &commit);
1957 assert!(
1958 !client_db
1959 .contains(&blob_oid)
1960 .expect("test operation should succeed"),
1961 "client starts with the promised blob missing"
1962 );
1963
1964 install_fetch_pack_via_local_upload_pack(
1965 &client_git,
1966 &remote_git,
1967 format,
1968 vec![blob_oid],
1969 None,
1970 true,
1971 false,
1972 None,
1973 None,
1974 false,
1975 None,
1976 )
1977 .expect("direct promisor blob fetch should not traverse missing local blobs");
1978
1979 assert!(
1980 FileObjectDatabase::from_git_dir(&client_git, format)
1981 .contains(&blob_oid)
1982 .expect("test operation should succeed"),
1983 "directly wanted promised blob should be installed"
1984 );
1985 assert_eq!(
1986 FileObjectDatabase::from_git_dir(&client_git, format)
1987 .read_object(&commit_oid)
1988 .expect("test operation should succeed")
1989 .object_type,
1990 ObjectType::Commit
1991 );
1992
1993 fs::remove_dir_all(root).expect("test operation should succeed");
1994 }
1995
1996 fn unique_local_test_dir(name: &str) -> std::path::PathBuf {
1997 let nanos = SystemTime::now()
1998 .duration_since(UNIX_EPOCH)
1999 .expect("test operation should succeed")
2000 .as_nanos();
2001 let root =
2002 std::env::temp_dir().join(format!("sley-remote-{name}-{}-{nanos}", std::process::id()));
2003 fs::create_dir_all(&root).expect("test operation should succeed");
2004 root
2005 }
2006
2007 fn write_test_object(db: &FileObjectDatabase, object: &EncodedObject) -> ObjectId {
2008 db.write_object(object.clone())
2009 .expect("test operation should succeed")
2010 }
2011
2012 fn test_tree(entries: &[(u32, &[u8], ObjectId)]) -> EncodedObject {
2013 EncodedObject::new(
2014 ObjectType::Tree,
2015 Tree {
2016 entries: entries
2017 .iter()
2018 .map(|(mode, name, oid)| TreeEntry {
2019 mode: *mode,
2020 name: BString::from(*name),
2021 oid: *oid,
2022 })
2023 .collect(),
2024 }
2025 .write(),
2026 )
2027 }
2028
2029 fn test_commit(tree: ObjectId, parents: &[ObjectId], message: &[u8]) -> EncodedObject {
2030 let identity = b"Example <example@example.invalid> 0 +0000".to_vec();
2031 EncodedObject::new(
2032 ObjectType::Commit,
2033 Commit {
2034 tree,
2035 parents: parents.to_vec(),
2036 author: identity.clone(),
2037 committer: identity,
2038 encoding: None,
2039 message: message.to_vec(),
2040 }
2041 .write(),
2042 )
2043 }
2044}