1use std::collections::{HashMap, HashSet, VecDeque};
13use std::fs;
14use std::path::Path;
15use std::time::{SystemTime, UNIX_EPOCH};
16
17use sley_config::GitConfig;
18use sley_core::{
19 Capability, GitError, ObjectFormat, ObjectId, Result, UPSTREAM_GIT_COMPAT_VERSION,
20};
21use sley_object::{Commit, ObjectType, Tag};
22use sley_odb::{
23 FileObjectDatabase, ObjectReader, RawPackInstallOptions, build_and_install_reachable_pack,
24 build_and_install_reachable_pack_filtered, build_reachable_pack, collect_reachable_object_ids,
25};
26use sley_protocol::{
27 PKT_LINE_MAX_PAYLOAD_LEN, ProtocolV2FetchAcknowledgment, ProtocolV2FetchFeatures,
28 ProtocolV2FetchRequest, ProtocolV2FetchResponseSection, ProtocolV2FetchShallowInfo,
29 ProtocolV2LsRefsFeatures, ProtocolV2LsRefsRecord, ProtocolV2LsRefsRef, ProtocolV2LsRefsRequest,
30 ProtocolVersion, ReceivePackCommand, ReceivePackFeatures, ReceivePackPushRequest,
31 ReceivePackReportStatus, ReceivePackRequest, RefAdvertisement, SideBandChannel, SideBandPacket,
32 TransportHandshake, UploadPackFeatures, UploadPackNegotiationRequest,
33 UploadPackPackfileResponse, UploadPackRawPackfileResponse, UploadPackRequest,
34 apply_receive_pack_push_request, build_upload_pack_raw_packfile_response,
35 classify_protocol_v2_command_request, encode_protocol_v2_fetch_capability,
36 encode_protocol_v2_ls_refs_capability, encode_receive_pack_features,
37 encode_upload_pack_features, read_protocol_v2_command_request,
38 read_upload_pack_negotiation_request, read_upload_pack_request,
39 write_protocol_v2_advertisement, write_protocol_v2_fetch_response,
40 write_protocol_v2_ls_refs_response, write_upload_pack_negotiation_request,
41 write_upload_pack_request,
42};
43use sley_refs::{
44 DeleteRef, FileRefStore, Ref, RefDeletePrecondition, RefPrecondition, RefTarget, ReflogEntry,
45};
46
47fn zero_oid(format: ObjectFormat) -> Result<ObjectId> {
50 Ok(ObjectId::null(format))
51}
52
53fn resolve_for_each_ref_target(
56 store: &FileRefStore,
57 reference: &Ref,
58) -> Result<Option<(ObjectId, Option<String>)>> {
59 let mut target = reference.target.clone();
60 let mut symref = None;
61 for _ in 0..5 {
62 match target {
63 RefTarget::Direct(oid) => return Ok(Some((oid, symref))),
64 RefTarget::Symbolic(name) => {
65 symref.get_or_insert_with(|| name.clone());
66 let Some(next) = store.read_ref(&name)? else {
67 return Ok(None);
68 };
69 target = next;
70 }
71 }
72 }
73 Ok(None)
74}
75
76pub fn upload_pack_features(git_dir: &Path, format: ObjectFormat) -> Result<UploadPackFeatures> {
79 let store = FileRefStore::new(git_dir, format);
80 let mut symrefs = Vec::new();
81 if let Some(RefTarget::Symbolic(target)) = store.read_ref("HEAD")? {
82 symrefs.push(format!("HEAD:{target}"));
83 }
84 Ok(UploadPackFeatures {
85 object_format: Some(format),
86 side_band_64k: true,
87 symrefs,
88 ..UploadPackFeatures::default()
89 })
90}
91
92pub fn upload_pack_request_uses_sideband(request: &UploadPackRequest) -> bool {
94 request
95 .capabilities
96 .iter()
97 .any(|capability| matches!(capability.name.as_str(), "side-band" | "side-band-64k"))
98}
99
100pub fn upload_pack_sideband_response(
103 response: UploadPackRawPackfileResponse,
104) -> UploadPackPackfileResponse {
105 let mut sideband = Vec::new();
106 let chunk_len = PKT_LINE_MAX_PAYLOAD_LEN - 1;
107 for chunk in response.packfile.chunks(chunk_len) {
108 sideband.push(SideBandPacket {
109 channel: SideBandChannel::Data,
110 data: chunk.to_vec(),
111 });
112 }
113 UploadPackPackfileResponse {
114 acknowledgments: response.acknowledgments,
115 sideband,
116 }
117}
118
119pub fn attach_upload_pack_capabilities(
122 advertisements: &mut Vec<RefAdvertisement>,
123 format: ObjectFormat,
124 features: &UploadPackFeatures,
125) -> Result<()> {
126 let capabilities = encode_upload_pack_features(features)?;
127 if let Some(first) = advertisements.first_mut() {
128 first.capabilities = capabilities;
129 } else {
130 advertisements.push(RefAdvertisement {
131 oid: zero_oid(format)?,
132 name: "capabilities^{}".into(),
133 capabilities,
134 });
135 }
136 Ok(())
137}
138
139pub fn upload_pack_from_local_repository(
143 git_dir: &Path,
144 format: ObjectFormat,
145 features: &UploadPackFeatures,
146 request: UploadPackRequest,
147 haves: HashSet<ObjectId>,
148) -> Result<UploadPackRawPackfileResponse> {
149 let db = FileObjectDatabase::from_git_dir(git_dir, format);
150 build_upload_pack_raw_packfile_response(
151 features,
152 request,
153 haves,
154 |oid| db.contains(oid),
155 |wants, known_haves| {
156 let excluded = collect_reachable_object_ids(&db, format, known_haves)?;
157 build_reachable_pack(&db, format, wants, &excluded)
158 .map(|pack| pack.map(|pack| pack.pack))
159 },
160 )
161}
162
163pub fn receive_pack_features(format: ObjectFormat) -> ReceivePackFeatures {
166 ReceivePackFeatures {
167 report_status: true,
168 delete_refs: true,
169 ofs_delta: true,
170 push_options: true,
171 quiet: true,
172 object_format: Some(format),
173 ..ReceivePackFeatures::default()
174 }
175}
176
177pub fn receive_pack_request_uses_push_options(request: &ReceivePackRequest) -> bool {
180 request
181 .capabilities
182 .iter()
183 .any(|capability| capability.name == "push-options")
184}
185
186pub fn attach_receive_pack_capabilities(
189 advertisements: &mut Vec<RefAdvertisement>,
190 format: ObjectFormat,
191 features: &ReceivePackFeatures,
192) -> Result<()> {
193 let capabilities = encode_receive_pack_features(features)?;
194 if let Some(first) = advertisements.first_mut() {
195 first.capabilities = capabilities;
196 } else {
197 advertisements.push(RefAdvertisement {
198 oid: zero_oid(format)?,
199 name: "capabilities^{}".into(),
200 capabilities,
201 });
202 }
203 Ok(())
204}
205
206pub fn receive_pack_into_local_repository(
210 remote_git_dir: &Path,
211 format: ObjectFormat,
212 request: &ReceivePackPushRequest,
213) -> Result<ReceivePackReportStatus> {
214 let remote_store = FileRefStore::new(remote_git_dir, format);
215 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
216 let deletes_applied_with_updates = std::cell::RefCell::new(HashSet::<String>::new());
217 apply_receive_pack_push_request(
218 &receive_pack_features(format),
219 request,
220 |name| match remote_store.read_ref(name)? {
221 Some(RefTarget::Direct(oid)) => Ok(Some(oid)),
222 Some(RefTarget::Symbolic(_)) | None => Ok(None),
223 },
224 |packfile| remote_db.install_raw_pack(packfile).map(|_| ()),
225 |oid| remote_db.contains(oid),
226 |commands| {
227 let applied = apply_receive_pack_ref_transaction(
228 remote_git_dir,
229 format,
230 &remote_store,
231 commands,
232 &request.commands.commands,
233 )?;
234 deletes_applied_with_updates.borrow_mut().extend(applied);
235 Ok(())
236 },
237 |command| {
238 if deletes_applied_with_updates
239 .borrow()
240 .contains(command.name.as_str())
241 {
242 return Ok(());
243 }
244 remote_store
245 .delete_ref_checked(DeleteRef {
246 name: command.name.clone(),
247 expected_old: (!command.old_id.is_null()).then_some(command.old_id),
248 reflog: None,
249 })
250 .map(|_| ())
251 .map_err(|err| GitError::Transaction(err.to_string()))
252 },
253 )
254}
255
256fn receive_pack_log_all_ref_updates(git_dir: &Path) -> bool {
257 let Ok(config) = fs::read_to_string(git_dir.join("config")) else {
258 return false;
259 };
260 let mut in_core = false;
261 for raw_line in config.lines() {
262 let line = raw_line.trim();
263 if line.starts_with('[') && line.ends_with(']') {
264 in_core = line.eq_ignore_ascii_case("[core]");
265 continue;
266 }
267 if !in_core || line.starts_with('#') || line.starts_with(';') {
268 continue;
269 }
270 let Some((name, value)) = line.split_once('=') else {
271 continue;
272 };
273 if name.trim().eq_ignore_ascii_case("logallrefupdates") {
274 return matches!(
275 value.trim().trim_matches('"').to_ascii_lowercase().as_str(),
276 "true" | "yes" | "on" | "1" | "always"
277 );
278 }
279 }
280 false
281}
282
283fn receive_pack_should_write_reflog(refname: &str) -> bool {
284 refname == "HEAD"
285 || refname.starts_with("refs/heads/")
286 || refname.starts_with("refs/remotes/")
287 || refname.starts_with("refs/notes/")
288}
289
290fn receive_pack_reflog_entry(
291 format: ObjectFormat,
292 old_oid: ObjectId,
293 new_oid: ObjectId,
294) -> ReflogEntry {
295 let old_oid = if old_oid.is_null() {
296 ObjectId::null(format)
297 } else {
298 old_oid
299 };
300 ReflogEntry {
301 old_oid,
302 new_oid,
303 committer: receive_pack_reflog_committer(),
304 message: b"push".to_vec(),
305 }
306}
307
308fn receive_pack_reflog_committer() -> Vec<u8> {
309 let seconds = SystemTime::now()
310 .duration_since(UNIX_EPOCH)
311 .map(|duration| duration.as_secs())
312 .unwrap_or(0);
313 format!("Git Rs <sley@example.invalid> {seconds} +0000").into_bytes()
314}
315
316pub fn receive_pack_reachable_pack_into_local_repository(
323 remote_git_dir: &Path,
324 format: ObjectFormat,
325 request: &ReceivePackPushRequest,
326 source_db: &FileObjectDatabase,
327 starts: Vec<ObjectId>,
328 excluded: HashSet<ObjectId>,
329) -> Result<ReceivePackReportStatus> {
330 let remote_store = FileRefStore::new(remote_git_dir, format);
331 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
332 let mut starts = Some(starts);
333 let deletes_applied_with_updates = std::cell::RefCell::new(HashSet::<String>::new());
334 apply_receive_pack_push_request(
335 &receive_pack_features(format),
336 request,
337 |name| match remote_store.read_ref(name)? {
338 Some(RefTarget::Direct(oid)) => Ok(Some(oid)),
339 Some(RefTarget::Symbolic(_)) | None => Ok(None),
340 },
341 |_| {
342 let starts = starts.take().ok_or_else(|| {
343 GitError::InvalidFormat("receive-pack attempted to install pack twice".into())
344 })?;
345 build_and_install_reachable_pack(
346 source_db,
347 &remote_db,
348 format,
349 starts,
350 &excluded,
351 RawPackInstallOptions { promisor: false },
352 )?;
353 Ok(())
354 },
355 |oid| remote_db.contains(oid),
356 |commands| {
357 let applied = apply_receive_pack_ref_transaction(
358 remote_git_dir,
359 format,
360 &remote_store,
361 commands,
362 &request.commands.commands,
363 )?;
364 deletes_applied_with_updates.borrow_mut().extend(applied);
365 Ok(())
366 },
367 |command| {
368 if deletes_applied_with_updates
369 .borrow()
370 .contains(command.name.as_str())
371 {
372 return Ok(());
373 }
374 remote_store
375 .delete_ref_checked(DeleteRef {
376 name: command.name.clone(),
377 expected_old: (!command.old_id.is_null()).then_some(command.old_id),
378 reflog: None,
379 })
380 .map(|_| ())
381 .map_err(|err| GitError::Transaction(err.to_string()))
382 },
383 )
384}
385
386fn apply_receive_pack_ref_transaction(
387 remote_git_dir: &Path,
388 format: ObjectFormat,
389 store: &FileRefStore,
390 updates: &[ReceivePackCommand],
391 all_commands: &[ReceivePackCommand],
392) -> Result<HashSet<String>> {
393 let updates = canonical_receive_pack_update_commands(store, updates)?;
394 let deletes = all_commands
395 .iter()
396 .filter(|command| command.new_id.is_null())
397 .collect::<Vec<_>>();
398 let mut tx = store.transaction();
399 for command in &deletes {
400 tx.delete_with_precondition(
401 command.name.clone(),
402 RefDeletePrecondition::Direct((!command.old_id.is_null()).then_some(command.old_id)),
403 None,
404 );
405 }
406 let log_updates = receive_pack_log_all_ref_updates(remote_git_dir);
407 for command in &updates {
408 let precondition = if command.old_id.is_null() {
409 RefPrecondition::MustNotExist
410 } else {
411 RefPrecondition::MustExistAndMatch(RefTarget::Direct(command.old_id))
412 };
413 let reflog = if log_updates && receive_pack_should_write_reflog(&command.name) {
414 Some(receive_pack_reflog_entry(
415 format,
416 command.old_id,
417 command.new_id,
418 ))
419 } else {
420 None
421 };
422 tx.update_to(
423 command.name.clone(),
424 RefTarget::Direct(command.new_id),
425 precondition,
426 reflog,
427 );
428 }
429 tx.commit()?;
430 Ok(deletes
431 .into_iter()
432 .map(|command| command.name.clone())
433 .collect())
434}
435
436fn canonical_receive_pack_update_commands(
437 store: &FileRefStore,
438 commands: &[ReceivePackCommand],
439) -> Result<Vec<ReceivePackCommand>> {
440 let mut by_actual = HashMap::<String, ObjectId>::new();
441 let mut canonical = Vec::with_capacity(commands.len());
442 for command in commands {
443 let name = match store.read_ref(&command.name)? {
444 Some(RefTarget::Symbolic(target)) => target,
445 Some(RefTarget::Direct(_)) | None => command.name.clone(),
446 };
447 if let Some(existing) = by_actual.get(&name) {
448 if existing != &command.new_id {
449 return Err(GitError::Command("refusing inconsistent update".into()));
450 }
451 } else {
452 by_actual.insert(name.clone(), command.new_id);
453 }
454 canonical.push(ReceivePackCommand {
455 old_id: command.old_id,
456 new_id: command.new_id,
457 name,
458 });
459 }
460 Ok(canonical)
461}
462
463pub fn local_fetch_advertisements(
466 git_dir: &Path,
467 format: ObjectFormat,
468) -> Result<Vec<RefAdvertisement>> {
469 let store = FileRefStore::new(git_dir, format);
470 let mut advertisements = Vec::new();
471 if let Some(target) = store.read_ref("HEAD")? {
472 let reference = Ref {
473 name: "HEAD".to_string(),
474 target,
475 };
476 if let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? {
477 advertisements.push(RefAdvertisement {
478 oid,
479 name: reference.name,
480 capabilities: Vec::new(),
481 });
482 }
483 }
484 for reference in store.list_refs()? {
485 let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? else {
486 continue;
487 };
488 advertisements.push(RefAdvertisement {
489 oid,
490 name: reference.name,
491 capabilities: Vec::new(),
492 });
493 }
494 Ok(advertisements)
495}
496
497pub fn local_have_oids(git_dir: &Path, format: ObjectFormat) -> Result<Vec<ObjectId>> {
501 let mut seen = HashSet::new();
502 let mut haves = Vec::new();
503 for advertisement in local_fetch_advertisements(git_dir, format)? {
504 if seen.insert(advertisement.oid) {
505 haves.push(advertisement.oid);
506 }
507 }
508 let db = FileObjectDatabase::from_git_dir(git_dir, format);
509 for oid in db.object_ids()? {
510 if seen.insert(oid) {
511 haves.push(oid);
512 }
513 }
514 Ok(haves)
515}
516
517#[derive(Debug, Clone)]
524pub struct LocalDeepenPlan {
525 pub depth: u32,
529 pub deepen_since: bool,
531 pub deepen_not: usize,
533 pub client_shallow: Vec<ObjectId>,
536 pub shallow_info: Vec<ProtocolV2FetchShallowInfo>,
539 pub excluded: HashSet<ObjectId>,
544 pub extra_wants: Vec<ObjectId>,
548}
549
550fn peel_to_commit<R: ObjectReader>(
554 remote_db: &R,
555 format: ObjectFormat,
556 oid: &ObjectId,
557) -> Result<Option<ObjectId>> {
558 let mut oid = *oid;
559 loop {
560 let object = remote_db.read_object(&oid)?;
561 match object.object_type {
562 ObjectType::Commit => return Ok(Some(oid)),
563 ObjectType::Tag => oid = Tag::parse_ref(format, &object.body)?.object,
564 _ => return Ok(None),
565 }
566 }
567}
568
569pub fn compute_local_deepen<R: ObjectReader>(
582 remote_db: &R,
583 format: ObjectFormat,
584 heads: &[ObjectId],
585 client_shallow: Vec<ObjectId>,
586 depth: u32,
587 deepen_relative: bool,
588) -> Result<LocalDeepenPlan> {
589 let depth = if deepen_relative && depth < INFINITE_DEPTH {
592 depth.saturating_add(client_shallow_min_depth(
593 remote_db,
594 format,
595 heads,
596 &client_shallow,
597 )?)
598 } else {
599 depth
600 };
601 let mut min_depth: HashMap<ObjectId, u32> = HashMap::new();
602 let mut queue: VecDeque<ObjectId> = VecDeque::new();
603 for head in heads {
604 let Some(commit) = peel_to_commit(remote_db, format, head)? else {
605 continue;
606 };
607 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(commit) {
608 entry.insert(0);
609 queue.push_back(commit);
610 }
611 }
612 let mut boundary = Vec::new();
617 let mut boundary_parents = HashSet::new();
618 while let Some(oid) = queue.pop_front() {
619 let commit_depth = min_depth[&oid];
620 let object = remote_db.read_object(&oid)?;
621 let parents = sley_odb::grafted_parents(
622 remote_db,
623 &oid,
624 Commit::parse_ref(format, &object.body)?.parents,
625 );
626 if (depth != INFINITE_DEPTH && commit_depth + 1 >= depth)
630 || remote_db.is_shallow_graft(&oid)
631 {
632 boundary.push(oid);
633 boundary_parents.extend(parents);
634 continue;
635 }
636 for parent in parents {
637 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(parent) {
638 entry.insert(commit_depth + 1);
639 queue.push_back(parent);
640 }
641 }
642 }
643 let excluded = boundary_parents
647 .into_iter()
648 .filter(|parent| !min_depth.contains_key(parent))
649 .collect::<HashSet<_>>();
650
651 let client: HashSet<ObjectId> = client_shallow.iter().copied().collect();
652 let boundary_set: HashSet<ObjectId> = boundary.iter().copied().collect();
653 let mut shallow_info = Vec::new();
654 for oid in &boundary {
655 if !client.contains(oid) {
656 shallow_info.push(ProtocolV2FetchShallowInfo::Shallow(*oid));
657 }
658 }
659 let mut extra_wants = Vec::new();
660 for oid in &client_shallow {
661 let unshallowed = min_depth.contains_key(oid) && !boundary_set.contains(oid);
664 if !unshallowed {
665 continue;
666 }
667 shallow_info.push(ProtocolV2FetchShallowInfo::Unshallow(*oid));
668 let object = remote_db.read_object(oid)?;
669 extra_wants.extend(sley_odb::grafted_parents(
670 remote_db,
671 oid,
672 Commit::parse_ref(format, &object.body)?.parents,
673 ));
674 }
675 Ok(LocalDeepenPlan {
676 depth,
677 deepen_since: false,
678 deepen_not: 0,
679 client_shallow,
680 shallow_info,
681 excluded,
682 extra_wants,
683 })
684}
685
686pub const INFINITE_DEPTH: u32 = 0x7fff_ffff;
689
690fn client_shallow_min_depth<R: ObjectReader>(
694 remote_db: &R,
695 format: ObjectFormat,
696 heads: &[ObjectId],
697 client_shallow: &[ObjectId],
698) -> Result<u32> {
699 if client_shallow.is_empty() {
700 return Ok(0);
701 }
702 let client: HashSet<ObjectId> = client_shallow.iter().copied().collect();
703 let mut min_depth: HashMap<ObjectId, u32> = HashMap::new();
704 let mut queue: VecDeque<ObjectId> = VecDeque::new();
705 for head in heads {
706 let Some(commit) = peel_to_commit(remote_db, format, head)? else {
707 continue;
708 };
709 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(commit) {
710 entry.insert(1);
711 queue.push_back(commit);
712 }
713 }
714 let mut best: u32 = 0;
715 while let Some(oid) = queue.pop_front() {
716 let commit_depth = min_depth[&oid];
717 if client.contains(&oid) && (best == 0 || commit_depth < best) {
718 best = commit_depth;
719 }
720 let object = remote_db.read_object(&oid)?;
721 let parents = sley_odb::grafted_parents(
722 remote_db,
723 &oid,
724 Commit::parse_ref(format, &object.body)?.parents,
725 );
726 for parent in parents {
727 if let std::collections::hash_map::Entry::Vacant(entry) = min_depth.entry(parent) {
728 entry.insert(commit_depth + 1);
729 queue.push_back(parent);
730 }
731 }
732 }
733 Ok(best)
734}
735
736pub fn compute_local_deepen_by_rev_list<R: ObjectReader>(
742 remote_db: &R,
743 format: ObjectFormat,
744 heads: &[ObjectId],
745 client_shallow: Vec<ObjectId>,
746 since: Option<i64>,
747 deepen_not: &[ObjectId],
748) -> Result<LocalDeepenPlan> {
749 let mut excluded_not: HashSet<ObjectId> = HashSet::new();
751 let mut queue: VecDeque<ObjectId> = VecDeque::new();
752 for tip in deepen_not {
753 if let Some(commit) = peel_to_commit(remote_db, format, tip)?
754 && excluded_not.insert(commit)
755 {
756 queue.push_back(commit);
757 }
758 }
759 while let Some(oid) = queue.pop_front() {
760 let object = remote_db.read_object(&oid)?;
761 for parent in sley_odb::grafted_parents(
762 remote_db,
763 &oid,
764 Commit::parse_ref(format, &object.body)?.parents,
765 ) {
766 if excluded_not.insert(parent) {
767 queue.push_back(parent);
768 }
769 }
770 }
771
772 let commit_time = |oid: &ObjectId| -> Result<i64> {
773 let object = remote_db.read_object(oid)?;
774 Ok(Commit::parse_ref(format, &object.body)?
775 .committer_signature()
776 .map(|signature| signature.time.seconds)
777 .unwrap_or(0))
778 };
779 let keeps = |oid: &ObjectId| -> Result<bool> {
780 if excluded_not.contains(oid) {
781 return Ok(false);
782 }
783 match since {
784 Some(since) => Ok(commit_time(oid)? >= since),
785 None => Ok(true),
786 }
787 };
788
789 let mut kept: HashSet<ObjectId> = HashSet::new();
792 let mut kept_order: Vec<ObjectId> = Vec::new();
793 let mut queue: VecDeque<ObjectId> = VecDeque::new();
794 for head in heads {
795 let Some(commit) = peel_to_commit(remote_db, format, head)? else {
796 continue;
797 };
798 if keeps(&commit)? && kept.insert(commit) {
799 kept_order.push(commit);
800 queue.push_back(commit);
801 }
802 }
803 while let Some(oid) = queue.pop_front() {
804 let object = remote_db.read_object(&oid)?;
805 for parent in sley_odb::grafted_parents(
806 remote_db,
807 &oid,
808 Commit::parse_ref(format, &object.body)?.parents,
809 ) {
810 if !kept.contains(&parent) && keeps(&parent)? {
811 kept.insert(parent);
812 kept_order.push(parent);
813 queue.push_back(parent);
814 }
815 }
816 }
817 if kept.is_empty() {
818 return Err(GitError::Command(
820 "no commits selected for shallow requests".into(),
821 ));
822 }
823
824 let mut boundary = Vec::new();
826 let mut boundary_set: HashSet<ObjectId> = HashSet::new();
827 let mut excluded: HashSet<ObjectId> = HashSet::new();
828 for oid in &kept_order {
829 let object = remote_db.read_object(oid)?;
830 let parents = sley_odb::grafted_parents(
831 remote_db,
832 oid,
833 Commit::parse_ref(format, &object.body)?.parents,
834 );
835 let mut is_boundary = false;
836 for parent in parents {
837 if !kept.contains(&parent) {
838 is_boundary = true;
839 excluded.insert(parent);
840 }
841 }
842 if is_boundary && boundary_set.insert(*oid) {
843 boundary.push(*oid);
844 }
845 }
846
847 let client: HashSet<ObjectId> = client_shallow.iter().copied().collect();
848 let mut shallow_info = Vec::new();
849 for oid in &boundary {
850 if !client.contains(oid) {
851 shallow_info.push(ProtocolV2FetchShallowInfo::Shallow(*oid));
852 }
853 }
854 let mut extra_wants = Vec::new();
855 for oid in &client_shallow {
856 let unshallowed = kept.contains(oid) && !boundary_set.contains(oid);
857 if !unshallowed {
858 continue;
859 }
860 shallow_info.push(ProtocolV2FetchShallowInfo::Unshallow(*oid));
861 let object = remote_db.read_object(oid)?;
862 extra_wants.extend(sley_odb::grafted_parents(
863 remote_db,
864 oid,
865 Commit::parse_ref(format, &object.body)?.parents,
866 ));
867 }
868 Ok(LocalDeepenPlan {
869 depth: 0,
870 deepen_since: since.is_some(),
871 deepen_not: deepen_not.len(),
872 client_shallow,
873 shallow_info,
874 excluded,
875 extra_wants,
876 })
877}
878
879#[allow(clippy::too_many_arguments)]
892pub fn install_fetch_pack_via_local_upload_pack(
893 git_dir: &Path,
894 remote_git_dir: &Path,
895 format: ObjectFormat,
896 wants: Vec<ObjectId>,
897 deepen: Option<&LocalDeepenPlan>,
898 promisor: bool,
899 record_promisor_refs: bool,
900 filter: Option<sley_odb::PackObjectFilter>,
901 refetch: bool,
902 unpack_limit: Option<usize>,
903) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
904 if wants.is_empty() {
905 return Ok(Vec::new());
906 }
907 let local_db = FileObjectDatabase::from_git_dir(git_dir, format);
908 let all_wants_present = wants
909 .iter()
910 .map(|want| local_db.contains(want))
911 .collect::<Result<Vec<_>>>()?
912 .into_iter()
913 .all(|contains| contains);
914 let deepen_noop = match deepen {
915 Some(plan) => plan.shallow_info.is_empty() && plan.extra_wants.is_empty(),
916 None => true,
917 };
918 if all_wants_present && deepen_noop && !refetch {
919 sley_protocol::trace_packet_write_payload(b"0000");
920 return Ok(Vec::new());
921 }
922
923 let request = UploadPackRequest {
924 wants,
925 capabilities: deepen
928 .map(|_| {
929 vec![Capability {
930 name: "shallow".into(),
931 value: None,
932 }]
933 })
934 .unwrap_or_default(),
935 shallow: deepen
936 .map(|plan| plan.client_shallow.clone())
937 .unwrap_or_default(),
938 deepen: deepen.and_then(|plan| (plan.depth > 0).then_some(plan.depth)),
939 ..UploadPackRequest::default()
940 };
941 let mut encoded_request = Vec::new();
942 write_upload_pack_request(&mut encoded_request, Some(&request))?;
943 let decoded_request = read_upload_pack_request(format, &mut encoded_request.as_slice())?
944 .ok_or_else(|| GitError::InvalidFormat("encoded upload-pack request was empty".into()))?;
945
946 let haves = if refetch {
947 Vec::new()
948 } else {
949 local_have_oids(git_dir, format)?
950 };
951 let negotiation = UploadPackNegotiationRequest { haves, done: true };
952 let mut encoded_negotiation = Vec::new();
953 write_upload_pack_negotiation_request(&mut encoded_negotiation, &negotiation)?;
954 let decoded_negotiation =
955 read_upload_pack_negotiation_request(format, &mut encoded_negotiation.as_slice())?;
956 sley_core::trace2::data("negotiation_v2", "total_rounds", 1);
957
958 let remote_db = FileObjectDatabase::from_git_dir(remote_git_dir, format);
959 for want in &decoded_request.wants {
960 if !remote_db.contains(want)? {
961 return Err(GitError::InvalidObject(format!(
962 "upload-pack requested missing object {want}"
963 )));
964 }
965 }
966 let known_haves = decoded_negotiation
967 .haves
968 .into_iter()
969 .filter_map(|oid| match remote_db.contains(&oid) {
970 Ok(true) => Some(Ok(oid)),
971 Ok(false) => None,
972 Err(err) => Some(Err(err)),
973 })
974 .collect::<Result<Vec<_>>>()?;
975 trace2_fetch_info(
979 known_haves.len(),
980 decoded_request.wants.len(),
981 deepen.map(|plan| plan.depth).unwrap_or(0),
982 deepen.map(|plan| plan.client_shallow.len()).unwrap_or(0),
983 deepen.is_some_and(|plan| plan.deepen_since),
984 deepen.map(|plan| plan.deepen_not).unwrap_or(0),
985 filter.as_ref(),
986 );
987 let mut excluded = match deepen {
992 Some(plan) => {
993 let cut: HashSet<ObjectId> = plan.client_shallow.iter().copied().collect();
994 sley_odb::collect_reachable_object_ids_with_cut(&remote_db, format, known_haves, &cut)?
995 }
996 None => collect_reachable_object_ids(&remote_db, format, known_haves)?,
997 };
998 let mut starts = decoded_request.wants;
999 let promisor_ref_wants = starts.iter().copied().collect::<HashSet<_>>();
1000 for want in &starts {
1001 excluded.remove(want);
1002 }
1003 if let Some(plan) = deepen {
1004 excluded.extend(plan.excluded.iter().copied());
1007 starts.extend(plan.extra_wants.iter().copied());
1008 }
1009 let install = build_and_install_reachable_pack_filtered(
1010 &remote_db,
1011 &local_db,
1012 format,
1013 starts,
1014 &excluded,
1015 RawPackInstallOptions { promisor },
1016 filter.clone(),
1017 unpack_limit,
1018 )?;
1019 if promisor
1020 && record_promisor_refs
1021 && let Some(result) = install
1022 && let Some(promisor_path) = result.promisor_path
1023 {
1024 append_promisor_ref_lines(&promisor_path, remote_git_dir, format, &promisor_ref_wants)?;
1025 }
1026 Ok(deepen
1027 .map(|plan| plan.shallow_info.clone())
1028 .unwrap_or_default())
1029}
1030
1031fn append_promisor_ref_lines(
1032 promisor_path: &Path,
1033 remote_git_dir: &Path,
1034 format: ObjectFormat,
1035 wanted: &HashSet<ObjectId>,
1036) -> Result<()> {
1037 if wanted.is_empty() {
1038 return Ok(());
1039 }
1040 let store = FileRefStore::new(remote_git_dir, format);
1041 let mut lines = Vec::new();
1042 if let Some(head_target) = store.read_ref("HEAD")? {
1043 let head = Ref {
1044 name: "HEAD".into(),
1045 target: head_target,
1046 };
1047 if let Some((oid, _)) = resolve_for_each_ref_target(&store, &head)?
1048 && wanted.contains(&oid)
1049 {
1050 lines.push(format!("{oid} HEAD\n"));
1051 }
1052 }
1053 for reference in store.list_refs()? {
1054 let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? else {
1055 continue;
1056 };
1057 if wanted.contains(&oid) {
1058 lines.push(format!("{oid} {}\n", reference.name));
1059 }
1060 }
1061 if lines.is_empty() {
1062 return Ok(());
1063 }
1064 lines.sort();
1065 let mut file = fs::OpenOptions::new().append(true).open(promisor_path)?;
1066 use std::io::Write as _;
1067 for line in lines {
1068 file.write_all(line.as_bytes())?;
1069 }
1070 Ok(())
1071}
1072
1073fn trace2_fetch_info(
1077 haves: usize,
1078 wants: usize,
1079 depth: u32,
1080 shallows: usize,
1081 deepen_since: bool,
1082 deepen_not: usize,
1083 filter: Option<&sley_odb::PackObjectFilter>,
1084) {
1085 let Some(path) = std::env::var_os("GIT_TRACE2_EVENT") else {
1086 return;
1087 };
1088 if path.is_empty() {
1089 return;
1090 }
1091 let filter_json = match filter {
1092 Some(sley_odb::PackObjectFilter::BlobNone) => "\"blob:none\"".to_string(),
1093 Some(sley_odb::PackObjectFilter::BlobLimit(limit)) => {
1094 format!("\"blob:limit={limit}\"")
1095 }
1096 Some(sley_odb::PackObjectFilter::TreeDepth(depth)) => {
1097 format!("\"tree:{depth}\"")
1098 }
1099 Some(sley_odb::PackObjectFilter::SparsePathSet(_)) => "\"sparse:oid\"".to_string(),
1100 None => "null".to_string(),
1101 };
1102 let line = format!(
1103 "{{\"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"
1104 );
1105 if let Ok(mut file) = std::fs::OpenOptions::new()
1106 .create(true)
1107 .append(true)
1108 .open(&path)
1109 {
1110 use std::io::Write as _;
1111 let _ = file.write_all(line.as_bytes());
1112 }
1113}
1114
1115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1126enum LsRefsUnbornConfig {
1127 Ignore,
1128 Allow,
1129 Advertise,
1130}
1131
1132fn lsrefs_unborn_config(config: &GitConfig) -> LsRefsUnbornConfig {
1133 match config.get("lsrefs", None, "unborn") {
1134 Some("ignore") => LsRefsUnbornConfig::Ignore,
1135 Some("allow") => LsRefsUnbornConfig::Allow,
1136 Some("advertise") | None => LsRefsUnbornConfig::Advertise,
1137 Some(_) => LsRefsUnbornConfig::Advertise,
1138 }
1139}
1140
1141fn upload_pack_blob_packfile_uri_configured(config: &GitConfig) -> bool {
1142 config
1143 .get_all("uploadpack", None, "blobpackfileuri")
1144 .into_iter()
1145 .any(|value| value.is_some_and(|value| !value.is_empty()))
1146}
1147
1148fn upload_pack_v2_capabilities(
1152 format: ObjectFormat,
1153 config: &GitConfig,
1154) -> Result<Vec<Capability>> {
1155 let mut capabilities = vec![
1156 Capability {
1157 name: "agent".into(),
1158 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
1159 },
1160 encode_protocol_v2_ls_refs_capability(&ProtocolV2LsRefsFeatures {
1161 unborn: lsrefs_unborn_config(config) == LsRefsUnbornConfig::Advertise,
1162 unknown: Vec::new(),
1163 })?,
1164 encode_protocol_v2_fetch_capability(&ProtocolV2FetchFeatures {
1165 shallow: true,
1166 wait_for_done: true,
1167 filter: config
1168 .get_bool("uploadpack", None, "allowfilter")
1169 .unwrap_or(false),
1170 packfile_uris: upload_pack_blob_packfile_uri_configured(config),
1171 ..ProtocolV2FetchFeatures::default()
1172 })?,
1173 Capability {
1174 name: "server-option".into(),
1175 value: None,
1176 },
1177 Capability {
1178 name: "object-format".into(),
1179 value: Some(format.name().into()),
1180 },
1181 ];
1182 if config
1183 .get_bool("transfer", None, "advertisesid")
1184 .unwrap_or(false)
1185 {
1186 capabilities.push(Capability {
1187 name: "session-id".into(),
1188 value: Some("sley".into()),
1189 });
1190 }
1191 Ok(capabilities)
1192}
1193
1194fn head_symref_target(store: &FileRefStore) -> Result<Option<String>> {
1198 match store.read_ref("HEAD")? {
1199 Some(RefTarget::Symbolic(name)) => Ok(Some(name)),
1200 _ => Ok(None),
1201 }
1202}
1203
1204fn local_ls_refs_v2_records(
1208 git_dir: &Path,
1209 format: ObjectFormat,
1210 request: &ProtocolV2LsRefsRequest,
1211 config: &GitConfig,
1212) -> Result<Vec<ProtocolV2LsRefsRecord>> {
1213 let store = FileRefStore::new(git_dir, format);
1214 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1215 let head_symref = head_symref_target(&store)?;
1216
1217 let mut entries: Vec<(String, ObjectId, Option<String>)> = Vec::new();
1220 if let Some(target) = store.read_ref("HEAD")? {
1221 let reference = Ref {
1222 name: "HEAD".to_string(),
1223 target,
1224 };
1225 if let Some((oid, _)) = resolve_for_each_ref_target(&store, &reference)? {
1226 entries.push(("HEAD".to_string(), oid, head_symref.clone()));
1227 } else if request.unborn && lsrefs_unborn_config(config) != LsRefsUnbornConfig::Ignore {
1228 entries.push((
1231 "HEAD".to_string(),
1232 ObjectId::null(format),
1233 head_symref.clone(),
1234 ));
1235 }
1236 }
1237 for reference in store.list_refs()? {
1238 let name = reference.name.clone();
1239 let Some((oid, symref)) = resolve_for_each_ref_target(&store, &reference)? else {
1240 continue;
1241 };
1242 entries.push((name, oid, symref));
1243 }
1244
1245 let matches_prefix = |name: &str| -> bool {
1246 if request.ref_prefixes.is_empty() {
1247 return true;
1248 }
1249 request
1250 .ref_prefixes
1251 .iter()
1252 .any(|prefix| name.starts_with(prefix.as_str()))
1253 };
1254
1255 let mut records = Vec::new();
1256 for (name, oid, symref) in entries {
1257 if !matches_prefix(&name) {
1258 continue;
1259 }
1260 if name == "HEAD" && oid == ObjectId::null(format) {
1262 records.push(ProtocolV2LsRefsRecord::Unborn {
1263 name,
1264 symref_target: if request.symrefs { symref } else { None },
1265 attributes: Vec::new(),
1266 });
1267 continue;
1268 }
1269 let peeled = if request.peel {
1270 let object = db.read_object(&oid)?;
1271 if object.object_type == ObjectType::Tag {
1272 Some(sley_rev::peel_tags(&db, format, &oid)?)
1273 } else {
1274 None
1275 }
1276 } else {
1277 None
1278 };
1279 let symref_target = if request.symrefs { symref } else { None };
1280 records.push(ProtocolV2LsRefsRecord::Ref(ProtocolV2LsRefsRef {
1281 oid,
1282 name,
1283 peeled,
1284 symref_target,
1285 attributes: Vec::new(),
1286 }));
1287 }
1288 Ok(records)
1289}
1290
1291fn packfile_section_lines(pack: &[u8]) -> Vec<Vec<u8>> {
1297 let chunk = PKT_LINE_MAX_PAYLOAD_LEN - 1;
1298 let mut lines = Vec::new();
1299 for slice in pack.chunks(chunk) {
1300 let mut payload = Vec::with_capacity(slice.len() + 1);
1301 payload.push(1u8); payload.extend_from_slice(slice);
1303 lines.push(payload);
1304 }
1305 lines
1306}
1307
1308fn local_fetch_v2_sections(
1314 git_dir: &Path,
1315 format: ObjectFormat,
1316 request: &ProtocolV2FetchRequest,
1317) -> Result<Vec<ProtocolV2FetchResponseSection>> {
1318 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1319
1320 let mut sections = Vec::new();
1321
1322 if !request.done {
1328 let mut acks: Vec<ProtocolV2FetchAcknowledgment> = Vec::new();
1329 for have in &request.haves {
1330 if db.contains(have)? {
1331 acks.push(ProtocolV2FetchAcknowledgment::Ack(*have));
1332 }
1333 }
1334 if acks.is_empty() {
1335 acks.push(ProtocolV2FetchAcknowledgment::Nak);
1336 }
1337 sections.push(ProtocolV2FetchResponseSection::Acknowledgments(acks));
1338 if !request.wait_for_done {
1341 return Ok(sections);
1342 }
1343 }
1344
1345 if !request.want_refs.is_empty() {
1347 let store = FileRefStore::new(git_dir, format);
1348 let mut wanted = Vec::new();
1349 for name in &request.want_refs {
1350 let reference = Ref {
1351 name: name.clone(),
1352 target: store
1353 .read_ref(name)?
1354 .ok_or_else(|| GitError::not_found(format!("want-ref {name}")))?,
1355 };
1356 let (oid, _) = resolve_for_each_ref_target(&store, &reference)?
1357 .ok_or_else(|| GitError::not_found(format!("want-ref {name}")))?;
1358 wanted.push(sley_protocol::ProtocolV2FetchWantedRef {
1359 oid,
1360 name: name.clone(),
1361 });
1362 }
1363 sections.push(ProtocolV2FetchResponseSection::WantedRefs(wanted));
1364 }
1365
1366 let mut wants: Vec<ObjectId> = request.wants.clone();
1368 if !request.want_refs.is_empty()
1369 && let Some(ProtocolV2FetchResponseSection::WantedRefs(wanted)) = sections
1370 .iter()
1371 .find(|s| matches!(s, ProtocolV2FetchResponseSection::WantedRefs(_)))
1372 {
1373 for w in wanted {
1374 wants.push(w.oid);
1375 }
1376 }
1377
1378 let mut known_haves: Vec<ObjectId> = Vec::new();
1380 for have in &request.haves {
1381 if db.contains(have)? {
1382 known_haves.push(*have);
1383 }
1384 }
1385 let excluded = collect_reachable_object_ids(&db, format, known_haves)?;
1386 let pack = build_reachable_pack(&db, format, wants, &excluded)?
1387 .map(|pack| pack.pack)
1388 .unwrap_or_default();
1389
1390 sections.push(ProtocolV2FetchResponseSection::Packfile(
1391 packfile_section_lines(&pack),
1392 ));
1393 Ok(sections)
1394}
1395
1396pub fn serve_upload_pack_v2(
1402 git_dir: &Path,
1403 format: ObjectFormat,
1404 reader: &mut impl std::io::Read,
1405 writer: &mut impl std::io::Write,
1406) -> Result<()> {
1407 let config = sley_config::read_repo_config(git_dir, None).unwrap_or_default();
1408 serve_upload_pack_v2_with_config(git_dir, format, &config, reader, writer)
1409}
1410
1411pub fn serve_upload_pack_v2_with_config(
1412 git_dir: &Path,
1413 format: ObjectFormat,
1414 config: &GitConfig,
1415 reader: &mut impl std::io::Read,
1416 writer: &mut impl std::io::Write,
1417) -> Result<()> {
1418 let handshake = TransportHandshake {
1419 protocol: ProtocolVersion::V2,
1420 capabilities: upload_pack_v2_capabilities(format, config)?,
1421 };
1422 write_protocol_v2_advertisement(writer, &handshake)?;
1423 writer.flush()?;
1424
1425 loop {
1430 let request = match read_protocol_v2_command_request(reader) {
1431 Ok(request) => request,
1432 Err(GitError::InvalidFormat(message))
1433 if message == "pkt-line stream ended before control packet"
1434 || message == "protocol v2 command request must start with a command line" =>
1435 {
1436 break;
1437 }
1438 Err(err) => return Err(err),
1439 };
1440 match classify_protocol_v2_command_request(&handshake, format, &request)? {
1441 sley_protocol::ProtocolV2Command::LsRefs(ls_refs) => {
1442 let records = local_ls_refs_v2_records(git_dir, format, &ls_refs, config)?;
1443 write_protocol_v2_ls_refs_response(writer, &records)?;
1444 writer.flush()?;
1445 }
1446 sley_protocol::ProtocolV2Command::Fetch(fetch) => {
1447 let sections = local_fetch_v2_sections(git_dir, format, &fetch)?;
1448 write_protocol_v2_fetch_response(writer, §ions)?;
1449 writer.flush()?;
1450 }
1451 sley_protocol::ProtocolV2Command::ObjectInfo(_)
1452 | sley_protocol::ProtocolV2Command::Unknown(_) => {
1453 return Err(GitError::InvalidFormat(format!(
1454 "unsupported protocol v2 command {}",
1455 request.command
1456 )));
1457 }
1458 }
1459 }
1460 Ok(())
1461}