1use crate::environment::RepositoryOptions;
19use crate::partial_clone::ObjectFilterSpec;
20
21use std::collections::{HashMap, HashSet, VecDeque};
22use std::fs;
23use std::io::Write;
24use std::path::{Path, PathBuf};
25
26use crate::error::{Error, RemoteUnreadableReason, Result};
27use crate::fetch::{NoProgress, Progress};
28use crate::objects::{parse_commit, parse_tag, parse_tree, HashAlgo, ObjectId, ObjectKind};
29use crate::odb::Odb;
30use crate::progress::{ProgressPhase, ProgressSink, ReceivedObjects};
31use crate::push_report::{PushRefResult, PushRefStatus};
32use crate::refspec::{parse_fetch_refspec, RefspecItem};
33use std::num::NonZeroU32;
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
41#[cfg_attr(feature = "serde-models", serde(rename_all = "snake_case"))]
42pub enum UpdateMode {
43 New,
45 FastForward,
47 Forced,
49 UpToDate,
51 NoChangeNeeded,
53 NonFastForwardRejected,
55 TagUpdateRejected,
57 SourceObjectNotFound,
59 Unborn,
61 DeletedMissing,
63 RefLockRejected,
65}
66
67impl UpdateMode {
68 #[must_use]
73 pub fn modified_repo(self) -> bool {
74 matches!(
75 self,
76 Self::New | Self::FastForward | Self::Forced | Self::DeletedMissing
77 )
78 }
79
80 #[must_use]
83 pub fn is_rejected(self) -> bool {
84 matches!(
85 self,
86 Self::NonFastForwardRejected
87 | Self::TagUpdateRejected
88 | Self::SourceObjectNotFound
89 | Self::RefLockRejected
90 )
91 }
92
93 #[must_use]
98 pub fn is_reported(self) -> bool {
99 self.modified_repo() || self.is_rejected()
100 }
101
102 #[must_use]
109 pub fn kind(self) -> UpdateKind {
110 match self {
111 Self::New => UpdateKind::New,
112 Self::FastForward => UpdateKind::FastForward,
113 Self::Forced => UpdateKind::Forced,
114 Self::DeletedMissing => UpdateKind::Pruned,
115 Self::NonFastForwardRejected
116 | Self::TagUpdateRejected
117 | Self::SourceObjectNotFound
118 | Self::RefLockRejected => UpdateKind::Rejected,
119 Self::UpToDate | Self::NoChangeNeeded | Self::Unborn => UpdateKind::UpToDate,
120 }
121 }
122
123 pub(crate) fn fetch_reflog_reason(self) -> Option<&'static str> {
124 match self {
125 Self::New => Some("storing head"),
126 Self::FastForward => Some("fast-forward"),
127 Self::Forced => Some("forced-update"),
128 _ => None,
129 }
130 }
131}
132
133#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
135#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
136#[cfg_attr(feature = "serde-models", serde(rename_all = "snake_case"))]
137pub enum UpdateKind {
138 New,
140 FastForward,
142 Forced,
144 Pruned,
146 Rejected,
148 UpToDate,
150}
151
152impl UpdateKind {
153 #[must_use]
155 pub fn changes_ref(self) -> bool {
156 matches!(
157 self,
158 Self::New | Self::FastForward | Self::Forced | Self::Pruned
159 )
160 }
161}
162
163pub(crate) struct FetchApplyReflog<'a> {
165 pub action: &'static str,
166 pub identity: &'a str,
167}
168
169#[derive(Clone, Debug)]
171#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
172pub struct RefUpdate {
173 pub remote_ref: String,
175 pub local_ref: Option<String>,
177 pub old_oid: Option<ObjectId>,
179 pub new_oid: Option<ObjectId>,
181 pub mode: UpdateMode,
183 pub note: Option<String>,
185}
186
187#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
189pub enum TagMode {
190 None,
192 #[default]
211 Following,
212 All,
215}
216
217#[derive(Clone, Copy, Debug, PartialEq, Eq)]
219pub enum FetchDepth {
220 Depth(NonZeroU32),
223 Deepen(NonZeroU32),
226 Unshallow,
228}
229
230impl FetchDepth {
231 #[must_use]
233 pub fn depth(n: u32) -> Option<Self> {
234 NonZeroU32::new(n).map(Self::Depth)
235 }
236
237 #[must_use]
239 pub fn deepen(n: u32) -> Option<Self> {
240 NonZeroU32::new(n).map(Self::Deepen)
241 }
242}
243
244#[non_exhaustive]
246pub struct FetchOptions {
247 pub refspecs: Vec<String>,
249 pub negative_refspecs: Vec<String>,
251 pub tags: TagMode,
253 pub prune: bool,
255 pub dry_run: bool,
257 pub fetch_depth: Option<FetchDepth>,
260 pub deepen_since: Option<String>,
265 pub deepen_not: Vec<String>,
269 pub(crate) initial_remote_fetch: bool,
272 pub remote_name: Option<String>,
274 pub fetch_source: Option<String>,
277 pub configured_refspecs: Vec<String>,
280 pub clone_reflog: Option<CloneReflog>,
282 pub(crate) reflog_action: &'static str,
284 pub force: bool,
288 pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
290 pub network_trace: bool,
292 pub index_pack_threads: Option<usize>,
294 pub thread_policy: crate::parallel::ThreadPolicy,
296 pub environment: crate::environment::Environment,
298 pub(crate) pack_spool_only: bool,
300 pub http_protocol_version: Option<u8>,
302 pub remote_progress: bool,
305 pub filter: Option<ObjectFilterSpec>,
308 pub auto_filter: bool,
314 pub(crate) mark_received_pack_promisor: bool,
319 pub(crate) object_wants: Vec<ObjectId>,
321 pub(crate) promisor_object_fetch: bool,
323 pub link_source_objects: bool,
334}
335
336impl Default for FetchOptions {
337 fn default() -> Self {
338 Self {
339 refspecs: Vec::new(),
340 negative_refspecs: Vec::new(),
341 tags: TagMode::Following,
342 prune: false,
343 dry_run: false,
344 fetch_depth: None,
345 deepen_since: None,
346 deepen_not: Vec::new(),
347 initial_remote_fetch: false,
348 remote_name: None,
349 fetch_source: None,
350 configured_refspecs: Vec::new(),
351 clone_reflog: None,
352 reflog_action: "fetch",
353 diagnostics: None,
354 network_trace: false,
355 index_pack_threads: None,
356 thread_policy: crate::parallel::ThreadPolicy::default(),
357 environment: crate::environment::Environment::empty(),
358 pack_spool_only: false,
359 http_protocol_version: None,
360 remote_progress: false,
361 filter: None,
362 auto_filter: false,
363 mark_received_pack_promisor: false,
364 object_wants: Vec::new(),
365 promisor_object_fetch: false,
366 link_source_objects: false,
367 force: false,
368 }
369 }
370}
371
372#[derive(Clone, Debug)]
374pub struct CloneReflog {
375 pub identity: crate::ident::Signature,
377 pub message: String,
379}
380
381impl Clone for FetchOptions {
382 fn clone(&self) -> Self {
383 Self {
384 refspecs: self.refspecs.clone(),
385 negative_refspecs: self.negative_refspecs.clone(),
386 tags: self.tags,
387 prune: self.prune,
388 dry_run: self.dry_run,
389 fetch_depth: self.fetch_depth,
390 deepen_since: self.deepen_since.clone(),
391 deepen_not: self.deepen_not.clone(),
392 initial_remote_fetch: self.initial_remote_fetch,
393 remote_name: self.remote_name.clone(),
394 fetch_source: self.fetch_source.clone(),
395 configured_refspecs: self.configured_refspecs.clone(),
396 clone_reflog: self.clone_reflog.clone(),
397 reflog_action: self.reflog_action,
398 diagnostics: self.diagnostics.clone(),
399 network_trace: self.network_trace,
400 index_pack_threads: self.index_pack_threads,
401 thread_policy: self.thread_policy,
402 environment: self.environment.clone(),
403 pack_spool_only: self.pack_spool_only,
404 http_protocol_version: self.http_protocol_version,
405 remote_progress: self.remote_progress,
406 filter: self.filter,
407 auto_filter: self.auto_filter,
408 mark_received_pack_promisor: self.mark_received_pack_promisor,
409 object_wants: self.object_wants.clone(),
410 promisor_object_fetch: self.promisor_object_fetch,
411 link_source_objects: self.link_source_objects,
412 force: self.force,
413 }
414 }
415}
416
417impl FetchOptions {
418 #[must_use]
420 pub fn new() -> Self {
421 Self::default()
422 }
423
424 #[must_use]
426 pub fn initial_remote_fetch(mut self, enabled: bool) -> Self {
427 self.initial_remote_fetch = enabled;
428 self
429 }
430
431 #[doc(hidden)]
433 #[must_use]
434 pub fn pack_spool_only(mut self, enabled: bool) -> Self {
435 self.pack_spool_only = enabled;
436 self
437 }
438
439 #[doc(hidden)]
441 #[must_use]
442 pub fn promisor_object_wants(mut self, wants: Vec<ObjectId>) -> Self {
443 self.object_wants = wants;
444 self.promisor_object_fetch = true;
445 self
446 }
447
448 #[must_use]
454 pub fn has_deepen_request(&self) -> bool {
455 self.fetch_depth.is_some()
456 || self
457 .deepen_since
458 .as_deref()
459 .is_some_and(|v| !v.trim().is_empty())
460 || self.deepen_not.iter().any(|v| !v.trim().is_empty())
461 }
462
463 #[must_use]
465 pub(crate) fn wire_deepen(&self) -> Option<(u32, bool)> {
466 match self.fetch_depth {
467 Some(FetchDepth::Depth(n)) => Some((n.get(), false)),
468 Some(FetchDepth::Deepen(n)) => Some((n.get(), true)),
469 Some(FetchDepth::Unshallow) => Some((crate::shallow::INFINITE_DEPTH, false)),
470 None => None,
471 }
472 }
473
474 #[must_use]
476 pub(crate) fn absolute_depth_for_tags(&self) -> Option<u32> {
477 match self.fetch_depth {
478 Some(FetchDepth::Depth(n)) => Some(n.get()),
479 Some(FetchDepth::Deepen(_)) | Some(FetchDepth::Unshallow) | None => None,
480 }
481 }
482}
483
484pub fn validate_fetch_depth_local(local_git_dir: &Path, opts: &FetchOptions) -> Result<()> {
491 if matches!(opts.fetch_depth, Some(FetchDepth::Unshallow)) {
492 let local = crate::shallow::load_shallow_oids(local_git_dir)?;
493 if local.is_empty() {
494 return Err(Error::FetchUnshallowNotShallow);
495 }
496 }
497 Ok(())
498}
499
500pub(crate) fn validate_fetch_depth_server(
502 opts: &FetchOptions,
503 v0_caps: Option<&str>,
504 v2_caps: Option<&[String]>,
505) -> Result<()> {
506 let needs_shallow = opts.has_deepen_request()
507 || opts.wire_deepen().is_some()
508 || opts
509 .deepen_since
510 .as_deref()
511 .is_some_and(|s| !s.trim().is_empty())
512 || opts.deepen_not.iter().any(|s| !s.trim().is_empty());
513 if !needs_shallow {
514 return Ok(());
515 }
516 if let Some(caps) = v0_caps {
517 if !caps.contains("shallow") {
518 return Err(Error::FetchShallowUnsupported);
519 }
520 if opts.wire_deepen().is_some_and(|(_, rel)| rel) && !caps.contains("deepen-relative") {
521 return Err(Error::FetchDeepenRelativeUnsupported);
522 }
523 } else if let Some(caps) = v2_caps {
524 if !crate::protocol_v2::fetch_supports_feature(caps, "shallow") {
525 return Err(Error::FetchShallowUnsupported);
526 }
527 }
528 Ok(())
529}
530
531pub fn resolve_fetch_prune(
541 config: &crate::config::ConfigSet,
542 remote: &str,
543 cli_prune: Option<bool>,
544) -> Result<bool> {
545 if let Some(enabled) = cli_prune {
546 return Ok(enabled);
547 }
548 let remote_key = format!("remote.{remote}.prune");
549 if let Some(value) = config.get_bool(&remote_key).transpose()? {
550 return Ok(value);
551 }
552 if let Some(value) = config.get_bool("fetch.prune").transpose()? {
553 return Ok(value);
554 }
555 Ok(false)
556}
557
558#[must_use]
565pub fn resolve_fetch_tags(
566 config: &crate::config::ConfigSet,
567 remote: &str,
568 cli_tags: Option<bool>,
569) -> TagMode {
570 if let Some(all) = cli_tags {
571 return if all { TagMode::All } else { TagMode::None };
572 }
573 let key = format!("remote.{remote}.tagOpt");
574 match config.get(&key).as_deref().map(str::trim) {
575 Some("--tags") | Some("tags") | Some("all") => TagMode::All,
576 Some("--no-tags") | Some("no-tags") | Some("no") | Some("never") => TagMode::None,
577 _ => TagMode::Following,
578 }
579}
580
581#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
583#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
584pub struct TransferStats {
585 pub objects: u64,
587 pub bytes: u64,
589 pub kinds: Option<crate::index_pack::ObjectKindCounts>,
592}
593
594impl TransferStats {
595 #[must_use]
598 pub fn from_ingested_pack(ingested: &crate::index_pack::IngestedPack) -> Self {
599 Self {
600 objects: u64::try_from(ingested.object_ids.len()).unwrap_or(u64::MAX),
601 bytes: ingested.pack_bytes,
602 kinds: Some(ingested.object_counts),
603 }
604 }
605}
606
607impl std::ops::AddAssign for TransferStats {
612 fn add_assign(&mut self, other: Self) {
613 self.kinds = match (self.kinds, other.kinds) {
614 (Some(mut mine), Some(theirs)) => {
615 mine += theirs;
616 Some(mine)
617 }
618 (mine, None) if other.objects == 0 => mine,
619 (None, theirs) if self.objects == 0 => theirs,
620 _ => None,
621 };
622 self.objects += other.objects;
623 self.bytes += other.bytes;
624 }
625}
626
627#[derive(Clone, Debug, Default)]
629#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
630pub struct FetchOutcome {
631 pub updates: Vec<RefUpdate>,
633 pub default_branch: Option<String>,
635 pub new_shallow: Vec<ObjectId>,
639 pub new_unshallow: Vec<ObjectId>,
643 pub transfer: TransferStats,
645 pub filter_sent: bool,
650 pub promisor: Option<crate::promisor_remote::PromisorHandshake>,
653}
654
655impl FetchOutcome {
656 #[must_use]
658 pub fn branch_count(&self) -> u64 {
659 self.updates
660 .iter()
661 .filter(|u| u.remote_ref.starts_with("refs/heads/"))
662 .count() as u64
663 }
664
665 #[must_use]
667 pub fn tag_count(&self) -> u64 {
668 self.updates
669 .iter()
670 .filter(|u| u.remote_ref.starts_with("refs/tags/") && !u.remote_ref.ends_with("^{}"))
671 .count() as u64
672 }
673
674 pub fn reported_updates(&self) -> impl Iterator<Item = &RefUpdate> {
676 self.updates.iter().filter(|u| u.mode.is_reported())
677 }
678
679 #[must_use]
684 pub fn changed_ref_count(&self) -> usize {
685 self.updates
686 .iter()
687 .filter(|u| u.mode.modified_repo())
688 .count()
689 }
690
691 #[must_use]
693 pub fn has_rejections(&self) -> bool {
694 self.updates.iter().any(|u| u.mode.is_rejected())
695 }
696}
697
698fn object_kind_counts_for_oids(
699 odb: &Odb,
700 oids: &HashSet<ObjectId>,
701) -> Result<crate::index_pack::ObjectKindCounts> {
702 use crate::index_pack::ObjectKindCounts;
703 let mut kinds = ObjectKindCounts::default();
704 for oid in oids {
705 kinds.add(odb.read_info(oid)?.kind);
706 }
707 Ok(kinds)
708}
709
710fn loose_transfer_stats(odb: &Odb, oids: &HashSet<ObjectId>) -> (u64, u64) {
711 let mut objects = 0u64;
712 let mut bytes = 0u64;
713 for size in oids.iter().filter_map(|oid| odb.local_loose_file_size(oid)) {
714 objects += 1;
715 bytes += size;
716 }
717 (objects, bytes)
718}
719
720fn pack_file_basenames(pack_dir: &Path) -> HashSet<String> {
721 let mut names = HashSet::new();
722 let Ok(entries) = fs::read_dir(pack_dir) else {
723 return names;
724 };
725 for entry in entries.flatten() {
726 let path = entry.path();
727 if path.extension().is_some_and(|e| e == "pack") {
728 if let Some(name) = path.file_name() {
729 names.insert(name.to_string_lossy().into_owned());
730 }
731 }
732 }
733 names
734}
735
736const LOCAL_LINK_KINDS_OID_PROBE_MAX: u64 = 4096;
737
738fn oids_from_newly_linked_pack_indexes(
739 local_odb: &Odb,
740 pack_dir_before: &HashSet<String>,
741) -> Result<Vec<ObjectId>> {
742 let pack_dir = local_odb.objects_dir().join("pack");
743 let mut oids = Vec::new();
744 if !pack_dir.is_dir() {
745 return Ok(oids);
746 }
747 for entry in fs::read_dir(&pack_dir).map_err(Error::Io)? {
748 let path = entry.map_err(Error::Io)?.path();
749 if path.extension().is_none_or(|e| e != "pack") {
750 continue;
751 }
752 let Some(name) = path.file_name() else {
753 continue;
754 };
755 if pack_dir_before.contains(name.to_string_lossy().as_ref()) {
756 continue;
757 }
758 let idx = path.with_extension("idx");
759 if !idx.is_file() {
760 continue;
761 }
762 let index = crate::pack::read_pack_index_no_verify(&idx)?;
763 for entry in index.iter() {
764 let oid = ObjectId::from_bytes(entry.oid())?;
765 oids.push(oid);
766 }
767 }
768 Ok(oids)
769}
770
771fn enrich_local_link_transfer_kinds(
772 local_odb: &Odb,
773 pack_dir_before: &HashSet<String>,
774 loose_reachable: &HashSet<ObjectId>,
775 transfer: &mut TransferStats,
776) -> Result<()> {
777 if transfer.kinds.is_some() || transfer.objects == 0 {
778 return Ok(());
779 }
780 if transfer.objects > LOCAL_LINK_KINDS_OID_PROBE_MAX {
781 return Ok(());
782 }
783 let mut probe: HashSet<ObjectId> = loose_reachable.clone();
784 for oid in oids_from_newly_linked_pack_indexes(local_odb, pack_dir_before)? {
785 probe.insert(oid);
786 }
787 if u64::try_from(probe.len()).unwrap_or(u64::MAX) != transfer.objects {
788 return Ok(());
789 }
790 transfer.kinds = Some(object_kind_counts_for_oids(local_odb, &probe)?);
791 Ok(())
792}
793
794fn transfer_stats_for_local_operation(
799 local_odb: &Odb,
800 installed: &HashSet<ObjectId>,
801 loose_candidates: Option<&HashSet<ObjectId>>,
802 pack_dir_before: &HashSet<String>,
803) -> Result<TransferStats> {
804 let objects_dir = local_odb.objects_dir();
805 let pack_dir = objects_dir.join("pack");
806 let mut bytes = 0u64;
807 let mut objects_from_new_packs = 0u64;
808 if pack_dir.is_dir() {
809 for entry in fs::read_dir(&pack_dir).map_err(Error::Io)? {
810 let path = entry.map_err(Error::Io)?.path();
811 if path.extension().is_none_or(|e| e != "pack") {
812 continue;
813 }
814 let Some(name) = path.file_name() else {
815 continue;
816 };
817 let name = name.to_string_lossy();
818 if pack_dir_before.contains(name.as_ref()) {
819 continue;
820 }
821 bytes += fs::metadata(&path).map_err(Error::Io)?.len();
822 let idx = path.with_extension("idx");
823 if idx.is_file() {
824 let index = crate::pack::read_pack_index_no_verify(&idx)?;
825 objects_from_new_packs += u64::try_from(index.len()).unwrap_or(u64::MAX);
826 }
827 }
828 }
829 let (loose_objects, loose_bytes) =
830 loose_transfer_stats(local_odb, loose_candidates.unwrap_or(installed));
831 bytes = bytes.saturating_add(loose_bytes);
832 let installed_count = u64::try_from(installed.len()).unwrap_or(u64::MAX);
833 let objects = if objects_from_new_packs > 0 {
834 objects_from_new_packs.saturating_add(loose_objects)
835 } else {
836 installed_count
837 };
838 let kinds = if objects == installed_count && installed_count <= LOCAL_LINK_KINDS_OID_PROBE_MAX {
839 Some(object_kind_counts_for_oids(local_odb, installed)?)
840 } else {
841 None
842 };
843 Ok(TransferStats {
844 objects,
845 bytes,
846 kinds,
847 })
848}
849
850#[derive(Clone, Debug, Default)]
852pub struct PushRefSpec {
853 pub src: Option<ObjectId>,
855 pub src_ref: Option<String>,
857 pub dst: String,
859 pub force: bool,
861 pub delete: bool,
863 pub expected_old: Option<ObjectId>,
866 pub expect_absent: bool,
871 pub hook_src_ref: Option<String>,
875}
876
877#[must_use]
879pub(crate) fn push_spec_local_ref(spec: &PushRefSpec) -> Option<String> {
880 spec.src_ref.clone()
881}
882
883#[must_use]
889pub fn resolve_push_delete_ref(
890 git_dir: &Path,
891 remote_ref_names: &HashSet<String>,
892 spec: &str,
893) -> String {
894 if spec.starts_with("refs/") {
895 return spec.to_owned();
896 }
897 let branch = format!("refs/heads/{spec}");
898 let tag = format!("refs/tags/{spec}");
899 if crate::refs::resolve_ref(git_dir, &branch).is_ok() {
900 return branch;
901 }
902 if remote_ref_names.contains(&branch) {
903 return branch;
904 }
905 if crate::refs::resolve_ref(git_dir, &tag).is_ok() {
906 return tag;
907 }
908 if remote_ref_names.contains(&tag) {
909 return tag;
910 }
911 branch
912}
913
914#[derive(Clone, Default)]
916pub struct PushOptions {
917 pub atomic: bool,
924 pub dry_run: bool,
926 pub tracking_remote: Option<String>,
929 pub push_options: Vec<String>,
943 pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
945 pub network_trace: bool,
947 pub remote_ref_tips: Option<Vec<ObjectId>>,
955 pub resumable_threshold: Option<u64>,
960 pub no_verify: bool,
968}
969
970pub(crate) struct PrePushGate<'a> {
972 pub(crate) repo: &'a crate::repo::Repository,
974 pub(crate) remote: &'a str,
976 pub(crate) url: &'a str,
978}
979
980impl PrePushGate<'_> {
981 pub(crate) fn check<'r>(
988 &self,
989 updates: impl IntoIterator<Item = (&'r PushRefSpec, &'r PushRefResult)>,
990 ) -> Result<()> {
991 let refs: Vec<crate::hooks::PrePushRef> = updates
992 .into_iter()
993 .filter(|(_, r)| {
994 !matches!(
995 r.status,
996 PushRefStatus::UpToDate
997 | PushRefStatus::RejectNonFastForward
998 | PushRefStatus::RejectNeedsForce
999 | PushRefStatus::RejectStale
1000 | PushRefStatus::RejectRemoteRefMissing
1001 )
1002 })
1003 .map(|(spec, r)| crate::hooks::PrePushRef {
1004 local_ref: if r.deletion {
1005 None
1006 } else {
1007 spec.hook_src_ref.clone().or_else(|| r.local_ref.clone())
1008 },
1009 local_oid: r.new_oid,
1010 remote_ref: r.remote_ref.clone(),
1011 remote_oid: r.old_oid,
1012 })
1013 .collect();
1014 crate::hooks::run_pre_push_hook(self.repo, self.remote, self.url, &refs)?;
1015 Ok(())
1016 }
1017}
1018
1019#[derive(Clone, Debug, Default)]
1021#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
1022pub struct PushOutcome {
1023 pub results: Vec<PushRefResult>,
1025 pub remote_messages: Vec<String>,
1030}
1031
1032impl PushOutcome {
1033 #[must_use]
1036 pub fn has_updates(&self) -> bool {
1037 self.results.iter().any(|r| r.status == PushRefStatus::Ok)
1038 }
1039
1040 #[must_use]
1043 pub fn has_rejections(&self) -> bool {
1044 self.results.iter().any(|r| r.status.is_error())
1045 }
1046}
1047
1048pub use crate::pack_objects::{
1049 build_pack, build_pack_from_send_list, reachable_closure, PackBuildOptions, PackObjects,
1050 PackObjectsOptions, PackStats,
1051};
1052
1053pub(crate) use crate::pack_objects::write_pack_for_local_fetch;
1054
1055fn pack_build_options_for_send(
1056 source_odb: &Odb,
1057 oids: &[ObjectId],
1058 base: &PackBuildOptions,
1059) -> PackBuildOptions {
1060 if oids.is_empty() || oids.len() > 512 {
1061 return *base;
1062 }
1063 let commits_tags_only = oids.iter().all(|oid| {
1064 source_odb
1065 .read(oid)
1066 .map(|obj| matches!(obj.kind, ObjectKind::Commit | ObjectKind::Tag))
1067 .unwrap_or(false)
1068 });
1069 if !commits_tags_only {
1070 return *base;
1071 }
1072 let mut fast = *base;
1073 fast.delta = false;
1074 fast.reuse_deltas = false;
1075 fast.thin = false;
1076 fast
1077}
1078
1079fn ingest_pack_options_for_fetch(
1080 local_git_dir: &Path,
1081 fetch_opts: &FetchOptions,
1082) -> Result<crate::index_pack::IngestPackOptions> {
1083 let pack_config =
1084 crate::config::ConfigSet::load(&fetch_opts.environment, Some(local_git_dir), true)?;
1085 let index_threads = crate::parallel::index_pack_threads_for_ingest(
1086 fetch_opts.thread_policy,
1087 &pack_config,
1088 fetch_opts.index_pack_threads,
1089 )?;
1090 Ok(crate::index_pack::IngestPackOptions {
1091 fix_thin: true,
1092 skip_post_index_verify: true,
1093 threads: index_threads,
1094 merged_config: Some(pack_config),
1095 environment: fetch_opts.environment.clone(),
1096 mark_promisor_pack: fetch_opts.mark_received_pack_promisor,
1097 keep_message: None,
1098 trusted_index_rows: None,
1099 })
1100}
1101
1102struct LocalCopyProgress {
1104 total: u64,
1105 done: u64,
1106 last_reported: u64,
1107 phase_active: bool,
1108}
1109
1110struct CopyingLooseLink<'a> {
1112 copy: &'a mut LocalCopyProgress,
1113 progress: &'a mut dyn Progress,
1114}
1115
1116impl ProgressSink for CopyingLooseLink<'_> {
1117 fn inc(&mut self, n: u64) {
1118 self.copy.add(self.progress, n);
1119 }
1120}
1121
1122impl LocalCopyProgress {
1123 fn new(total: u64) -> Self {
1124 Self {
1125 total,
1126 done: 0,
1127 last_reported: 0,
1128 phase_active: false,
1129 }
1130 }
1131
1132 fn ensure_started(&mut self, progress: &mut dyn Progress) {
1133 if !self.phase_active {
1134 progress.start(ProgressPhase::CopyingObjects, Some(self.total));
1135 self.phase_active = true;
1136 }
1137 }
1138
1139 fn add(&mut self, progress: &mut dyn Progress, n: u64) {
1140 if n == 0 {
1141 return;
1142 }
1143 self.ensure_started(progress);
1144 self.done = self.done.saturating_add(n).min(self.total);
1145 if self.last_reported < self.done {
1146 crate::progress::report_object_progress_stride(
1147 progress,
1148 self.done,
1149 &mut self.last_reported,
1150 Some(self.total),
1151 );
1152 }
1153 if progress.wants_received_objects() {
1154 progress.received_objects(ReceivedObjects {
1155 received: self.done,
1156 total: self.total,
1157 deltas: 0,
1158 });
1159 }
1160 }
1161
1162 fn finish(&mut self, progress: &mut dyn Progress) {
1163 if !self.phase_active {
1164 return;
1165 }
1166 if self.last_reported < self.done {
1167 progress.set(self.done);
1168 self.last_reported = self.done;
1169 }
1170 progress.finish();
1171 self.phase_active = false;
1172 }
1173}
1174
1175#[allow(clippy::too_many_arguments)]
1181fn copy_objects_for_local_fetch(
1182 local_git_dir: &Path,
1183 remote_odb: &Odb,
1184 local_odb: &Odb,
1185 wants: &[ObjectId],
1186 haves: &[ObjectId],
1187 source_shallow: &HashSet<ObjectId>,
1188 have_shallow: &HashSet<ObjectId>,
1189 try_pack_link: bool,
1190 pack_build: &PackBuildOptions,
1191 fetch_opts: &FetchOptions,
1192 progress: &mut dyn Progress,
1193 cancel: &dyn crate::progress::Cancel,
1194 prep_enumerating: bool,
1195) -> Result<(HashSet<ObjectId>, TransferStats)> {
1196 use crate::local_object_copy::{
1197 count_store_objects_for_link, link_loose_objects, link_loose_objects_for_oids,
1198 link_packs_within, link_remote_pack_files, linked_object_set, refresh_local_odb_after_link,
1199 };
1200
1201 let remote_objects = remote_odb.objects_dir();
1202 let local_objects = local_odb.objects_dir();
1203 let pack_dir_before = pack_file_basenames(&local_objects.join("pack"));
1204 let remote_has_alternates = !crate::pack::read_alternates_recursive(remote_objects)?.is_empty();
1205
1206 if local_odb.uses_files_primary()
1207 && fetch_opts.link_source_objects
1208 && !remote_has_alternates
1209 && !pack_build.thin
1210 && try_pack_link
1211 && haves.is_empty()
1212 && !wants.is_empty()
1213 {
1214 crate::progress::poll_cancel(progress, cancel)?;
1215 let copy_total = count_store_objects_for_link(remote_objects)? as u64;
1216 let mut copy = LocalCopyProgress::new(copy_total);
1217 let linked_packs = link_remote_pack_files(
1218 remote_objects,
1219 local_objects,
1220 &mut crate::progress::NullProgress,
1221 cancel,
1222 0,
1223 )?;
1224 if linked_packs.objects > 0 {
1225 refresh_local_odb_after_link(local_odb);
1226 copy.add(progress, linked_packs.objects as u64);
1227 }
1228 let pack_alone_satisfies_wants =
1229 linked_packs.objects > 0 && wants.iter().all(|oid| local_odb.exists(oid));
1230 let linked_loose = link_loose_objects(
1231 remote_objects,
1232 local_objects,
1233 &mut crate::progress::NullProgress,
1234 cancel,
1235 )?;
1236 if linked_loose > 0 {
1237 refresh_local_odb_after_link(local_odb);
1238 copy.add(progress, linked_loose as u64);
1239 }
1240 if (linked_packs.files > 0 || linked_loose > 0)
1241 && wants.iter().all(|oid| local_odb.exists(oid))
1242 {
1243 copy.finish(progress);
1244 let stats_loose = if pack_alone_satisfies_wants {
1245 HashSet::new()
1246 } else {
1247 crate::local_object_copy::loose_reachable_from_roots(
1248 local_odb,
1249 wants,
1250 source_shallow,
1251 cancel,
1252 )?
1253 };
1254 let oids = if stats_loose.is_empty() {
1255 linked_object_set(local_odb, wants)
1256 } else {
1257 stats_loose.clone()
1258 };
1259 let mut transfer = transfer_stats_for_local_operation(
1260 local_odb,
1261 &stats_loose,
1262 None,
1263 &pack_dir_before,
1264 )?;
1265 enrich_local_link_transfer_kinds(
1266 local_odb,
1267 &pack_dir_before,
1268 &stats_loose,
1269 &mut transfer,
1270 )?;
1271 return Ok((oids, transfer));
1272 }
1273 copy.finish(progress);
1274 }
1275
1276 let stop = crate::progress::CancelSignal::new(cancel);
1277 let have_tips: HashSet<ObjectId> = haves.iter().copied().collect();
1278 if !prep_enumerating {
1279 progress.start(ProgressPhase::EnumeratingObjects, None);
1280 }
1281 let mut enum_last = 0u64;
1282 let send_set = collect_reachable_excluding_peer(
1283 remote_odb,
1284 wants,
1285 local_odb,
1286 &have_tips,
1287 false,
1288 source_shallow,
1289 &stop,
1290 progress,
1291 &mut enum_last,
1292 )?;
1293 let send = &send_set.objects;
1294 let send_len = send.len() as u64;
1295 progress.set(send_len);
1296
1297 let will_scan_packs =
1298 local_odb.uses_files_primary() && !pack_build.thin && try_pack_link && !send.is_empty();
1299 if !will_scan_packs {
1300 progress.finish();
1301 }
1302
1303 let mut copy = LocalCopyProgress::new(send_len);
1304
1305 if will_scan_packs {
1306 let local_has = |oid: &ObjectId| local_odb.exists_materialized_local_without_rescan(oid);
1307 let mut pack_scan = send_len;
1308 let mut pack_scan_last = send_len;
1309 let mut on_pack_scan = |n: u64| {
1310 pack_scan = pack_scan.saturating_add(n);
1311 crate::progress::report_object_progress_stride(
1312 progress,
1313 pack_scan,
1314 &mut pack_scan_last,
1315 None,
1316 );
1317 };
1318 let linked = link_packs_within(
1319 remote_objects,
1320 local_objects,
1321 send,
1322 &local_has,
1323 &mut crate::progress::NullProgress,
1324 cancel,
1325 Some(&mut on_pack_scan),
1326 )?;
1327 progress.finish();
1328 if linked > 0 {
1329 refresh_local_odb_after_link(local_odb);
1330 copy.add(progress, linked as u64);
1331 }
1332 }
1333
1334 let mut still_missing = objects_missing_from_peer(local_odb, send)?;
1335 if still_missing.is_empty() {
1336 copy.finish(progress);
1337 let oids: HashSet<ObjectId> = send.iter().copied().collect();
1339 let transfer = transfer_stats_for_local_operation(
1340 local_odb,
1341 &oids,
1342 Some(&HashSet::new()),
1343 &pack_dir_before,
1344 )?;
1345 return Ok((oids, transfer));
1346 }
1347
1348 let loose_candidates: HashSet<ObjectId> = still_missing.iter().copied().collect();
1352 if local_odb.uses_files_primary() {
1353 let mut loose_link = CopyingLooseLink {
1354 copy: &mut copy,
1355 progress,
1356 };
1357 let linked_loose = link_loose_objects_for_oids(
1358 remote_objects,
1359 local_objects,
1360 &still_missing,
1361 &mut loose_link,
1362 cancel,
1363 )?;
1364 if linked_loose > 0 {
1365 refresh_local_odb_after_link(local_odb);
1366 copy.add(progress, linked_loose as u64);
1367 still_missing = local_odb.missing_oids(still_missing)?;
1368 }
1369 }
1370 if still_missing.is_empty() {
1371 copy.finish(progress);
1372 let oids: HashSet<ObjectId> = send.iter().copied().collect();
1374 let transfer = transfer_stats_for_local_operation(
1375 local_odb,
1376 &oids,
1377 Some(&loose_candidates),
1378 &pack_dir_before,
1379 )?;
1380 return Ok((oids, transfer));
1381 }
1382
1383 let pack_opts = pack_build_options_for_send(remote_odb, &still_missing, pack_build);
1384 crate::progress::poll_cancel(progress, cancel)?;
1385 let mut spool = crate::pack_receive::TempPackReceive::create_in_pack_dir(local_objects)?;
1388 write_pack_for_local_transfer(
1389 remote_odb,
1390 &send_set,
1391 &still_missing,
1392 local_odb,
1393 haves,
1394 source_shallow,
1395 have_shallow,
1396 &pack_opts,
1397 &stop,
1398 &mut spool,
1399 Some((&mut copy, progress)),
1400 )?;
1401 copy.finish(progress);
1402 crate::progress::poll_cancel(progress, cancel)?;
1403 let ingested = match spool.finish()? {
1404 Some(pack_path) => {
1405 let ingest_opts = ingest_pack_options_for_fetch(local_git_dir, fetch_opts)?;
1406 crate::index_pack::ingest_received_pack_path_with_progress(
1407 pack_path,
1408 local_odb,
1409 &ingest_opts,
1410 Some(progress),
1411 Some(cancel),
1412 )?
1413 }
1414 None => crate::index_pack::IngestedPack::default(),
1415 };
1416 refresh_local_odb_after_link(local_odb);
1417 let mut out: HashSet<ObjectId> = send.iter().copied().collect();
1419 out.extend(ingested.object_ids);
1420 let transfer = transfer_stats_for_local_operation(local_odb, &out, None, &pack_dir_before)?;
1421 Ok((out, transfer))
1422}
1423
1424#[allow(clippy::too_many_arguments)]
1436fn copy_objects_for_local_push(
1437 local_odb: &Odb,
1438 peer_odb: &Odb,
1439 dest_odb: &Odb,
1440 wants: &[ObjectId],
1441 haves: &[ObjectId],
1442 source_shallow: &HashSet<ObjectId>,
1443 have_shallow: &HashSet<ObjectId>,
1444 opts: &PackBuildOptions,
1445 cancel: &dyn crate::progress::Cancel,
1446) -> Result<()> {
1447 use crate::local_object_copy::{
1448 link_loose_objects_for_oids, link_packs_within, refresh_local_odb_after_link,
1449 };
1450
1451 let local_objects = local_odb.objects_dir();
1452 let dest_objects = dest_odb.objects_dir();
1453 let stop = crate::progress::CancelSignal::new(cancel);
1454
1455 let have_tips: HashSet<ObjectId> = haves.iter().copied().collect();
1456 let mut enum_last = 0u64;
1457 let mut enum_sink = crate::progress::NullProgress;
1458 let send_set = collect_reachable_excluding_peer(
1459 local_odb,
1460 wants,
1461 peer_odb,
1462 &have_tips,
1463 false,
1464 source_shallow,
1465 &stop,
1466 &mut enum_sink,
1467 &mut enum_last,
1468 )?;
1469 let send = &send_set.objects;
1470
1471 let mut progress = crate::progress::NullProgress;
1472 let dest_has = |oid: &ObjectId| dest_odb.exists_materialized_local_without_rescan(oid);
1473 if local_odb.uses_files_primary()
1474 && !opts.thin
1475 && link_packs_within(
1476 local_objects,
1477 dest_objects,
1478 send,
1479 &dest_has,
1480 &mut progress,
1481 cancel,
1482 None,
1483 )? > 0
1484 {
1485 refresh_local_odb_after_link(dest_odb);
1486 }
1487
1488 let mut still_missing = objects_missing_from_peer(dest_odb, send)?;
1489 if still_missing.is_empty() {
1490 return Ok(());
1491 }
1492
1493 if local_odb.uses_files_primary() && !haves.is_empty() {
1494 let linked_loose = link_loose_objects_for_oids(
1495 local_objects,
1496 dest_objects,
1497 &still_missing,
1498 &mut progress,
1499 cancel,
1500 )?;
1501 if linked_loose > 0 {
1502 refresh_local_odb_after_link(dest_odb);
1503 still_missing = dest_odb.missing_oids(still_missing)?;
1504 }
1505 if still_missing.is_empty() {
1506 return Ok(());
1507 }
1508 }
1509
1510 let pack_opts = pack_build_options_for_send(local_odb, &still_missing, opts);
1511 let mut spool = crate::pack_receive::TempPackReceive::create_in_pack_dir(dest_objects)?;
1512 write_pack_for_local_transfer(
1513 local_odb,
1514 &send_set,
1515 &still_missing,
1516 peer_odb,
1517 haves,
1518 source_shallow,
1519 have_shallow,
1520 &pack_opts,
1521 &stop,
1522 &mut spool,
1523 None,
1524 )?;
1525 let Some(pack_path) = spool.finish()? else {
1526 return Ok(());
1527 };
1528 crate::index_pack::ingest_received_pack_path_with_progress(
1531 pack_path,
1532 dest_odb,
1533 &crate::index_pack::IngestPackOptions {
1534 fix_thin: pack_opts.thin,
1535 skip_post_index_verify: true,
1536 ..Default::default()
1537 },
1538 None,
1539 Some(cancel),
1540 )?;
1541 refresh_local_odb_after_link(dest_odb);
1542 Ok(())
1543}
1544
1545fn objects_missing_from_peer(peer_odb: &Odb, send: &[ObjectId]) -> Result<Vec<ObjectId>> {
1551 if !peer_odb.uses_files_primary() {
1552 if peer_odb.custom_primary_is_empty()? {
1553 return Ok(send.to_vec());
1554 }
1555 return peer_odb.missing_materialized_many(send);
1556 }
1557 peer_odb.reprepare_local_packs();
1558 Ok(send
1559 .iter()
1560 .copied()
1561 .filter(|oid| !peer_odb.exists_materialized_local_without_rescan(oid))
1562 .collect())
1563}
1564
1565#[allow(clippy::too_many_arguments)]
1576fn write_pack_for_local_transfer(
1577 source_odb: &Odb,
1578 send_set: &PeerSendSet,
1579 missing: &[ObjectId],
1580 peer_odb: &Odb,
1581 haves: &[ObjectId],
1582 source_shallow: &HashSet<ObjectId>,
1583 have_shallow: &HashSet<ObjectId>,
1584 opts: &PackBuildOptions,
1585 stop: &crate::progress::CancelSignal<'_>,
1586 w: &mut dyn Write,
1587 copy: Option<(&mut LocalCopyProgress, &mut dyn Progress)>,
1588) -> Result<()> {
1589 if !opts.thin && have_shallow.is_empty() {
1590 let pack_budget = missing.len() as u64;
1591 if let Some((copy, progress)) = copy {
1592 let mut pack_reported = 0u64;
1593 let mut on_pack = |n: u64| {
1594 let add = n.min(pack_budget.saturating_sub(pack_reported));
1595 if add > 0 {
1596 pack_reported += add;
1597 copy.add(progress, add);
1598 }
1599 };
1600 crate::pack_objects::write_pack_from_send_list(
1601 source_odb,
1602 missing,
1603 opts,
1604 &send_set.name_hashes,
1605 stop,
1606 w,
1607 Some(&mut on_pack),
1608 )?;
1609 } else {
1610 crate::pack_objects::write_pack_from_send_list(
1611 source_odb,
1612 missing,
1613 opts,
1614 &send_set.name_hashes,
1615 stop,
1616 w,
1617 None,
1618 )?;
1619 }
1620 } else {
1621 write_pack_for_local_fetch(
1622 source_odb,
1623 missing,
1624 peer_odb,
1625 haves,
1626 source_shallow,
1627 have_shallow,
1628 opts,
1629 stop,
1630 w,
1631 )?;
1632 }
1633 Ok(())
1634}
1635
1636struct PeerSendSet {
1638 objects: Vec<ObjectId>,
1640 name_hashes: HashMap<ObjectId, u32>,
1642}
1643
1644#[allow(clippy::too_many_arguments)]
1649fn collect_reachable_excluding_peer(
1650 source_odb: &Odb,
1651 roots: &[ObjectId],
1652 peer_odb: &Odb,
1653 peer_boundary: &HashSet<ObjectId>,
1654 skip_missing: bool,
1655 shallow_grafts: &HashSet<ObjectId>,
1656 stop: &crate::progress::CancelSignal<'_>,
1657 enum_sink: &mut dyn ProgressSink,
1658 enum_last: &mut u64,
1659) -> Result<PeerSendSet> {
1660 use crate::pack_name_hash::pack_name_hash_extend;
1661
1662 stop.poll()?;
1663 if peer_boundary.is_empty() && shallow_grafts.is_empty() {
1664 if let Some(send_set) = bitmap_send_set(source_odb, roots, peer_odb, enum_sink, enum_last)?
1665 {
1666 stop.poll()?;
1667 return Ok(send_set);
1668 }
1669 }
1670
1671 let mut visited: HashSet<ObjectId> = HashSet::new();
1672 let mut ordered: Vec<ObjectId> = Vec::new();
1673 let mut name_hashes: HashMap<ObjectId, u32> = HashMap::new();
1674 let mut queue: VecDeque<(ObjectId, u32)> = VecDeque::new();
1676
1677 let peer_is_files = peer_odb.uses_files_primary();
1680 if peer_is_files {
1681 peer_odb.reprepare_local_packs();
1682 }
1683 let at_boundary = |oid: ObjectId| {
1684 peer_boundary.contains(&oid)
1685 || (peer_is_files && peer_odb.exists_materialized_local_without_rescan(&oid))
1686 };
1687
1688 let enqueue = |oid: ObjectId,
1692 prefix_hash: Option<u32>,
1693 queue: &mut VecDeque<(ObjectId, u32)>,
1694 visited: &mut HashSet<ObjectId>,
1695 ordered: &mut Vec<ObjectId>|
1696 -> bool {
1697 if !visited.insert(oid) || at_boundary(oid) {
1698 return false;
1699 }
1700 ordered.push(oid);
1701 if let Some(prefix_hash) = prefix_hash {
1702 queue.push_back((oid, prefix_hash));
1703 }
1704 true
1705 };
1706
1707 for &root in roots {
1708 enqueue(root, Some(0), &mut queue, &mut visited, &mut ordered);
1709 }
1710
1711 while let Some((oid, prefix_hash)) = queue.pop_front() {
1712 stop.poll()?;
1713 let obj = match source_odb.read(&oid) {
1714 Ok(o) => o,
1715 Err(_) if skip_missing => continue,
1716 Err(e) => return Err(e),
1717 };
1718 match obj.kind {
1719 ObjectKind::Commit => {
1720 let commit = parse_commit(&obj.data)?;
1721 if !shallow_grafts.contains(&oid) {
1722 for parent in commit.parents {
1723 enqueue(parent, Some(0), &mut queue, &mut visited, &mut ordered);
1724 }
1725 }
1726 enqueue(commit.tree, Some(0), &mut queue, &mut visited, &mut ordered);
1727 }
1728 ObjectKind::Tree => {
1729 for entry in parse_tree(&obj.data)? {
1730 if entry.mode == 0o160000 {
1731 continue;
1732 }
1733 let path_hash = pack_name_hash_extend(prefix_hash, &entry.name);
1734 let queued = if entry.mode & 0o170000 == 0o040000 {
1735 let dir_hash = pack_name_hash_extend(path_hash, b"/");
1736 enqueue(
1737 entry.oid,
1738 Some(dir_hash),
1739 &mut queue,
1740 &mut visited,
1741 &mut ordered,
1742 )
1743 } else {
1744 enqueue(entry.oid, None, &mut queue, &mut visited, &mut ordered)
1745 };
1746 if queued {
1747 name_hashes.insert(entry.oid, path_hash);
1748 }
1749 }
1750 }
1751 ObjectKind::Tag => {
1752 let tag = parse_tag(&obj.data)?;
1753 enqueue(
1754 tag.object,
1755 Some(prefix_hash),
1756 &mut queue,
1757 &mut visited,
1758 &mut ordered,
1759 );
1760 }
1761 ObjectKind::Blob => {}
1762 }
1763 let n = ordered.len() as u64;
1764 crate::progress::report_object_progress_stride(enum_sink, n, enum_last, None);
1765 }
1766
1767 Ok(PeerSendSet {
1768 objects: ordered,
1769 name_hashes,
1770 })
1771}
1772
1773fn bitmap_send_set(
1778 source_odb: &Odb,
1779 roots: &[ObjectId],
1780 peer_odb: &Odb,
1781 enum_sink: &mut dyn ProgressSink,
1782 enum_last: &mut u64,
1783) -> Result<Option<PeerSendSet>> {
1784 let no_haves = HashSet::new();
1785 let Some((reachable, _)) = crate::pack_object_select::try_bitmap_reachable_for_upload(
1786 source_odb,
1787 roots,
1788 &[],
1789 None,
1790 &no_haves,
1791 )?
1792 else {
1793 return Ok(None);
1794 };
1795 let mut objects: Vec<ObjectId> = Vec::new();
1796 if peer_odb.uses_files_primary() {
1797 peer_odb.reprepare_local_packs();
1798 for oid in reachable
1799 .iter_grouped_by_kind()
1800 .filter(|oid| !peer_odb.exists_materialized_local_without_rescan(oid))
1801 {
1802 objects.push(oid);
1803 let n = objects.len() as u64;
1804 crate::progress::report_object_progress_stride(enum_sink, n, enum_last, None);
1805 }
1806 } else {
1807 for oid in reachable.iter_grouped_by_kind() {
1808 objects.push(oid);
1809 let n = objects.len() as u64;
1810 crate::progress::report_object_progress_stride(enum_sink, n, enum_last, None);
1811 }
1812 }
1813 let name_hashes = crate::pack_object_select::name_hashes_for_send(
1814 reachable.bitmap_index(),
1815 &objects,
1816 &no_haves,
1817 );
1818 Ok(Some(PeerSendSet {
1819 objects,
1820 name_hashes,
1821 }))
1822}
1823
1824pub fn fetch_local(
1826 local_git_dir: &Path,
1827 remote_git_dir: &Path,
1828 opts: &FetchOptions,
1829) -> Result<FetchOutcome> {
1830 fetch_local_with_progress(
1831 local_git_dir,
1832 remote_git_dir,
1833 opts,
1834 &mut NoProgress,
1835 &crate::progress::NeverCancel,
1836 )
1837}
1838
1839pub(crate) struct FetchMatchSet {
1841 pub(crate) matched: Vec<MatchedRef>,
1842 pub(crate) positive: Vec<RefspecItem>,
1843 pub(crate) negatives: Vec<RefspecItem>,
1844 pub(crate) explicit_refspec_fetch: bool,
1845}
1846
1847pub(crate) fn build_fetch_matched_set(
1849 opts: &FetchOptions,
1850 remote_refs: &[(String, ObjectId)],
1851 remote_head_oid: Option<ObjectId>,
1852) -> Result<FetchMatchSet> {
1853 use crate::fetch_request::{FetchRefspecInput, FetchRefspecPlan, FetchRemoteAdvertisement};
1854
1855 let remote = FetchRemoteAdvertisement {
1856 refs: remote_refs,
1857 head_oid: remote_head_oid,
1858 };
1859 let plan = FetchRefspecPlan::resolve(
1860 &FetchRefspecInput {
1861 user_refspecs: opts.refspecs.clone(),
1862 configured_refspecs: opts.configured_refspecs.clone(),
1863 remote_name: opts.remote_name.clone(),
1864 allow_missing_explicit_for_prune: opts.prune,
1865 },
1866 remote,
1867 )?;
1868 let explicit_refspec_fetch = plan.is_explicit();
1869
1870 let mut negatives = match &plan {
1871 FetchRefspecPlan::Pattern { negatives, .. } | FetchRefspecPlan::Mixed { negatives, .. } => {
1872 negatives.clone()
1873 }
1874 };
1875 for spec in &opts.negative_refspecs {
1876 let item = parse_fetch_refspec(spec)
1877 .map_err(|e| Error::Message(format!("invalid negative refspec '{spec}': {e}")))?;
1878 negatives.push(item);
1879 }
1880
1881 let mut positive = match &plan {
1882 FetchRefspecPlan::Pattern { positive, .. } => positive.clone(),
1883 FetchRefspecPlan::Mixed {
1884 pattern_positive, ..
1885 } => {
1886 let mut positive = pattern_positive.clone();
1887 for spec in &opts.refspecs {
1888 if let Ok(item) = parse_fetch_refspec(spec) {
1889 if !item.negative && !item.pattern && !item.matching {
1890 positive.push(item);
1891 }
1892 }
1893 }
1894 positive
1895 }
1896 };
1897 if opts.prune {
1898 crate::fetch_request::supplement_positive_for_explicit_prune(
1899 &mut positive,
1900 &opts.configured_refspecs,
1901 opts.remote_name.as_deref(),
1902 );
1903 }
1904
1905 let mut matched =
1906 crate::fetch_request::matched_refs_with_negatives(&plan, remote, &negatives, opts.prune)?;
1907 if opts.force {
1908 for m in &mut matched {
1909 if !m.auto_followed {
1910 m.force = true;
1911 }
1912 }
1913 }
1914 Ok(FetchMatchSet {
1915 matched,
1916 positive,
1917 negatives,
1918 explicit_refspec_fetch,
1919 })
1920}
1921
1922pub(crate) fn enforce_fetch_ref_update(
1924 local_ref: &str,
1925 old: Option<ObjectId>,
1926 new: ObjectId,
1927 mode: UpdateMode,
1928 opts: &FetchOptions,
1929 explicit_refspec_fetch: bool,
1930 local_repo: Option<&crate::repo::Repository>,
1931) -> Result<()> {
1932 if opts.dry_run {
1933 return Ok(());
1934 }
1935 let would_write = matches!(
1936 mode,
1937 UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
1938 );
1939 if let Some(short) = local_ref.strip_prefix("refs/heads/") {
1940 if old.is_some()
1941 && old != Some(new)
1942 && (would_write || mode == UpdateMode::NonFastForwardRejected)
1943 {
1944 if let Some(repo) = local_repo {
1945 if crate::worktree::find_worktree_with_branch_checked_out(repo, short, None)?
1946 .is_some()
1947 {
1948 return Err(Error::FetchIntoCheckedOutBranch {
1949 branch: short.to_owned(),
1950 });
1951 }
1952 }
1953 }
1954 }
1955 let _ = explicit_refspec_fetch;
1956 Ok(())
1957}
1958
1959pub(crate) fn ensure_local_remote_repository(remote_git_dir: &Path) -> Result<()> {
1963 if crate::submodule_gitdir::is_valid_git_dir(remote_git_dir) {
1964 return Ok(());
1965 }
1966 Err(Error::remote_unreadable(
1967 remote_git_dir.display().to_string(),
1968 RemoteUnreadableReason::RepositoryNotFound,
1969 ))
1970}
1971
1972pub fn fetch_local_with_progress(
1977 local_git_dir: &Path,
1978 remote_git_dir: &Path,
1979 opts: &FetchOptions,
1980 progress: &mut dyn Progress,
1981 cancel: &dyn crate::progress::Cancel,
1982) -> Result<FetchOutcome> {
1983 crate::progress::poll_cancel(progress, cancel)?;
1984 ensure_local_remote_repository(remote_git_dir)?;
1985 let local = crate::fetch::open_fetch_target(local_git_dir, opts)?;
1986 fetch_local_into(&local, remote_git_dir, opts, progress, cancel)
1987}
1988
1989pub(crate) fn fetch_local_into(
1991 local: &crate::repo::Repository,
1992 remote_git_dir: &Path,
1993 opts: &FetchOptions,
1994 progress: &mut dyn Progress,
1995 cancel: &dyn crate::progress::Cancel,
1996) -> Result<FetchOutcome> {
1997 crate::progress::poll_cancel(progress, cancel)?;
1998 ensure_local_remote_repository(remote_git_dir)?;
1999 let local_git_dir = local.git_dir();
2000 validate_fetch_depth_local(local_git_dir, opts)?;
2001
2002 let remote_odb = open_odb(remote_git_dir);
2003 let remote_algo = remote_odb.hash_algo();
2004 let reopened = sync_empty_destination_object_format(local_git_dir, remote_algo, opts)?
2005 .then(|| reopen_after_format_change(local))
2006 .transpose()?;
2007 let local = reopened.as_ref().unwrap_or(local);
2008 let local_git_dir = local.git_dir();
2009 let local_odb = local.odb();
2010
2011 let remote_entries = crate::remote::list_refs_from_git_dir_for_fetch(
2013 remote_git_dir,
2014 Some(&remote_odb),
2015 &opts.refspecs,
2016 )
2017 .map_err(crate::error::Error::from)?;
2018
2019 let mut default_branch = None;
2020 let mut remote_head_oid = None;
2021 let mut remote_refs: Vec<(String, ObjectId)> = Vec::new();
2023 for entry in &remote_entries {
2024 if entry.name == "HEAD" {
2025 remote_head_oid = Some(entry.oid);
2026 default_branch = entry
2027 .symref_target
2028 .as_ref()
2029 .map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned());
2030 continue;
2031 }
2032 if entry.name.ends_with("^{}") {
2033 continue;
2034 }
2035 if !crate::refs::is_valid_fetch_advertised_ref(&entry.name) {
2036 continue;
2037 }
2038 remote_refs.push((entry.name.clone(), entry.oid));
2039 }
2040
2041 let FetchMatchSet {
2042 mut matched,
2043 positive,
2044 negatives,
2045 explicit_refspec_fetch,
2046 } = build_fetch_matched_set(opts, &remote_refs, remote_head_oid)?;
2047 let mut matched_oids: HashSet<ObjectId> = matched.iter().map(|m| m.oid).collect();
2048 let mut seen_remote_ref: HashSet<String> =
2049 matched.iter().map(|m| m.remote_ref.clone()).collect();
2050
2051 let local_shallow = crate::shallow::load_shallow_boundaries(local_git_dir);
2052 let remote_shallow = crate::shallow::load_shallow_boundaries(remote_git_dir);
2053
2054 let mut prep_enumerating = false;
2055 if !opts.dry_run {
2056 progress.start(ProgressPhase::EnumeratingObjects, None);
2057 prep_enumerating = true;
2058 }
2059
2060 if opts.tags == TagMode::All {
2063 apply_tag_mode(
2064 opts.tags,
2065 &remote_refs,
2066 &remote_odb,
2067 &remote_shallow,
2068 &negatives,
2069 &HashSet::new(),
2070 opts.force,
2071 &mut matched,
2072 &mut matched_oids,
2073 &mut seen_remote_ref,
2074 )?;
2075 }
2076 let shallow_response = if opts.has_deepen_request() {
2077 let shallow_wants: HashSet<ObjectId> = matched
2078 .iter()
2079 .map(|m| shallow_want_commit_tip(&remote_odb, m))
2080 .collect::<Result<_>>()?;
2081 let remote_repo =
2082 crate::repo::Repository::open(remote_git_dir, &RepositoryOptions::empty())?;
2083 let (wire_depth, deepen_relative) = opts
2084 .wire_deepen()
2085 .map(|(d, r)| (Some(d), r))
2086 .unwrap_or((None, false));
2087 let req = crate::serve::ShallowRequest {
2088 client_shallow: local_shallow.iter().copied().collect(),
2089 depth: wire_depth,
2090 deepen_since: opts
2091 .deepen_since
2092 .as_deref()
2093 .and_then(|s| s.parse::<i64>().ok()),
2094 deepen_not: Vec::new(),
2095 deepen_relative,
2096 };
2097 Some(crate::serve::compute_shallow_response(
2098 &remote_repo,
2099 &shallow_wants.into_iter().collect::<Vec<_>>(),
2100 &req,
2101 )?)
2102 } else {
2103 None
2104 };
2105 let copy_source_shallow = shallow_response
2106 .as_ref()
2107 .map(|resp| resp.pack_shallow_grafts.clone())
2108 .unwrap_or_else(|| remote_shallow.clone());
2109 let mut tag_shallow = copy_source_shallow.clone();
2110 if shallow_response.is_none() {
2111 tag_shallow.extend(local_shallow.iter().copied());
2112 }
2113
2114 let following_only = if opts.tags == TagMode::Following {
2115 let tags_present = crate::fetch::tags_already_local(local.refs(), &remote_refs);
2116 apply_tag_mode(
2117 opts.tags,
2118 &remote_refs,
2119 &remote_odb,
2120 &tag_shallow,
2121 &negatives,
2122 &tags_present,
2123 opts.force,
2124 &mut matched,
2125 &mut matched_oids,
2126 &mut seen_remote_ref,
2127 )?
2128 } else {
2129 HashSet::new()
2130 };
2131
2132 let mut following_tag_wants = if shallow_response.is_some() {
2133 local_odb
2136 .missing_oids(
2137 matched
2138 .iter()
2139 .filter(|m| {
2140 following_only.contains(&m.oid)
2141 && m.advertised_peel.is_some_and(|peel| peel != m.oid)
2142 })
2143 .map(|m| m.oid),
2144 )?
2145 .into_iter()
2146 .collect()
2147 } else {
2148 crate::fetch::following_tag_wants_with_local_peel(&matched, &following_only, local_odb)?
2149 };
2150 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
2151 if shallow_request && shallow_response.is_none() {
2152 let fetch_commit_oids = matched
2153 .iter()
2154 .filter(|m| !m.is_tag)
2155 .map(|m| m.oid)
2156 .collect();
2157 let peel_set = crate::fetch::following_peel_commit_set_for_shallow(
2158 &remote_odb,
2159 &fetch_commit_oids,
2160 opts.absolute_depth_for_tags(),
2161 &tag_shallow,
2162 )?;
2163 following_tag_wants.extend(
2164 crate::fetch::following_tag_wants_peel_among_fetched_commits(
2165 &matched,
2166 &following_only,
2167 &peel_set,
2168 ),
2169 );
2170 }
2171
2172 let mut wants: Vec<ObjectId> = if shallow_request {
2174 matched_oids
2175 .iter()
2176 .copied()
2177 .filter(|oid| !following_only.contains(oid) || following_tag_wants.contains(oid))
2178 .collect()
2179 } else {
2180 local_odb.missing_oids(matched_oids.iter().copied())?
2181 };
2182 let (shallow_new, shallow_unshallow) = if let Some(resp) = shallow_response {
2183 for oid in resp.extra_wants.iter().chain(resp.shallow.iter()).copied() {
2184 if !wants.contains(&oid) {
2185 wants.push(oid);
2186 }
2187 }
2188 (resp.shallow, resp.unshallow)
2189 } else {
2190 (Vec::new(), Vec::new())
2191 };
2192 let mut pack_oids: HashSet<ObjectId> = HashSet::new();
2193 let mut transfer = TransferStats::default();
2194 if wants.is_empty() && prep_enumerating {
2195 progress.finish();
2196 prep_enumerating = false;
2197 }
2198 if !wants.is_empty() && !opts.dry_run {
2199 let mut haves: Vec<ObjectId> = Vec::new();
2201 let mut have_seen: HashSet<ObjectId> = HashSet::new();
2202 for m in &matched {
2203 if let Some(local_ref) = &m.local_ref {
2204 if let Ok(old) = local.resolve_ref_oid(local_ref) {
2205 if have_seen.insert(old) {
2206 haves.push(old);
2207 }
2208 }
2209 }
2210 }
2211
2212 let remote_cfg = config_for_git_dir(remote_git_dir);
2213 let pack_opts = PackBuildOptions::for_local_copy(remote_cfg.as_ref());
2214 let (oids, fetch_transfer) = copy_objects_for_local_fetch(
2215 local_git_dir,
2216 &remote_odb,
2217 local_odb,
2218 &wants,
2219 &haves,
2220 ©_source_shallow,
2221 &local_shallow,
2222 !opts.has_deepen_request(),
2223 &pack_opts,
2224 opts,
2225 progress,
2226 cancel,
2227 prep_enumerating,
2228 )?;
2229 pack_oids = oids;
2230 transfer = fetch_transfer;
2231 }
2232
2233 if opts.initial_remote_fetch
2234 && !remote_shallow.is_empty()
2235 && !opts.has_deepen_request()
2236 && !opts.dry_run
2237 {
2238 let remote_shallow_vec: Vec<ObjectId> = remote_shallow.iter().copied().collect();
2239 crate::shallow::apply_shallow_updates(local_git_dir, &remote_shallow_vec, &[])?;
2240 }
2241 if (!shallow_new.is_empty() || !shallow_unshallow.is_empty()) && !opts.dry_run {
2242 crate::shallow::apply_shallow_updates(local_git_dir, &shallow_new, &shallow_unshallow)?;
2243 }
2244
2245 if opts.tags == TagMode::Following {
2246 let boundaries = crate::shallow::load_shallow_boundaries(local_git_dir);
2247 crate::fetch::retain_following_tags(local_odb, &mut matched, &pack_oids, &boundaries)?;
2248 }
2249
2250 let dry_run_remote = if opts.dry_run {
2253 crate::repo::Repository::open(remote_git_dir, &RepositoryOptions::empty()).ok()
2254 } else {
2255 None
2256 };
2257 let classify_repo = if opts.dry_run {
2258 dry_run_remote.as_ref()
2259 } else {
2260 Some(local)
2261 };
2262
2263 let mut updates: Vec<RefUpdate> = Vec::new();
2264
2265 if opts.prune {
2270 prune_tracking_refs(local, &positive, &remote_refs, opts.dry_run, &mut updates)?;
2271 }
2272
2273 let tag_targets_ok = tag_refs_with_local_targets(
2274 local_odb,
2275 &remote_odb,
2276 matched.iter().filter(|m| m.dest_is_tag).map(|m| m.oid),
2277 &pack_oids,
2278 )?;
2279
2280 let mut store_batch: Vec<(String, crate::refs::store::RawRef)> =
2281 Vec::with_capacity(matched.len());
2282 for m in &matched {
2283 let Some(local_ref) = &m.local_ref else {
2284 updates.push(RefUpdate {
2286 remote_ref: m.remote_ref.clone(),
2287 local_ref: None,
2288 old_oid: None,
2289 new_oid: Some(m.oid),
2290 mode: UpdateMode::NoChangeNeeded,
2291 note: Some("not stored (empty destination)".to_owned()),
2292 });
2293 continue;
2294 };
2295
2296 let old = local.resolve_ref_oid(local_ref).ok();
2297 let mode = classify_update(
2298 old.as_ref(),
2299 &m.oid,
2300 m.force,
2301 m.dest_is_tag,
2302 classify_repo,
2303 None,
2304 );
2305
2306 let write = matches!(
2307 mode,
2308 UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
2309 );
2310 enforce_fetch_ref_update(
2311 local_ref,
2312 old,
2313 m.oid,
2314 mode,
2315 opts,
2316 explicit_refspec_fetch,
2317 classify_repo,
2318 )?;
2319 if write && !opts.dry_run {
2320 if !crate::refs::is_valid_storable_ref_name(local_ref) {
2321 updates.push(RefUpdate {
2322 remote_ref: m.remote_ref.clone(),
2323 local_ref: Some(local_ref.clone()),
2324 old_oid: old,
2325 new_oid: Some(m.oid),
2326 mode,
2327 note: Some("skipped (invalid ref name)".to_owned()),
2328 });
2329 continue;
2330 }
2331 if m.dest_is_tag && !tag_targets_ok.contains(&m.oid) {
2332 updates.push(RefUpdate {
2333 remote_ref: m.remote_ref.clone(),
2334 local_ref: Some(local_ref.clone()),
2335 old_oid: old,
2336 new_oid: Some(m.oid),
2337 mode: UpdateMode::NoChangeNeeded,
2338 note: Some("skipped (tag target not present locally)".to_owned()),
2339 });
2340 continue;
2341 }
2342 store_batch.push((local_ref.clone(), crate::refs::store::RawRef::Direct(m.oid)));
2343 }
2344
2345 updates.push(RefUpdate {
2346 remote_ref: m.remote_ref.clone(),
2347 local_ref: Some(local_ref.clone()),
2348 old_oid: old,
2349 new_oid: Some(m.oid),
2350 mode,
2351 note: None,
2352 });
2353 }
2354
2355 if let Some(remote) = opts.remote_name.as_deref() {
2356 if !opts.dry_run {
2357 if let Some(branch) = default_branch.as_deref() {
2358 let head_ref = format!("refs/remotes/{remote}/HEAD");
2359 if crate::refs::resolve_ref_in(local, &head_ref).is_err() {
2360 let target = format!("refs/remotes/{remote}/{branch}");
2361 let target_will_exist = store_batch.iter().any(|(name, _)| *name == target)
2362 || crate::refs::resolve_ref_in(local, &target).is_ok();
2363 if target_will_exist {
2364 store_batch.push((head_ref, crate::refs::store::RawRef::Symbolic(target)));
2365 }
2366 }
2367 }
2368 }
2369 }
2370 if !opts.dry_run && !store_batch.is_empty() {
2371 let fetch_reflog = crate::fetch::prepare_fetch_apply_reflog(local, opts)?;
2372 let fetch_reflog_ctx = fetch_reflog
2373 .as_ref()
2374 .map(crate::fetch::PreparedFetchReflog::context);
2375 apply_fetch_ref_updates(
2376 local,
2377 store_batch,
2378 &mut updates,
2379 FetchRefRejectHint {
2380 prune: opts.prune,
2381 positive: &positive,
2382 remote_refs: &remote_refs,
2383 remote_name: opts.remote_name.as_deref(),
2384 },
2385 opts.initial_remote_fetch,
2386 fetch_reflog_ctx.as_ref(),
2387 )?;
2388 }
2389
2390 crate::fetch::finish_initial_remote_fetch_layout(local, opts, default_branch.as_deref())?;
2391 if let Some(remote) = opts.remote_name.as_deref() {
2392 crate::fetch::ensure_remote_tracking_head_symref(
2393 local,
2394 remote,
2395 default_branch.as_deref(),
2396 opts.dry_run,
2397 )?;
2398 }
2399 finish_anonymous_url_fetch(
2400 local_git_dir,
2401 opts,
2402 explicit_refspec_fetch,
2403 default_branch.as_deref(),
2404 &remote_refs,
2405 &matched,
2406 )?;
2407 let new_shallow = crate::shallow::dedupe_shallow_list(if opts.has_deepen_request() {
2408 shallow_new
2409 } else if opts.initial_remote_fetch {
2410 remote_shallow.iter().copied().collect()
2411 } else {
2412 Vec::new()
2413 });
2414 Ok(FetchOutcome {
2415 updates,
2416 default_branch,
2417 new_shallow,
2418 new_unshallow: shallow_unshallow,
2419 transfer,
2420 filter_sent: false,
2421 promisor: None,
2422 })
2423}
2424
2425pub fn push_local(
2492 local_git_dir: &Path,
2493 remote_git_dir: &Path,
2494 refs: &[PushRefSpec],
2495 opts: &PushOptions,
2496 cancel: &dyn crate::progress::Cancel,
2497) -> Result<PushOutcome> {
2498 let local = crate::repo::Repository::open(local_git_dir, &RepositoryOptions::empty())?;
2499 push_local_from(
2500 &local,
2501 remote_git_dir,
2502 refs,
2503 opts,
2504 &mut crate::fetch::NoProgress,
2505 cancel,
2506 None,
2507 )
2508}
2509
2510pub(crate) fn push_local_from(
2515 local: &crate::repo::Repository,
2516 remote_git_dir: &Path,
2517 refs: &[PushRefSpec],
2518 opts: &PushOptions,
2519 progress: &mut dyn crate::fetch::Progress,
2520 cancel: &dyn crate::progress::Cancel,
2521 pre_push: Option<&PrePushGate<'_>>,
2522) -> Result<PushOutcome> {
2523 let local_git_dir = local.git_dir();
2524 let mut progress_sink = crate::progress::NullProgress;
2525 crate::progress::poll_cancel(&mut progress_sink, cancel)?;
2526 ensure_local_remote_repository(remote_git_dir)?;
2527 let remote_repo =
2528 crate::repo::Repository::open(remote_git_dir, &local.peer_repository_options())?;
2529 let remote_cfg = remote_repo
2530 .config()
2531 .unwrap_or_else(|_| std::sync::Arc::new(crate::config::ConfigSet::new()));
2532 if !opts.push_options.is_empty()
2533 && !remote_cfg
2534 .get_bool("receive.advertisepushoptions")
2535 .and_then(|r| r.ok())
2536 .unwrap_or(false)
2537 {
2538 return Err(Error::PushOptionsUnsupported);
2539 }
2540
2541 let local_odb = local.odb();
2542
2543 let remote_have_tips: Vec<ObjectId> = match &opts.remote_ref_tips {
2546 Some(tips) => tips.clone(),
2547 None => crate::refs::list_refs(remote_git_dir, "refs/")?
2548 .into_iter()
2549 .map(|(_, oid)| oid)
2550 .collect(),
2551 };
2552
2553 let mut decisions: Vec<PushDecision> = Vec::with_capacity(refs.len());
2555 for spec in refs {
2556 decisions.push(decide_push(spec, local_odb, remote_git_dir, Some(local))?);
2557 }
2558 if let Some(gate) = pre_push {
2559 gate.check(refs.iter().zip(decisions.iter().map(|d| &d.result)))?;
2560 }
2561
2562 let push_wants: Vec<ObjectId> = decisions
2563 .iter()
2564 .filter(|d| d.apply && !opts.dry_run)
2565 .filter_map(|d| match &d.action {
2566 PushAction::Update(src) => Some(*src),
2567 _ => None,
2568 })
2569 .collect();
2570 let mut quarantine = None;
2573 if !push_wants.is_empty() {
2574 crate::progress::poll_cancel(&mut progress_sink, cancel)?;
2575 let local_cfg = config_for_git_dir(local_git_dir);
2576 let pack_opts = if remote_have_tips.is_empty() {
2577 PackBuildOptions::for_local_copy(local_cfg.as_ref())
2578 } else {
2579 PackBuildOptions::for_local_push(local_cfg.as_ref())
2580 };
2581 let empty_shallow = HashSet::new();
2582 let remote_shallow = crate::shallow::load_shallow_boundaries(remote_git_dir);
2583 let q = crate::receive_quarantine::ReceiveQuarantine::create(
2584 remote_repo.odb(),
2585 remote_git_dir,
2586 )?;
2587 let q_odb = q.odb();
2588 copy_objects_for_local_push(
2589 local_odb,
2590 remote_repo.odb(),
2591 &q_odb,
2592 &push_wants,
2593 &remote_have_tips,
2594 &empty_shallow,
2595 &remote_shallow,
2596 &pack_opts,
2597 cancel,
2598 )?;
2599 quarantine = Some((q, q_odb));
2600 }
2601
2602 let received: Vec<crate::serve::RefCommand> = if opts.dry_run {
2605 Vec::new()
2606 } else {
2607 decisions
2608 .iter()
2609 .filter(|d| d.apply)
2610 .filter_map(PushDecision::ref_command)
2611 .collect()
2612 };
2613
2614 #[cfg(test)]
2615 tests::run_push_hook(tests::PushHookPoint::BeforeReceivePolicy);
2616
2617 apply_remote_receive_policy(
2618 &remote_repo,
2619 local_git_dir,
2620 opts.dry_run,
2621 quarantine.as_ref().map(|(_, odb)| odb),
2622 &mut decisions,
2623 )?;
2624
2625 if opts.atomic && decisions.iter().any(|d| d.result.status.is_error()) {
2626 fail_atomic_push(&mut decisions);
2627 return Ok(PushOutcome {
2628 results: decisions.into_iter().map(|d| d.result).collect(),
2629 remote_messages: Vec::new(),
2630 });
2631 }
2632
2633 let scripts =
2634 crate::serve::ReceiveHookScripts::new(&remote_repo, &opts.push_options, Vec::new());
2635 let mut remote_messages = Vec::new();
2636 if !received.is_empty() {
2637 let commands: Vec<&crate::serve::RefCommand> = received.iter().collect();
2638 let pre = scripts.pre_receive(&commands, quarantine.as_ref().map(|(q, _)| q));
2639 relay_hook_output(progress, &mut remote_messages, &pre);
2640 if !pre.is_ok() {
2641 for d in decisions.iter_mut().filter(|d| d.apply) {
2642 d.refuse(&crate::serve::RefUpdateRefusal::PreReceiveHookDeclined);
2643 }
2644 }
2645 }
2646 let mut migrated_pack_keeps = Vec::new();
2647 if decisions.iter().any(|d| d.apply) && !opts.dry_run {
2648 if let Some((mut q, _)) = quarantine.take() {
2649 migrated_pack_keeps = q.migrate()?;
2650 remote_repo.odb().invalidate_packs();
2651 }
2652 for d in decisions.iter_mut().filter(|d| d.apply) {
2653 let Some(cmd) = d.ref_command() else {
2654 continue;
2655 };
2656 let update = scripts.update(&cmd);
2657 relay_hook_output(progress, &mut remote_messages, &update);
2658 if !update.is_ok() {
2659 d.refuse(&crate::serve::RefUpdateRefusal::UpdateHookDeclined);
2660 let line = format!("error: hook declined to update {}", d.result.remote_ref);
2663 progress.message(format!("{line}\n").as_bytes());
2664 remote_messages.push(line);
2665 }
2666 }
2667 if opts.atomic && decisions.iter().any(|d| d.result.status.is_error()) {
2668 fail_atomic_push(&mut decisions);
2669 }
2670 }
2671
2672 #[cfg(test)]
2673 tests::run_push_hook(tests::PushHookPoint::BeforeRefCommit);
2674
2675 let mut ref_batch: Vec<crate::refs::store::RefUpdate> = Vec::new();
2677 let mut non_atomic_batch: Vec<(usize, crate::refs::store::RefUpdate)> = Vec::new();
2678 for (idx, d) in decisions.iter_mut().enumerate() {
2679 if !d.apply || opts.dry_run {
2680 continue;
2681 }
2682 match &d.action {
2683 PushAction::Update(src) => {
2684 let update = push_store_ref_update(
2685 d.result.remote_ref.as_str(),
2686 Some(*src),
2687 d.result.old_oid,
2688 );
2689 if opts.atomic {
2690 ref_batch.push(update);
2691 } else {
2692 non_atomic_batch.push((idx, update));
2693 }
2694 }
2695 PushAction::Delete => {
2696 let update =
2697 push_store_ref_update(d.result.remote_ref.as_str(), None, d.result.old_oid);
2698 if opts.atomic {
2699 ref_batch.push(update);
2700 } else {
2701 non_atomic_batch.push((idx, update));
2702 }
2703 }
2704 PushAction::None => {}
2705 }
2706 }
2707 if opts.atomic && !opts.dry_run && !ref_batch.is_empty() {
2708 if let Err(err) = crate::refs::commit_ref_store_batch(remote_git_dir, &ref_batch) {
2709 apply_atomic_batch_ref_store_error(&mut decisions, &ref_batch, err);
2710 }
2711 } else if !opts.dry_run && !non_atomic_batch.is_empty() {
2712 let indices: Vec<_> = non_atomic_batch.iter().map(|(idx, _)| *idx).collect();
2713 let updates: Vec<_> = non_atomic_batch.into_iter().map(|(_, u)| u).collect();
2714 match crate::refs::commit_ref_updates_non_atomic_on_store(remote_repo.refs(), updates) {
2715 Ok(outcomes) => {
2716 for (idx, outcome) in indices.into_iter().zip(outcomes) {
2717 if let Err(err) = outcome.result {
2718 apply_ref_store_error_to_push_decision(
2719 &mut decisions[idx],
2720 &Error::RefStore(err),
2721 false,
2722 );
2723 }
2724 }
2725 }
2726 Err(err) => {
2727 for idx in indices {
2728 apply_ref_store_error_to_push_decision(&mut decisions[idx], &err, false);
2729 }
2730 }
2731 }
2732 }
2733
2734 if !opts.dry_run {
2735 let applied: Vec<crate::serve::RefCommand> = decisions
2736 .iter()
2737 .filter(|d| d.apply)
2738 .filter_map(PushDecision::ref_command)
2739 .collect();
2740 if !applied.is_empty() {
2741 let applied: Vec<&crate::serve::RefCommand> = applied.iter().collect();
2742 relay_hook_output(
2743 progress,
2744 &mut remote_messages,
2745 &scripts.post_receive(&applied),
2746 );
2747 relay_hook_output(
2748 progress,
2749 &mut remote_messages,
2750 &scripts.post_update(&applied),
2751 );
2752 }
2753 }
2754
2755 let results: Vec<_> = decisions.into_iter().map(|d| d.result).collect();
2756 if !opts.dry_run {
2757 if let Some(remote) = opts.tracking_remote.as_deref() {
2758 crate::branch_tracking::apply_push_remote_tracking_updates_in(local, remote, &results)?;
2759 }
2760 }
2761
2762 drop(migrated_pack_keeps);
2763
2764 Ok(PushOutcome {
2765 results,
2766 remote_messages,
2767 })
2768}
2769
2770fn fail_atomic_push(decisions: &mut [PushDecision]) {
2773 for d in decisions {
2774 if matches!(d.result.status, PushRefStatus::Ok) {
2775 d.result.status = PushRefStatus::AtomicPushFailed;
2776 d.apply = false;
2777 }
2778 }
2779}
2780
2781fn relay_hook_output(
2785 progress: &mut dyn crate::fetch::Progress,
2786 messages: &mut Vec<String>,
2787 result: &crate::hooks::HookResult,
2788) {
2789 use crate::sideband_lines::{RemoteLine, SidebandLines};
2790
2791 let Some(output) = result.output() else {
2792 return;
2793 };
2794 let mut record = |line: RemoteLine| {
2795 if let RemoteLine::Line(text) = line {
2796 if !text.is_empty() {
2797 messages.push(text);
2798 }
2799 }
2800 };
2801 for stream in [&output.stdout, &output.stderr] {
2802 if !stream.is_empty() {
2803 progress.message(stream);
2804 }
2805 let mut lines = SidebandLines::new();
2806 lines.feed(stream, &mut record);
2807 lines.finish(&mut record);
2808 }
2809}
2810
2811fn push_policy_object_lookup(remote_git_dir: &Path, local_git_dir: &Path) -> Odb {
2818 Odb::new(&objects_dir(local_git_dir)).with_env_alternate_dirs(vec![objects_dir(remote_git_dir)])
2819}
2820
2821fn apply_remote_receive_policy(
2826 remote_repo: &crate::repo::Repository,
2827 local_git_dir: &Path,
2828 dry_run: bool,
2829 quarantine: Option<&Odb>,
2830 decisions: &mut [PushDecision],
2831) -> Result<()> {
2832 if !decisions.iter().any(|d| d.apply) {
2833 return Ok(());
2834 }
2835 let cfg = remote_repo
2836 .config()
2837 .unwrap_or_else(|_| std::sync::Arc::new(crate::config::ConfigSet::new()));
2838 let policy = crate::serve::ReceivePolicy::from_config(cfg.as_ref());
2839 let hidden_refs = crate::hide_refs::hide_ref_patterns_receive(cfg.as_ref());
2840 let head_branch = crate::serve::head_branch(remote_repo);
2841 let checked_out = crate::serve::checked_out_branch(remote_repo);
2842 let branches = crate::serve::HeadBranches {
2843 head: head_branch.as_deref(),
2844 checked_out: checked_out.as_deref(),
2845 };
2846 let pre_copy_lookup =
2847 dry_run.then(|| push_policy_object_lookup(remote_repo.git_dir(), local_git_dir));
2848 let object_lookup = pre_copy_lookup
2849 .as_ref()
2850 .or(quarantine)
2851 .unwrap_or(remote_repo.odb());
2852
2853 for d in decisions.iter_mut().filter(|d| d.apply) {
2854 let Some(cmd) = d.ref_command() else {
2855 continue;
2856 };
2857 let refusal = match crate::serve::validate_ref_update(
2858 remote_repo,
2859 Some(object_lookup),
2860 &cmd,
2861 &policy,
2862 &hidden_refs,
2863 branches,
2864 ) {
2865 Ok(r) => r,
2866 Err(crate::serve::ServeError::Repository(e)) => return Err(e),
2867 Err(e) => return Err(Error::Message(e.to_string())),
2868 };
2869 if let Some(reason) = refusal {
2870 d.result.status = match (&reason, cmd.old) {
2871 (crate::serve::RefUpdateRefusal::StaleInfo, None) => {
2872 PushRefStatus::RejectAlreadyExists
2873 }
2874 (crate::serve::RefUpdateRefusal::StaleInfo, Some(_)) => PushRefStatus::RejectStale,
2875 _ => PushRefStatus::RemoteRejected,
2876 };
2877 d.result.message = Some(match d.result.status {
2878 PushRefStatus::RejectAlreadyExists => "already exists".to_owned(),
2879 _ => reason.to_string(),
2880 });
2881 d.apply = false;
2882 d.action = PushAction::None;
2883 }
2884 }
2885 Ok(())
2886}
2887
2888fn push_store_ref_update(
2893 refname: &str,
2894 new_oid: Option<ObjectId>,
2895 observed_old: Option<ObjectId>,
2896) -> crate::refs::store::RefUpdate {
2897 crate::refs::store::RefUpdate {
2898 name: refname.to_owned(),
2899 new_value: new_oid.map(crate::refs::store::RawRef::Direct),
2900 expected: match observed_old {
2901 Some(oid) => crate::refs::store::Expected::Oid(oid),
2902 None => crate::refs::store::Expected::Missing,
2903 },
2904 reflog: None,
2905 flags: Default::default(),
2906 }
2907}
2908
2909fn push_status_for_ref_store_error(err: &Error) -> PushRefStatus {
2910 match err {
2911 Error::RefStore(crate::refs::store::RefStoreError::ExpectedMismatch {
2912 expected: crate::refs::store::Expected::Missing,
2913 ..
2914 }) => PushRefStatus::RejectAlreadyExists,
2915 Error::RefStore(crate::refs::store::RefStoreError::ExpectedMismatch { .. }) => {
2916 PushRefStatus::RejectStale
2917 }
2918 _ => PushRefStatus::RemoteRejected,
2919 }
2920}
2921
2922fn push_message_for_ref_store_error(err: &Error) -> String {
2924 match push_status_for_ref_store_error(err) {
2925 PushRefStatus::RejectAlreadyExists => "already exists".to_owned(),
2926 _ => err.to_string(),
2927 }
2928}
2929
2930fn ref_store_error_refname(err: &Error) -> Option<String> {
2931 let Error::RefStore(store_err) = err else {
2932 return None;
2933 };
2934 Some(match store_err {
2935 crate::refs::store::RefStoreError::ExpectedMismatch { name, .. }
2936 | crate::refs::store::RefStoreError::DuplicateUpdate { name }
2937 | crate::refs::store::RefStoreError::LockHeld { name }
2938 | crate::refs::store::RefStoreError::InvalidRefName { name }
2939 | crate::refs::store::RefStoreError::InvalidSymrefTarget { name, .. }
2940 | crate::refs::store::RefStoreError::NullOid { name }
2941 | crate::refs::store::RefStoreError::MissingObject { name, .. }
2942 | crate::refs::store::RefStoreError::NotACommit { name, .. }
2943 | crate::refs::store::RefStoreError::ObjectLookup { name, .. }
2944 | crate::refs::store::RefStoreError::NotFound { name }
2945 | crate::refs::store::RefStoreError::ReflogRewriteStale { name } => name.clone(),
2946 crate::refs::store::RefStoreError::NameUnavailable { reason } => match reason {
2947 crate::refs::RefnameUnavailable::SameBatch { refname, .. }
2948 | crate::refs::RefnameUnavailable::AncestorExists {
2949 new_ref: refname, ..
2950 }
2951 | crate::refs::RefnameUnavailable::DescendantExists {
2952 new_ref: refname, ..
2953 } => refname.clone(),
2954 },
2955 crate::refs::store::RefStoreError::SymrefLoop
2956 | crate::refs::store::RefStoreError::Corrupt(_)
2957 | crate::refs::store::RefStoreError::GitDirPathIo { .. }
2958 | crate::refs::store::RefStoreError::InvalidReflog(_)
2959 | crate::refs::store::RefStoreError::Backend(_)
2960 | crate::refs::store::RefStoreError::Hook(_) => return None,
2961 })
2962}
2963
2964fn apply_ref_store_error_to_push_decision(d: &mut PushDecision, err: &Error, _atomic: bool) {
2965 d.result.status = push_status_for_ref_store_error(err);
2966 d.result.message = Some(push_message_for_ref_store_error(err));
2967 d.apply = false;
2968}
2969
2970fn apply_atomic_batch_ref_store_error(
2971 decisions: &mut [PushDecision],
2972 batch: &[crate::refs::store::RefUpdate],
2973 err: Error,
2974) {
2975 let fail_ref = ref_store_error_refname(&err);
2976 let message = push_message_for_ref_store_error(&err);
2977 let batch_names: HashSet<&str> = batch.iter().map(|u| u.name.as_str()).collect();
2978 let in_batch = decisions
2979 .iter_mut()
2980 .filter(|d| d.apply && batch_names.contains(d.result.remote_ref.as_str()));
2981 if let Some(fail_name) = fail_ref {
2982 for d in in_batch {
2983 if d.result.remote_ref == fail_name {
2984 d.result.status = push_status_for_ref_store_error(&err);
2985 d.result.message = Some(message.clone());
2986 } else {
2987 d.result.status = PushRefStatus::AtomicPushFailed;
2988 d.result.message = Some("atomic push failed".to_owned());
2989 }
2990 d.apply = false;
2991 }
2992 } else {
2993 let mut first = true;
2994 for d in in_batch {
2995 if first {
2996 d.result.status = push_status_for_ref_store_error(&err);
2997 d.result.message = Some(message.clone());
2998 first = false;
2999 } else {
3000 d.result.status = PushRefStatus::AtomicPushFailed;
3001 d.result.message = Some("atomic push failed".to_owned());
3002 }
3003 d.apply = false;
3004 }
3005 }
3006}
3007
3008enum PushAction {
3010 Update(ObjectId),
3012 Delete,
3014 None,
3016}
3017
3018struct PushDecision {
3020 result: PushRefResult,
3021 action: PushAction,
3022 apply: bool,
3024}
3025
3026impl PushDecision {
3027 fn ref_command(&self) -> Option<crate::serve::RefCommand> {
3030 let new = match self.action {
3031 PushAction::Update(src) => Some(src),
3032 PushAction::Delete => None,
3033 PushAction::None => return None,
3034 };
3035 Some(crate::serve::RefCommand {
3036 refname: self.result.remote_ref.clone(),
3037 old: self.result.old_oid,
3038 new,
3039 })
3040 }
3041
3042 fn refuse(&mut self, reason: &crate::serve::RefUpdateRefusal) {
3044 self.result.status = PushRefStatus::RemoteRejected;
3045 self.result.message = Some(reason.to_string());
3046 self.apply = false;
3047 self.action = PushAction::None;
3048 }
3049}
3050
3051fn decide_push(
3053 spec: &PushRefSpec,
3054 local_odb: &Odb,
3055 remote_git_dir: &Path,
3056 local_repo: Option<&crate::repo::Repository>,
3057) -> Result<PushDecision> {
3058 let remote_current = crate::refs::resolve_ref(remote_git_dir, &spec.dst).ok();
3059
3060 if !spec.delete {
3066 if let Some(src) = spec.src {
3067 if remote_current == Some(src) {
3068 return Ok(PushDecision {
3069 result: PushRefResult {
3070 local_ref: push_spec_local_ref(spec),
3071 remote_ref: spec.dst.clone(),
3072 old_oid: remote_current,
3073 new_oid: Some(src),
3074 forced: false,
3075 deletion: false,
3076 status: PushRefStatus::UpToDate,
3077 message: None,
3078 },
3079 action: PushAction::None,
3080 apply: false,
3081 });
3082 }
3083 }
3084 }
3085
3086 if spec.expect_absent && remote_current.is_some() {
3090 return Ok(PushDecision {
3091 result: PushRefResult {
3092 local_ref: push_spec_local_ref(spec),
3093 remote_ref: spec.dst.clone(),
3094 old_oid: remote_current,
3095 new_oid: spec.src,
3096 forced: false,
3097 deletion: spec.delete,
3098 status: PushRefStatus::RejectStale,
3099 message: Some("stale info".to_owned()),
3100 },
3101 action: PushAction::None,
3102 apply: false,
3103 });
3104 }
3105
3106 if let Some(expected) = spec.expected_old {
3110 if remote_current != Some(expected) {
3111 return Ok(PushDecision {
3112 result: PushRefResult {
3113 local_ref: None,
3114 remote_ref: spec.dst.clone(),
3115 old_oid: remote_current,
3116 new_oid: spec.src,
3117 forced: false,
3118 deletion: spec.delete,
3119 status: PushRefStatus::RejectStale,
3120 message: Some("stale info".to_owned()),
3121 },
3122 action: PushAction::None,
3123 apply: false,
3124 });
3125 }
3126 }
3127
3128 if spec.delete {
3129 let (status, action, apply, message) = match remote_current {
3130 Some(_) => (PushRefStatus::Ok, PushAction::Delete, true, None),
3131 None => (
3132 PushRefStatus::RejectRemoteRefMissing,
3133 PushAction::None,
3134 false,
3135 Some("remote ref does not exist".to_owned()),
3136 ),
3137 };
3138 return Ok(PushDecision {
3139 result: PushRefResult {
3140 local_ref: push_spec_local_ref(spec),
3141 remote_ref: spec.dst.clone(),
3142 old_oid: remote_current,
3143 new_oid: None,
3144 forced: false,
3145 deletion: true,
3146 status,
3147 message,
3148 },
3149 action,
3150 apply,
3151 });
3152 }
3153
3154 let Some(src) = spec.src else {
3156 return Err(Error::Message(format!(
3157 "push to '{}' has no source object and is not a deletion",
3158 spec.dst
3159 )));
3160 };
3161 if !local_odb.contains(&src)? {
3162 return Err(Error::Message(format!(
3163 "source object {src} for '{}' is missing from the local object store",
3164 spec.dst
3165 )));
3166 }
3167
3168 if remote_current == Some(src) {
3170 return Ok(PushDecision {
3171 result: PushRefResult {
3172 local_ref: push_spec_local_ref(spec),
3173 remote_ref: spec.dst.clone(),
3174 old_oid: remote_current,
3175 new_oid: Some(src),
3176 forced: false,
3177 deletion: false,
3178 status: PushRefStatus::UpToDate,
3179 message: None,
3180 },
3181 action: PushAction::None,
3182 apply: false,
3183 });
3184 }
3185
3186 let Some(old) = remote_current else {
3188 return Ok(PushDecision {
3189 result: PushRefResult {
3190 local_ref: push_spec_local_ref(spec),
3191 remote_ref: spec.dst.clone(),
3192 old_oid: None,
3193 new_oid: Some(src),
3194 forced: false,
3195 deletion: false,
3196 status: PushRefStatus::Ok,
3197 message: None,
3198 },
3199 action: PushAction::Update(src),
3200 apply: true,
3201 });
3202 };
3203
3204 if let Some(tag_decision) = existing_tag_push_decision(spec, &spec.dst, old, src) {
3205 return Ok(match tag_decision {
3206 ExistingTagPushDecision::Reject => PushDecision {
3207 result: PushRefResult {
3208 local_ref: push_spec_local_ref(spec),
3209 remote_ref: spec.dst.clone(),
3210 old_oid: Some(old),
3211 new_oid: Some(src),
3212 forced: false,
3213 deletion: false,
3214 status: PushRefStatus::RejectAlreadyExists,
3215 message: Some(PUSH_TAG_EXISTS_MESSAGE.to_owned()),
3216 },
3217 action: PushAction::None,
3218 apply: false,
3219 },
3220 ExistingTagPushDecision::ForcedUpdate => PushDecision {
3221 result: PushRefResult {
3222 local_ref: push_spec_local_ref(spec),
3223 remote_ref: spec.dst.clone(),
3224 old_oid: Some(old),
3225 new_oid: Some(src),
3226 forced: true,
3227 deletion: false,
3228 status: PushRefStatus::Ok,
3229 message: None,
3230 },
3231 action: PushAction::Update(src),
3232 apply: true,
3233 },
3234 });
3235 }
3236
3237 let is_ff = local_repo
3241 .map(|r| crate::merge_base::is_ancestor(r, old, src).unwrap_or(false))
3242 .unwrap_or(false);
3243
3244 if is_ff {
3245 Ok(PushDecision {
3246 result: PushRefResult {
3247 local_ref: push_spec_local_ref(spec),
3248 remote_ref: spec.dst.clone(),
3249 old_oid: Some(old),
3250 new_oid: Some(src),
3251 forced: false,
3252 deletion: false,
3253 status: PushRefStatus::Ok,
3254 message: None,
3255 },
3256 action: PushAction::Update(src),
3257 apply: true,
3258 })
3259 } else if spec.force {
3260 Ok(PushDecision {
3261 result: PushRefResult {
3262 local_ref: push_spec_local_ref(spec),
3263 remote_ref: spec.dst.clone(),
3264 old_oid: Some(old),
3265 new_oid: Some(src),
3266 forced: true,
3267 deletion: false,
3268 status: PushRefStatus::Ok,
3269 message: None,
3270 },
3271 action: PushAction::Update(src),
3272 apply: true,
3273 })
3274 } else {
3275 let status = push_reject_status_when_not_fast_forward(local_repo, old, src);
3276 Ok(PushDecision {
3277 result: PushRefResult {
3278 local_ref: push_spec_local_ref(spec),
3279 remote_ref: spec.dst.clone(),
3280 old_oid: Some(old),
3281 new_oid: Some(src),
3282 forced: false,
3283 deletion: false,
3284 status,
3285 message: Some("non-fast-forward".to_owned()),
3286 },
3287 action: PushAction::None,
3288 apply: false,
3289 })
3290 }
3291}
3292
3293fn push_reject_status_when_not_fast_forward(
3296 local_repo: Option<&crate::repo::Repository>,
3297 old: crate::objects::ObjectId,
3298 src: crate::objects::ObjectId,
3299) -> PushRefStatus {
3300 let remote_ahead = local_repo
3301 .map(|r| crate::merge_base::is_ancestor(r, src, old).unwrap_or(false))
3302 .unwrap_or(false);
3303 if remote_ahead {
3304 PushRefStatus::RejectNonFastForward
3305 } else {
3306 PushRefStatus::RejectNeedsForce
3307 }
3308}
3309
3310pub(crate) struct MatchedRef {
3312 pub(crate) remote_ref: String,
3313 pub(crate) local_ref: Option<String>,
3315 pub(crate) oid: ObjectId,
3316 pub(crate) force: bool,
3317 pub(crate) is_tag: bool,
3318 pub(crate) auto_followed: bool,
3322 pub(crate) dest_is_tag: bool,
3324 pub(crate) advertised_peel: Option<ObjectId>,
3326 pub(crate) fetch_head: crate::fetch_request::FetchHeadPolicy,
3328}
3329
3330fn objects_dir(git_dir: &Path) -> PathBuf {
3335 crate::repo::common_git_dir_for_config(git_dir).join("objects")
3336}
3337
3338pub(crate) fn open_odb(git_dir: &Path) -> Odb {
3339 Odb::new(&objects_dir(git_dir)).with_config_git_dir(git_dir.to_path_buf())
3340}
3341
3342pub(crate) fn remote_hash_algo_from_advertisement(
3344 capability_blocks: &[&[String]],
3345 remote_refs: &[(String, ObjectId)],
3346) -> HashAlgo {
3347 for caps in capability_blocks {
3348 for c in *caps {
3349 if let Some(name) = c.strip_prefix("object-format=") {
3350 if let Some(algo) = HashAlgo::from_name(name) {
3351 return algo;
3352 }
3353 }
3354 }
3355 }
3356 remote_refs
3357 .iter()
3358 .find_map(|(_, oid)| HashAlgo::from_len(oid.as_bytes().len()))
3359 .unwrap_or(HashAlgo::Sha1)
3360}
3361
3362pub(crate) fn sync_empty_destination_object_format(
3366 local_git_dir: &Path,
3367 remote_algo: HashAlgo,
3368 opts: &FetchOptions,
3369) -> Result<bool> {
3370 if opts.dry_run || !opts.initial_remote_fetch {
3371 return Ok(false);
3372 }
3373 let local_odb = open_odb(local_git_dir);
3374 if local_odb.hash_algo() == remote_algo {
3375 return Ok(false);
3376 }
3377 if local_odb.approximate_object_count()? > 0 {
3378 return Err(Error::Message(format!(
3379 "unsupported object format: destination uses {} but remote uses {}; re-clone into an empty directory with a matching format",
3380 local_odb.hash_algo().name(),
3381 remote_algo.name()
3382 )));
3383 }
3384 write_empty_destination_object_format(local_git_dir, remote_algo)?;
3385 Ok(true)
3386}
3387
3388pub(crate) fn reopen_after_format_change(
3391 local: &crate::repo::Repository,
3392) -> Result<crate::repo::Repository> {
3393 crate::repo::Repository::open(
3394 local.git_dir(),
3395 &RepositoryOptions::with_environment(local.environment().clone()),
3396 )
3397}
3398
3399fn write_empty_destination_object_format(git_dir: &Path, algo: HashAlgo) -> Result<()> {
3400 use crate::config::{ConfigFile, ConfigScope};
3401 let config_path = crate::repo::local_config_write_path(git_dir);
3402 let content = fs::read_to_string(&config_path).unwrap_or_default();
3403 let mut file = ConfigFile::parse(&config_path, &content, ConfigScope::Local)?;
3404 if algo == HashAlgo::Sha1 {
3405 file.unset("extensions.objectformat")?;
3406 file.set("core.repositoryformatversion", "0")?;
3407 } else {
3408 file.set("core.repositoryformatversion", "1")?;
3409 file.set("extensions.objectformat", algo.name())?;
3410 }
3411 file.write()?;
3412 Ok(())
3413}
3414
3415fn config_for_git_dir(git_dir: &Path) -> Option<crate::config::ConfigSet> {
3416 crate::config::ConfigSet::load(
3417 &crate::environment::Environment::empty(),
3418 Some(git_dir),
3419 true,
3420 )
3421 .ok()
3422}
3423
3424pub(crate) fn finish_anonymous_url_fetch(
3426 git_dir: &Path,
3427 opts: &FetchOptions,
3428 explicit_refspec_fetch: bool,
3429 default_branch: Option<&str>,
3430 remote_refs: &[(String, ObjectId)],
3431 matched: &[MatchedRef],
3432) -> Result<()> {
3433 if opts.dry_run {
3434 return Ok(());
3435 }
3436 let write_fetch_head = opts.remote_name.is_none() || explicit_refspec_fetch;
3437 if !write_fetch_head {
3438 return Ok(());
3439 }
3440 let source = opts.fetch_source.clone().or_else(|| {
3441 let name = opts.remote_name.as_deref()?;
3442 let config = config_for_git_dir(git_dir)?;
3443 config
3444 .get(&format!("remote.{name}.url"))
3445 .filter(|url| !url.trim().is_empty())
3446 });
3447 let Some(source) = source.as_deref() else {
3448 return Ok(());
3449 };
3450 let lines = fetch_head_lines_for_ad_hoc_fetch(
3451 matched,
3452 remote_refs,
3453 &opts.refspecs,
3454 default_branch,
3455 source,
3456 );
3457 if lines.is_empty() {
3458 return Ok(());
3459 }
3460 crate::fetch_head::write_fetch_head(git_dir, &lines)
3461}
3462
3463fn fetch_head_lines_for_ad_hoc_fetch(
3464 matched: &[MatchedRef],
3465 remote_refs: &[(String, ObjectId)],
3466 refspecs: &[String],
3467 default_branch: Option<&str>,
3468 source: &str,
3469) -> Vec<crate::fetch_head::FetchHeadLine> {
3470 use crate::fetch_head::FetchHeadLine;
3471 use crate::remote::Remote;
3472
3473 let anonymous_only = refspecs.len() == 1
3474 && refspecs[0].trim_start_matches('+') == Remote::ANONYMOUS_FETCH_REFSPEC;
3475
3476 if anonymous_only {
3477 if let Some(branch) = default_branch {
3478 let head_ref = format!("refs/heads/{branch}");
3479 if let Some(m) = matched
3480 .iter()
3481 .find(|m| m.remote_ref == head_ref && !m.is_tag)
3482 {
3483 return vec![FetchHeadLine {
3484 oid: m.oid,
3485 for_merge: true,
3486 description: source.to_owned(),
3487 }];
3488 }
3489 }
3490 if let Some((_, oid)) = remote_refs.iter().find(|(name, _)| name == "HEAD") {
3491 return vec![FetchHeadLine {
3492 oid: *oid,
3493 for_merge: true,
3494 description: source.to_owned(),
3495 }];
3496 }
3497 return Vec::new();
3498 }
3499
3500 matched
3501 .iter()
3502 .filter(|m| m.fetch_head == crate::fetch_request::FetchHeadPolicy::ForMerge)
3503 .map(|m| FetchHeadLine {
3504 oid: m.oid,
3505 for_merge: true,
3506 description: crate::fetch_request::fetch_head_description(
3507 &m.remote_ref,
3508 m.is_tag,
3509 source,
3510 ),
3511 })
3512 .collect()
3513}
3514
3515#[must_use]
3517pub(crate) fn remote_head_oid_from_advertised(
3518 advertised: &[(String, ObjectId)],
3519) -> Option<ObjectId> {
3520 advertised
3521 .iter()
3522 .find(|(name, _)| name == "HEAD")
3523 .map(|(_, oid)| *oid)
3524}
3525
3526pub(crate) fn match_positive(refname: &str, positive: &[RefspecItem]) -> Option<Option<String>> {
3531 for item in positive {
3532 let Some(src) = item.src.as_deref() else {
3533 continue;
3534 };
3535 if let Some(dst) = apply_refspec(src, item.dst.as_deref(), refname) {
3536 if dst.is_empty() {
3538 return Some(None);
3539 }
3540 return Some(Some(dst));
3541 }
3542 }
3543 None
3544}
3545
3546pub(crate) fn refspecs_force(refname: &str, positive: &[RefspecItem]) -> bool {
3548 positive.iter().any(|item| {
3549 item.force
3550 && item
3551 .src
3552 .as_deref()
3553 .is_some_and(|src| apply_refspec(src, item.dst.as_deref(), refname).is_some())
3554 })
3555}
3556
3557pub(crate) fn ref_excluded(refname: &str, negatives: &[RefspecItem]) -> bool {
3559 negatives.iter().any(|item| {
3560 item.src
3561 .as_deref()
3562 .is_some_and(|src| glob_matches(src, refname))
3563 })
3564}
3565
3566pub(crate) fn apply_refspec(src: &str, dst: Option<&str>, refname: &str) -> Option<String> {
3572 match src.find('*') {
3573 Some(star) => {
3574 let prefix = &src[..star];
3575 let suffix = &src[star + 1..];
3576 if !refname.starts_with(prefix)
3577 || !refname.ends_with(suffix)
3578 || refname.len() < prefix.len() + suffix.len()
3579 {
3580 return None;
3581 }
3582 let middle = &refname[prefix.len()..refname.len() - suffix.len()];
3583 match dst {
3584 None => Some(refname.to_owned()),
3585 Some("") => Some(String::new()),
3586 Some(d) => Some(d.replacen('*', middle, 1)),
3587 }
3588 }
3589 None => {
3590 if src != refname {
3591 return None;
3592 }
3593 match dst {
3594 None => Some(refname.to_owned()),
3595 Some("") => Some(String::new()),
3596 Some(d) => Some(d.to_owned()),
3597 }
3598 }
3599 }
3600}
3601
3602fn glob_matches(pattern: &str, refname: &str) -> bool {
3604 match pattern.find('*') {
3605 Some(star) => {
3606 let prefix = &pattern[..star];
3607 let suffix = &pattern[star + 1..];
3608 refname.starts_with(prefix)
3609 && refname.ends_with(suffix)
3610 && refname.len() >= prefix.len() + suffix.len()
3611 }
3612 None => pattern == refname,
3613 }
3614}
3615
3616#[allow(clippy::too_many_arguments)]
3622pub(crate) fn apply_tag_mode(
3623 mode: TagMode,
3624 remote_refs: &[(String, ObjectId)],
3625 remote_odb: &Odb,
3626 shallow_boundaries: &HashSet<ObjectId>,
3627 negatives: &[RefspecItem],
3628 already_local: &HashSet<String>,
3629 force_all_tags: bool,
3630 matched: &mut Vec<MatchedRef>,
3631 matched_oids: &mut HashSet<ObjectId>,
3632 seen_remote_ref: &mut HashSet<String>,
3633) -> Result<HashSet<ObjectId>> {
3634 let mut following_only = HashSet::new();
3635 if mode == TagMode::None {
3636 return Ok(following_only);
3637 }
3638
3639 let candidates: Vec<(&String, ObjectId, Option<ObjectId>)> = remote_refs
3642 .iter()
3643 .filter(|(name, _)| {
3644 name.starts_with("refs/tags/")
3645 && !seen_remote_ref.contains(name)
3646 && !ref_excluded(name, negatives)
3647 && !(mode == TagMode::Following && already_local.contains(name))
3648 })
3649 .map(|(name, oid)| {
3650 let peeled = match mode {
3651 TagMode::Following => remote_odb.peel_tags(*oid).ok().map(|(p, _)| p),
3652 _ => None,
3653 };
3654 (name, *oid, peeled)
3655 })
3656 .collect();
3657
3658 let following_closure: HashSet<ObjectId> = if mode == TagMode::Following {
3663 let roots: Vec<ObjectId> = matched
3664 .iter()
3665 .filter(|m| !m.is_tag)
3666 .map(|m| m.oid)
3667 .collect();
3668 let targets: HashSet<ObjectId> = candidates
3669 .iter()
3670 .map(|(_, oid, peeled)| peeled.unwrap_or(*oid))
3671 .collect();
3672 crate::fetch::reachable_commits(remote_odb, &roots, &targets, shallow_boundaries)?
3673 } else {
3674 HashSet::new()
3675 };
3676
3677 for (name, oid, peeled) in candidates {
3678 let keep = match mode {
3679 TagMode::All => true,
3680 TagMode::Following => {
3682 following_closure.contains(&oid)
3683 || peeled.is_some_and(|p| following_closure.contains(&p))
3684 }
3685 TagMode::None => false,
3686 };
3687 if keep && seen_remote_ref.insert(name.clone()) {
3688 if matched_oids.insert(oid) && mode == TagMode::Following {
3689 following_only.insert(oid);
3690 }
3691 matched.push(MatchedRef {
3692 remote_ref: name.clone(),
3693 local_ref: Some(name.clone()),
3694 oid,
3695 force: force_all_tags && mode == TagMode::All,
3696 is_tag: true,
3697 auto_followed: mode == TagMode::Following,
3698 dest_is_tag: true,
3699 advertised_peel: peeled,
3700 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
3701 });
3702 }
3703 }
3704 Ok(following_only)
3705}
3706
3707fn shallow_want_commit_tip(remote_odb: &Odb, matched: &MatchedRef) -> Result<ObjectId> {
3709 if !matched.is_tag {
3710 return Ok(matched.oid);
3711 }
3712 if let Some(peel) = matched.advertised_peel.filter(|p| !p.is_zero()) {
3713 return Ok(peel);
3714 }
3715 if remote_odb.contains(&matched.oid)? {
3716 let (peeled, _) = remote_odb.peel_tags(matched.oid)?;
3717 return Ok(peeled);
3718 }
3719 Ok(matched.oid)
3720}
3721
3722fn ref_target_exists(local_odb: &Odb, remote_odb: &Odb, tag_oid: ObjectId) -> Result<bool> {
3725 let peel_odb = if local_odb.contains(&tag_oid)? {
3726 local_odb
3727 } else {
3728 remote_odb
3729 };
3730 match peel_odb.peel_tags(tag_oid) {
3731 Ok((peeled, _)) => local_odb.contains(&peeled),
3732 Err(_) => Ok(false),
3733 }
3734}
3735
3736fn tag_refs_with_local_targets(
3743 local_odb: &Odb,
3744 remote_odb: &Odb,
3745 tag_oids: impl IntoIterator<Item = ObjectId>,
3746 copied: &HashSet<ObjectId>,
3747) -> Result<HashSet<ObjectId>> {
3748 let mut ok = HashSet::new();
3749 let mut pending: Vec<(ObjectId, ObjectId)> = Vec::new();
3750
3751 for tag_oid in tag_oids {
3752 let Ok((peeled, _)) = remote_odb.peel_tags(tag_oid) else {
3753 continue;
3754 };
3755 let peeled_copied = copied.contains(&peeled);
3756 let tag_copied = tag_oid == peeled || copied.contains(&tag_oid);
3757 if peeled_copied && tag_copied {
3758 ok.insert(tag_oid);
3759 continue;
3760 }
3761 if local_odb.uses_files_primary() {
3762 if ref_target_exists(local_odb, remote_odb, tag_oid)? {
3763 ok.insert(tag_oid);
3764 }
3765 continue;
3766 }
3767 pending.push((tag_oid, peeled));
3768 }
3769
3770 if pending.is_empty() {
3771 return Ok(ok);
3772 }
3773
3774 let mut probe_ids = Vec::new();
3775 for (tag_oid, peeled) in &pending {
3776 if !copied.contains(peeled) {
3777 probe_ids.push(*peeled);
3778 }
3779 if *tag_oid != *peeled && !copied.contains(tag_oid) {
3780 probe_ids.push(*tag_oid);
3781 }
3782 }
3783 probe_ids.sort_unstable();
3784 probe_ids.dedup();
3785 let missing: HashSet<ObjectId> = if probe_ids.is_empty() {
3786 HashSet::new()
3787 } else {
3788 local_odb
3789 .missing_materialized_many(&probe_ids)?
3790 .into_iter()
3791 .collect()
3792 };
3793 for (tag_oid, peeled) in pending {
3794 let peeled_ok = copied.contains(&peeled) || !missing.contains(&peeled);
3795 let tag_ok = tag_oid == peeled || copied.contains(&tag_oid) || !missing.contains(&tag_oid);
3796 if peeled_ok && tag_ok {
3797 ok.insert(tag_oid);
3798 }
3799 }
3800 Ok(ok)
3801}
3802
3803pub(crate) const PUSH_TAG_EXISTS_MESSAGE: &str = "tag already exists";
3806
3807pub(crate) enum ExistingTagPushDecision {
3809 Reject,
3810 ForcedUpdate,
3811}
3812
3813pub(crate) fn existing_tag_push_decision(
3816 spec: &PushRefSpec,
3817 dst: &str,
3818 old: ObjectId,
3819 src: ObjectId,
3820) -> Option<ExistingTagPushDecision> {
3821 if spec.delete || !dst.starts_with("refs/tags/") || old == src {
3822 return None;
3823 }
3824 Some(if spec.force {
3825 ExistingTagPushDecision::ForcedUpdate
3826 } else {
3827 ExistingTagPushDecision::Reject
3828 })
3829}
3830
3831pub(crate) fn classify_update(
3837 old: Option<&ObjectId>,
3838 new: &ObjectId,
3839 force: bool,
3840 is_tag: bool,
3841 repo: Option<&crate::repo::Repository>,
3842 classify_odb: Option<&crate::odb::Odb>,
3843) -> UpdateMode {
3844 let Some(old) = old else {
3845 return UpdateMode::New;
3846 };
3847 if old == new {
3848 return UpdateMode::UpToDate;
3849 }
3850 let ff = if let Some(r) = repo {
3852 crate::merge_base::is_ancestor(r, *old, *new).unwrap_or(false)
3853 } else if let Some(odb) = classify_odb {
3854 crate::merge_base::is_ancestor_odb_walk(odb, *old, *new).unwrap_or(false)
3855 } else {
3856 false
3857 };
3858 if ff && !is_tag {
3859 return UpdateMode::FastForward;
3860 }
3861 if force {
3862 return UpdateMode::Forced;
3863 }
3864 if is_tag {
3865 return UpdateMode::TagUpdateRejected;
3866 }
3867 UpdateMode::NonFastForwardRejected
3868}
3869
3870fn is_default_head_under_tracking_prefix(refname: &str, tracking_prefix: &str) -> bool {
3874 refname == format!("{tracking_prefix}HEAD")
3875}
3876
3877pub fn tracking_refs_gone_on_remote(
3886 local_git_dir: &Path,
3887 positive: &[RefspecItem],
3888 remote_refs: &[(String, ObjectId)],
3889) -> Result<Vec<(String, ObjectId)>> {
3890 tracking_refs_gone_with(
3891 positive,
3892 remote_refs,
3893 |prefix| crate::refs::list_refs(local_git_dir, prefix),
3894 |name| crate::refs::resolve_ref(local_git_dir, name),
3895 )
3896}
3897
3898pub(crate) fn tracking_refs_gone_in(
3905 local: &crate::repo::Repository,
3906 positive: &[RefspecItem],
3907 remote_refs: &[(String, ObjectId)],
3908) -> Result<Vec<(String, ObjectId)>> {
3909 tracking_refs_gone_with(
3910 positive,
3911 remote_refs,
3912 |prefix| crate::refs::list_refs_in(local, prefix),
3913 |name| crate::refs::resolve_ref_in(local, name),
3914 )
3915}
3916
3917fn tracking_refs_gone_with(
3918 positive: &[RefspecItem],
3919 remote_refs: &[(String, ObjectId)],
3920 list: impl Fn(&str) -> Result<Vec<(String, ObjectId)>>,
3921 resolve: impl Fn(&str) -> Result<ObjectId>,
3922) -> Result<Vec<(String, ObjectId)>> {
3923 let mut live: HashSet<String> = HashSet::new();
3924 for (name, _) in remote_refs {
3925 if let Some(Some(dst)) = match_positive(name, positive) {
3926 live.insert(dst);
3927 }
3928 }
3929
3930 let mut pruned: HashMap<String, ObjectId> = HashMap::new();
3931 for item in positive {
3932 let Some(dst) = item.dst.as_deref() else {
3933 continue;
3934 };
3935 if let Some(star) = dst.find('*') {
3936 let prefix = &dst[..star];
3937 for (name, oid) in list(prefix)? {
3938 if !name.starts_with(prefix) {
3939 continue;
3940 }
3941 if is_default_head_under_tracking_prefix(&name, prefix) {
3942 continue;
3943 }
3944 if !live.contains(&name) {
3945 pruned.entry(name).or_insert(oid);
3946 }
3947 }
3948 } else if !live.contains(dst) {
3949 let exact_prefix = dst.strip_suffix("HEAD").filter(|p| p.ends_with('/'));
3950 if exact_prefix.is_some_and(|p| is_default_head_under_tracking_prefix(dst, p)) {
3951 continue;
3952 }
3953 if let Ok(oid) = resolve(dst) {
3954 pruned.entry(dst.to_owned()).or_insert(oid);
3955 }
3956 }
3957 }
3958 Ok(pruned.into_iter().collect())
3959}
3960
3961pub(crate) struct FetchRefRejectHint<'a> {
3963 pub prune: bool,
3965 pub positive: &'a [RefspecItem],
3967 pub remote_refs: &'a [(String, ObjectId)],
3969 pub remote_name: Option<&'a str>,
3971}
3972
3973fn fetch_ref_store_error_rejectable(err: &Error) -> bool {
3974 matches!(
3975 err,
3976 Error::RefStore(crate::refs::store::RefStoreError::NameUnavailable { .. })
3977 | Error::RefStore(crate::refs::store::RefStoreError::LockHeld { .. })
3978 )
3979}
3980
3981fn blocking_ref_from_unavailable(reason: &crate::refs::RefnameUnavailable) -> Option<&str> {
3982 match reason {
3983 crate::refs::RefnameUnavailable::AncestorExists { blocking, .. }
3984 | crate::refs::RefnameUnavailable::DescendantExists { blocking, .. } => Some(blocking),
3985 crate::refs::RefnameUnavailable::SameBatch { .. } => None,
3986 }
3987}
3988
3989fn append_fetch_prune_hint(
3990 note: &mut String,
3991 stale_tracking: &HashSet<String>,
3992 blocking: &str,
3993 remote_name: Option<&str>,
3994) {
3995 if stale_tracking.contains(blocking) {
3996 let remote = remote_name.unwrap_or("origin");
3997 note.push_str(&format!(
3998 " — stale remote-tracking ref; try `grit fetch --prune` or `grit remote prune {remote}`"
3999 ));
4000 }
4001}
4002
4003fn mark_fetch_ref_update_rejected(
4004 updates: &mut [RefUpdate],
4005 local_ref: &str,
4006 err: &Error,
4007 stale_tracking: &HashSet<String>,
4008 hint: &FetchRefRejectHint<'_>,
4009) {
4010 let Some(update) = updates
4011 .iter_mut()
4012 .find(|u| u.local_ref.as_deref() == Some(local_ref))
4013 else {
4014 return;
4015 };
4016 update.mode = UpdateMode::RefLockRejected;
4017 let mut note = match err {
4018 Error::RefStore(crate::refs::store::RefStoreError::NameUnavailable { reason }) => {
4019 let mut text = reason.lock_message_suffix();
4020 if let Some(blocking) = blocking_ref_from_unavailable(reason) {
4021 append_fetch_prune_hint(&mut text, stale_tracking, blocking, hint.remote_name);
4022 }
4023 text
4024 }
4025 Error::RefStore(crate::refs::store::RefStoreError::LockHeld { name }) => {
4026 format!("ref '{name}' is locked")
4027 }
4028 _ => err.to_string(),
4029 };
4030 if note.is_empty() {
4031 note = "unable to update local ref".to_owned();
4032 }
4033 update.note = Some(note);
4034}
4035
4036pub(crate) fn apply_fetch_ref_updates(
4057 local: &crate::repo::Repository,
4058 writes: Vec<(String, crate::refs::store::RawRef)>,
4059 updates: &mut [RefUpdate],
4060 hint: FetchRefRejectHint<'_>,
4061 initial: bool,
4062 fetch_reflog: Option<&FetchApplyReflog<'_>>,
4063) -> Result<()> {
4064 use crate::refs::store::{RawRef, ReflogUpdate};
4065 use std::collections::HashMap;
4066
4067 let store = local.refs();
4068 let config = local.config()?;
4069 let null_oid = store.reference_transaction_null_oid();
4070 let mode_by_local: HashMap<String, (UpdateMode, Option<ObjectId>, Option<ObjectId>)> = updates
4071 .iter()
4072 .filter_map(|u| {
4073 let local_ref = u.local_ref.as_ref()?;
4074 Some((local_ref.clone(), (u.mode, u.old_oid, u.new_oid)))
4075 })
4076 .collect();
4077
4078 let store_update = |(name, value): &(String, RawRef)| -> Result<crate::refs::store::RefUpdate> {
4079 let reflog = if initial {
4080 None
4081 } else if let Some(ctx) = fetch_reflog {
4082 let Some((mode, old_oid, new_oid)) = mode_by_local.get(name).copied() else {
4083 return Ok(crate::refs::store::RefUpdate {
4084 name: name.clone(),
4085 new_value: Some(value.clone()),
4086 expected: crate::refs::store::Expected::Any,
4087 reflog: None,
4088 flags: Default::default(),
4089 });
4090 };
4091 let Some(reason) = mode.fetch_reflog_reason() else {
4092 return Ok(crate::refs::store::RefUpdate {
4093 name: name.clone(),
4094 new_value: Some(value.clone()),
4095 expected: crate::refs::store::Expected::Any,
4096 reflog: None,
4097 flags: Default::default(),
4098 });
4099 };
4100 if !crate::refs::reflog_wanted(store, &config, name)? {
4101 return Ok(crate::refs::store::RefUpdate {
4102 name: name.clone(),
4103 new_value: Some(value.clone()),
4104 expected: crate::refs::store::Expected::Any,
4105 reflog: None,
4106 flags: Default::default(),
4107 });
4108 }
4109 let old = old_oid.unwrap_or(null_oid);
4110 let new = match value {
4111 RawRef::Direct(oid) => *oid,
4112 RawRef::Symbolic(_) => new_oid.unwrap_or(null_oid),
4113 };
4114 let message = format!("{}: {reason}", ctx.action);
4115 Some(ReflogUpdate {
4116 identity: ctx.identity.to_owned(),
4117 message,
4118 time: time::OffsetDateTime::UNIX_EPOCH,
4119 old_oid: Some(old),
4120 new_oid: Some(new),
4121 })
4122 } else {
4123 None
4124 };
4125 Ok(crate::refs::store::RefUpdate {
4126 name: name.clone(),
4127 new_value: Some(value.clone()),
4128 expected: crate::refs::store::Expected::Any,
4129 reflog,
4130 flags: Default::default(),
4131 })
4132 };
4133
4134 let mut pending: Vec<&(String, RawRef)> = writes.iter().collect();
4135 if initial {
4136 let (direct, rest): (Vec<_>, Vec<_>) = pending
4137 .iter()
4138 .copied()
4139 .partition(|(_, value)| matches!(value, RawRef::Direct(_)));
4140 let direct = direct
4141 .into_iter()
4142 .map(&store_update)
4143 .collect::<Result<Vec<_>>>()?;
4144 if crate::refs::commit_initial_ref_updates_on_store(store, direct).is_ok() {
4145 pending = rest;
4146 }
4147 }
4148 let batch = pending
4149 .iter()
4150 .copied()
4151 .map(&store_update)
4152 .collect::<Result<Vec<_>>>()?;
4153 if pending.is_empty() || crate::refs::commit_owned_ref_updates_on_store(store, batch).is_ok() {
4154 return Ok(());
4155 }
4156
4157 let stale_tracking: HashSet<String> = if hint.prune {
4158 HashSet::new()
4159 } else {
4160 tracking_refs_gone_in(local, hint.positive, hint.remote_refs)?
4161 .into_iter()
4162 .map(|(name, _)| name)
4163 .collect()
4164 };
4165
4166 for write in pending {
4167 match crate::refs::commit_ref_update_on_store(store, store_update(write)?) {
4168 Ok(()) => {}
4169 Err(err) if fetch_ref_store_error_rejectable(&err) => {
4170 mark_fetch_ref_update_rejected(updates, &write.0, &err, &stale_tracking, &hint);
4171 }
4172 Err(err) => return Err(err),
4173 }
4174 }
4175 Ok(())
4176}
4177
4178pub(crate) fn prune_tracking_refs(
4191 local: &crate::repo::Repository,
4192 positive: &[RefspecItem],
4193 remote_refs: &[(String, ObjectId)],
4194 dry_run: bool,
4195 updates: &mut Vec<RefUpdate>,
4196) -> Result<()> {
4197 let pruned: Vec<(String, ObjectId)> = tracking_refs_gone_in(local, positive, remote_refs)?;
4198
4199 let prune_delete_flags = crate::refs::store::RefUpdateFlags {
4200 no_deref: true,
4201 ..Default::default()
4202 };
4203 let mut batch: Vec<crate::refs::store::RefUpdate> = Vec::new();
4204 let mut pruned_names: Vec<String> = Vec::new();
4205 for (name, oid) in pruned.into_iter() {
4206 pruned_names.push(name.clone());
4207 if !dry_run {
4208 batch.push(crate::refs::store::RefUpdate {
4209 name: name.clone(),
4210 new_value: None,
4211 expected: crate::refs::store::Expected::Any,
4212 reflog: None,
4213 flags: prune_delete_flags,
4214 });
4215 }
4216 updates.push(RefUpdate {
4217 remote_ref: String::new(),
4218 local_ref: Some(name),
4219 old_oid: Some(oid),
4220 new_oid: None,
4221 mode: UpdateMode::DeletedMissing,
4222 note: Some("pruned (gone on remote)".to_owned()),
4223 });
4224 }
4225 let store = local.refs();
4226 crate::refs::commit_owned_ref_updates_on_store(store, batch)?;
4227 if !dry_run {
4228 for name in pruned_names {
4229 store.delete_reflog(&name)?;
4230 }
4231 }
4232 Ok(())
4233}
4234
4235#[cfg(test)]
4236mod tests {
4237 use super::*;
4238 use crate::progress::ProgressPhase;
4239 use crate::ref_storage::RefStorageFormat;
4240 use crate::repo::init_repository;
4241 use std::process::Command;
4242
4243 #[derive(Debug, Default)]
4244 struct PhaseRecorder {
4245 events: Vec<&'static str>,
4246 started: Vec<(ProgressPhase, Option<u64>)>,
4247 finished: Vec<ProgressPhase>,
4248 counts: Vec<u64>,
4249 enumerating_counts: Vec<u64>,
4250 copying_counts: Vec<u64>,
4251 active: Option<ProgressPhase>,
4252 current: u64,
4253 received: Vec<ReceivedObjects>,
4254 want_received: bool,
4255 }
4256
4257 impl ProgressSink for PhaseRecorder {
4258 fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
4259 self.events.push("start");
4260 self.started.push((phase, total));
4261 self.active = Some(phase);
4262 self.current = 0;
4263 }
4264 fn inc(&mut self, units: u64) {
4265 self.events.push("inc");
4266 self.current += units;
4267 self.record_count();
4268 }
4269 fn set(&mut self, current: u64) {
4270 self.events.push("set");
4271 self.current = current;
4272 self.record_count();
4273 }
4274 fn finish(&mut self) {
4275 self.events.push("finish");
4276 if let Some(phase) = self.active.take() {
4277 self.finished.push(phase);
4278 }
4279 }
4280 }
4281
4282 impl PhaseRecorder {
4283 fn record_count(&mut self) {
4284 self.counts.push(self.current);
4285 match self.active {
4286 Some(ProgressPhase::EnumeratingObjects) => {
4287 self.enumerating_counts.push(self.current);
4288 }
4289 Some(ProgressPhase::CopyingObjects) => {
4290 self.copying_counts.push(self.current);
4291 }
4292 _ => {}
4293 }
4294 }
4295 }
4296
4297 impl Progress for PhaseRecorder {
4298 fn wants_received_objects(&self) -> bool {
4299 self.want_received
4300 }
4301
4302 fn received_objects(&mut self, counts: ReceivedObjects) {
4303 assert_eq!(
4304 self.active,
4305 Some(ProgressPhase::CopyingObjects),
4306 "received_objects must run only during CopyingObjects (active={active:?}, counts={counts:?})",
4307 active = self.active,
4308 );
4309 assert!(
4310 counts.received <= counts.total,
4311 "received must not exceed total: {counts:?}"
4312 );
4313 self.received.push(counts);
4314 }
4315 }
4316
4317 fn git_ok(dir: &Path, args: &[&str]) -> bool {
4318 Command::new("git") .env("GIT_AUTHOR_NAME", "Test")
4320 .env("GIT_AUTHOR_EMAIL", "test@example.com")
4321 .env("GIT_COMMITTER_NAME", "Test")
4322 .env("GIT_COMMITTER_EMAIL", "test@example.com")
4323 .current_dir(dir)
4324 .args(args)
4325 .env("GIT_CONFIG_GLOBAL", "/dev/null")
4326 .env("GIT_CONFIG_SYSTEM", "/dev/null")
4327 .status()
4328 .map(|s| s.success())
4329 .unwrap_or(false)
4330 }
4331
4332 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
4334 pub(crate) enum PushHookPoint {
4335 BeforeReceivePolicy,
4338 BeforeRefCommit,
4340 }
4341
4342 type PushHook = (PushHookPoint, Box<dyn FnOnce()>);
4343
4344 thread_local! {
4346 static PUSH_HOOK: std::cell::RefCell<Option<PushHook>> =
4347 const { std::cell::RefCell::new(None) };
4348 }
4349
4350 pub(super) fn run_push_hook(point: PushHookPoint) {
4352 let hook = PUSH_HOOK.with(|slot| {
4353 let mut slot = slot.borrow_mut();
4354 match slot.as_ref() {
4355 Some((at, _)) if *at == point => slot.take().map(|(_, hook)| hook),
4356 _ => None,
4357 }
4358 });
4359 if let Some(hook) = hook {
4360 hook();
4361 }
4362 }
4363
4364 pub(crate) fn on_next_push(point: PushHookPoint, hook: impl FnOnce() + 'static) {
4366 PUSH_HOOK.with(|slot| *slot.borrow_mut() = Some((point, Box::new(hook))));
4367 }
4368
4369 fn git_stdout(dir: &Path, args: &[&str]) -> String {
4370 let out = Command::new("git") .env("GIT_AUTHOR_NAME", "Test")
4372 .env("GIT_AUTHOR_EMAIL", "test@example.com")
4373 .env("GIT_COMMITTER_NAME", "Test")
4374 .env("GIT_COMMITTER_EMAIL", "test@example.com")
4375 .env("GIT_CONFIG_GLOBAL", "/dev/null")
4376 .env("GIT_CONFIG_SYSTEM", "/dev/null")
4377 .current_dir(dir)
4378 .args(args)
4379 .output()
4380 .expect("run git");
4381 assert!(out.status.success(), "git {args:?} failed");
4382 String::from_utf8(out.stdout)
4383 .expect("utf8")
4384 .trim()
4385 .to_owned()
4386 }
4387
4388 fn racing_create_fixture(tmp: &Path) -> (PathBuf, PathBuf, ObjectId, ObjectId) {
4392 let remote = tmp.join("remote.git");
4393 let local = tmp.join("local");
4394 std::fs::create_dir_all(&remote).unwrap();
4395 std::fs::create_dir_all(&local).unwrap();
4396 assert!(git_ok(&remote, &["init", "-q", "--bare", "-b", "main"]));
4397 assert!(git_ok(&local, &["init", "-q", "-b", "main"]));
4398 assert!(git_ok(&local, &["commit", "--allow-empty", "-qm", "base"]));
4399 assert!(git_ok(
4400 &local,
4401 &["push", "-q", remote.to_str().unwrap(), "main"]
4402 ));
4403 assert!(git_ok(&local, &["checkout", "-q", "-b", "other"]));
4404 assert!(git_ok(&local, &["commit", "--allow-empty", "-qm", "other"]));
4405 assert!(git_ok(
4406 &local,
4407 &["push", "-q", remote.to_str().unwrap(), "other"]
4408 ));
4409 let other = ObjectId::from_hex(&git_stdout(&local, &["rev-parse", "HEAD"])).unwrap();
4410 assert!(git_ok(&local, &["checkout", "-q", "main"]));
4411 assert!(git_ok(&local, &["commit", "--allow-empty", "-qm", "mine"]));
4412 let tip = ObjectId::from_hex(&git_stdout(&local, &["rev-parse", "HEAD"])).unwrap();
4413 (remote, local.join(".git"), tip, other)
4414 }
4415
4416 #[cfg_attr(
4417 not(feature = "full-tests"),
4418 ignore = "starts system git or is heavy; run with --features full-tests"
4419 )]
4420 #[test]
4421 fn push_local_create_rejects_ref_created_after_decision() {
4422 let points = [
4423 PushHookPoint::BeforeReceivePolicy,
4424 PushHookPoint::BeforeRefCommit,
4425 ];
4426 let modes = [(false, false), (true, false), (false, true)];
4427 for (point, (force, atomic)) in points
4428 .into_iter()
4429 .flat_map(|p| modes.into_iter().map(move |m| (p, m)))
4430 {
4431 let tmp = tempfile::tempdir().unwrap();
4432 let (remote, local_git, tip, other) = racing_create_fixture(tmp.path());
4433
4434 let racer_remote = remote.clone();
4435 on_next_push(point, move || {
4436 assert!(git_ok(
4437 &racer_remote,
4438 &["update-ref", "refs/heads/shared", &other.to_hex(), ""]
4439 ));
4440 });
4441 let outcome = push_local(
4442 &local_git,
4443 &remote,
4444 &[PushRefSpec {
4445 src: Some(tip),
4446 dst: "refs/heads/shared".to_owned(),
4447 force,
4448 ..Default::default()
4449 }],
4450 &PushOptions {
4451 atomic,
4452 ..Default::default()
4453 },
4454 &crate::progress::NeverCancel,
4455 )
4456 .expect("push call");
4457
4458 let result = &outcome.results[0];
4459 let case = format!("{point:?} force={force} atomic={atomic}");
4460 assert_eq!(result.status, PushRefStatus::RejectAlreadyExists, "{case}");
4461 assert!(result.status.is_error(), "{case}");
4462 assert_eq!(result.message.as_deref(), Some("already exists"), "{case}");
4463 assert_eq!(
4464 git_stdout(&remote, &["rev-parse", "refs/heads/shared"]),
4465 other.to_hex(),
4466 "{case}: the racing creator's value must survive"
4467 );
4468 assert!(!git_ok(
4469 &remote,
4470 &[
4471 "merge-base",
4472 "--is-ancestor",
4473 &tip.to_hex(),
4474 &other.to_hex()
4475 ]
4476 ));
4477 }
4478 }
4479
4480 #[cfg_attr(
4481 not(feature = "full-tests"),
4482 ignore = "starts system git or is heavy; run with --features full-tests"
4483 )]
4484 #[test]
4485 fn push_local_update_rejects_ref_moved_after_decision() {
4486 for point in [
4487 PushHookPoint::BeforeReceivePolicy,
4488 PushHookPoint::BeforeRefCommit,
4489 ] {
4490 let tmp = tempfile::tempdir().unwrap();
4491 let (remote, local_git, tip, other) = racing_create_fixture(tmp.path());
4492
4493 let racer_remote = remote.clone();
4494 on_next_push(point, move || {
4495 assert!(git_ok(
4496 &racer_remote,
4497 &["update-ref", "refs/heads/main", &other.to_hex()]
4498 ));
4499 });
4500 let outcome = push_local(
4501 &local_git,
4502 &remote,
4503 &[PushRefSpec {
4504 src: Some(tip),
4505 dst: "refs/heads/main".to_owned(),
4506 ..Default::default()
4507 }],
4508 &PushOptions::default(),
4509 &crate::progress::NeverCancel,
4510 )
4511 .expect("push call");
4512
4513 assert_eq!(
4514 outcome.results[0].status,
4515 PushRefStatus::RejectStale,
4516 "{point:?}"
4517 );
4518 assert_eq!(
4519 git_stdout(&remote, &["rev-parse", "refs/heads/main"]),
4520 other.to_hex(),
4521 "{point:?}"
4522 );
4523 }
4524 }
4525
4526 fn assert_monotonic(counts: &[u64]) {
4527 for pair in counts.windows(2) {
4528 assert!(pair[1] >= pair[0], "progress counts must be monotonic");
4529 }
4530 }
4531
4532 fn copying_phase_total(rec: &PhaseRecorder) -> Option<u64> {
4533 rec.started
4534 .iter()
4535 .find(|(phase, _)| *phase == ProgressPhase::CopyingObjects)
4536 .and_then(|(_, total)| *total)
4537 }
4538
4539 fn assert_copy_progress_within_total(rec: &PhaseRecorder, copied_objects: u64) {
4540 let Some(total) = copying_phase_total(rec) else {
4541 panic!("missing CopyingObjects start: {:?}", rec.started);
4542 };
4543 assert!(
4544 total >= copied_objects,
4545 "progress total {total} smaller than copied objects {copied_objects}"
4546 );
4547 for &count in &rec.copying_counts {
4548 assert!(
4549 count <= total,
4550 "copy progress {count} exceeds phase total {total}"
4551 );
4552 }
4553 for snapshot in &rec.received {
4554 assert!(
4555 snapshot.received <= snapshot.total,
4556 "received {} exceeds snapshot total {}",
4557 snapshot.received,
4558 snapshot.total
4559 );
4560 assert!(
4561 snapshot.total <= total,
4562 "snapshot total {} exceeds phase total {}",
4563 snapshot.total,
4564 total
4565 );
4566 assert!(
4567 snapshot.received <= total,
4568 "received {} exceeds phase total {}",
4569 snapshot.received,
4570 total
4571 );
4572 }
4573 assert_eq!(
4574 rec.copying_counts.last().copied().unwrap_or(0),
4575 copied_objects,
4576 "final copy count should match transferred objects"
4577 );
4578 }
4579
4580 #[cfg_attr(
4581 not(feature = "full-tests"),
4582 ignore = "starts system git or is heavy; run with --features full-tests"
4583 )]
4584 #[test]
4585 fn local_fetch_classifies_through_repository_ref_store() {
4586 use crate::refs::store::{MemoryRefStore, RawRef};
4587 use std::sync::Arc;
4588
4589 let tmp = tempfile::tempdir().unwrap();
4590 assert!(git_ok(tmp.path(), &["init", "-q", "-b", "main"]));
4591 assert!(git_ok(
4592 tmp.path(),
4593 &["commit", "--allow-empty", "-qm", "base"]
4594 ));
4595 let repo =
4596 crate::repo::Repository::open(&tmp.path().join(".git"), &RepositoryOptions::empty())
4597 .unwrap();
4598 let tip = repo.resolve_ref_oid("HEAD").unwrap();
4599 let store = Arc::new(MemoryRefStore::new());
4600 store
4601 .set_ref("refs/remotes/origin/main", RawRef::Direct(tip))
4602 .unwrap();
4603 let repo = repo.with_ref_store(store);
4604 let opts = {
4605 let mut opts = FetchOptions::new();
4606 opts.refspecs = vec!["+refs/heads/*:refs/remotes/origin/*".into()];
4607 opts.tags = TagMode::None;
4608 opts.dry_run = true;
4609 opts
4610 };
4611 let outcome = fetch_local_into(
4612 &repo,
4613 &tmp.path().join(".git"),
4614 &opts,
4615 &mut PhaseRecorder::default(),
4616 &crate::progress::NeverCancel,
4617 )
4618 .unwrap();
4619 assert_eq!(outcome.transfer.objects, 0);
4620 assert_eq!(outcome.updates.len(), 1);
4621 assert_eq!(outcome.updates[0].old_oid, Some(tip));
4622 assert_eq!(outcome.updates[0].mode, UpdateMode::UpToDate);
4623 }
4624
4625 fn write_linear_commits(objects: &Path, depth: usize) -> (Odb, ObjectId) {
4626 use crate::objects::{serialize_commit, CommitData};
4627 let odb = Odb::new(objects);
4628 let mut parent = None;
4629 for i in 0..depth {
4630 let blob = odb
4631 .write(ObjectKind::Blob, format!("payload {i}\n").as_bytes())
4632 .expect("blob");
4633 let mut tree = b"100644 f.txt\0".to_vec();
4634 tree.extend_from_slice(blob.as_bytes());
4635 let tree_oid = odb.write(ObjectKind::Tree, &tree).expect("tree");
4636 let commit = CommitData {
4637 tree: tree_oid,
4638 parents: parent.into_iter().collect(),
4639 author: "T <t@e.com> 0 +0000".into(),
4640 committer: "T <t@e.com> 0 +0000".into(),
4641 author_raw: Vec::new(),
4642 committer_raw: Vec::new(),
4643 encoding: None,
4644 message: format!("c{i}\n"),
4645 raw_message: None,
4646 extra_headers: Vec::new(),
4647 };
4648 parent = Some(
4649 odb.write(ObjectKind::Commit, &serialize_commit(&commit))
4650 .expect("commit"),
4651 );
4652 }
4653 (odb, parent.expect("tip"))
4654 }
4655
4656 #[test]
4657 fn fetch_local_hardlinked_pack_reports_scan_progress() {
4658 let remote = tempfile::tempdir().expect("remote");
4659 assert!(git_ok(
4660 remote.path(),
4661 &["init", "-q", "-b", "main", "--bare"]
4662 ));
4663 let src = tempfile::tempdir().expect("src");
4664 assert!(git_ok(src.path(), &["init", "-q", "-b", "main"]));
4665 for i in 0..280 {
4666 std::fs::write(src.path().join("f.txt"), format!("payload {i}\n")).unwrap();
4667 assert!(git_ok(src.path(), &["add", "f.txt"]));
4668 assert!(git_ok(src.path(), &["commit", "-qm", &format!("c{i}")],));
4669 }
4670 assert!(git_ok(
4671 src.path(),
4672 &["push", "-q", remote.path().to_str().unwrap(), "main:main"],
4673 ));
4674 assert!(git_ok(remote.path(), &["repack", "-a", "-d", "-f"],));
4675
4676 let local = tempfile::tempdir().expect("local");
4677 init_repository(local.path(), false, "main", None, RefStorageFormat::Files).expect("init");
4678
4679 let opts = FetchOptions {
4680 refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
4681 initial_remote_fetch: true,
4682 tags: TagMode::None,
4683 ..Default::default()
4684 };
4685 let mut rec = PhaseRecorder {
4686 want_received: true,
4687 ..Default::default()
4688 };
4689 let outcome = fetch_local_with_progress(
4690 &local.path().join(".git"),
4691 remote.path(),
4692 &opts,
4693 &mut rec,
4694 &crate::progress::NeverCancel,
4695 )
4696 .expect("fetch_local");
4697 assert!(
4698 outcome.transfer.objects > 500,
4699 "expected a large hardlinked pack transfer"
4700 );
4701 assert!(
4702 rec.enumerating_counts.len() >= 3,
4703 "expected pack index scan progress under EnumeratingObjects, got {:?}",
4704 rec.enumerating_counts
4705 );
4706 assert_monotonic(&rec.enumerating_counts);
4707 assert_eq!(
4708 rec.started
4709 .iter()
4710 .filter(|(p, _)| *p == ProgressPhase::CopyingObjects)
4711 .count(),
4712 1,
4713 "expected a single CopyingObjects phase, got {:?}",
4714 rec.started
4715 );
4716 assert!(
4717 !rec.copying_counts.is_empty(),
4718 "expected copy progress after pack install, got {:?}",
4719 rec.copying_counts
4720 );
4721 assert_monotonic(&rec.copying_counts);
4722 assert!(
4723 rec.received.last().is_some_and(|s| s.received > 0),
4724 "expected received_objects only after objects were installed"
4725 );
4726 }
4727
4728 #[test]
4729 fn fetch_local_packed_remote_reports_incremental_copy_progress() {
4730 let remote = tempfile::tempdir().expect("remote");
4731 init_repository(remote.path(), true, "main", None, RefStorageFormat::Files)
4732 .expect("remote init");
4733 let objects = remote.path().join("objects");
4734 let (remote_odb, tip) = write_linear_commits(&objects, 280);
4735 crate::refs::write_ref(remote.path(), "refs/heads/main", &tip).expect("main ref");
4736 remote_odb
4737 .write(ObjectKind::Blob, b"unreachable orphan blob\n")
4738 .expect("orphan");
4739
4740 let local = tempfile::tempdir().expect("local");
4741 init_repository(local.path(), false, "main", None, RefStorageFormat::Files).expect("init");
4742
4743 let opts = {
4744 let mut opts = FetchOptions::new();
4745 opts.refspecs = vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()];
4746 opts.tags = TagMode::None;
4747 opts = opts.initial_remote_fetch(true);
4748 opts
4749 };
4750 let mut rec = PhaseRecorder {
4751 want_received: true,
4752 ..Default::default()
4753 };
4754 let outcome = fetch_local_with_progress(
4755 &local.path().join(".git"),
4756 remote.path(),
4757 &opts,
4758 &mut rec,
4759 &crate::progress::NeverCancel,
4760 )
4761 .expect("fetch_local");
4762 assert!(
4763 !rec.received.is_empty(),
4764 "expected received_objects callbacks"
4765 );
4766 for snapshot in &rec.received {
4767 assert!(snapshot.received <= snapshot.total);
4768 }
4769
4770 let enum_pos = rec
4771 .started
4772 .iter()
4773 .position(|(p, _)| *p == ProgressPhase::EnumeratingObjects)
4774 .expect("EnumeratingObjects");
4775 let copy_pos = rec
4776 .started
4777 .iter()
4778 .position(|(p, _)| *p == ProgressPhase::CopyingObjects)
4779 .expect("CopyingObjects");
4780 assert!(
4781 enum_pos < copy_pos,
4782 "enumerate before copy: {:?}",
4783 rec.started
4784 );
4785 assert_eq!(
4786 rec.started
4787 .iter()
4788 .filter(|(p, _)| *p == ProgressPhase::CopyingObjects)
4789 .count(),
4790 1,
4791 "expected a single CopyingObjects phase, got {:?}",
4792 rec.started
4793 );
4794 assert!(
4795 rec.counts.len() >= 3,
4796 "expected >=3 progress samples overall, got {:?}",
4797 rec.counts
4798 );
4799 assert!(
4800 rec.copying_counts.len() >= 3,
4801 "expected >=3 copy progress samples, got {:?}",
4802 rec.copying_counts
4803 );
4804 assert_monotonic(&rec.copying_counts);
4805 assert!(
4806 rec.copying_counts.last().copied().unwrap_or(0) >= outcome.transfer.objects as u64,
4807 "final copy count should reach transferred objects"
4808 );
4809 }
4810
4811 #[test]
4812 fn link_source_objects_progress_counts_objects_not_sidecar_files() {
4813 for want_received in [false, true] {
4814 let remote = tempfile::tempdir().expect("remote");
4815 assert!(git_ok(
4816 remote.path(),
4817 &["init", "-q", "-b", "main", "--bare"]
4818 ));
4819 let src = tempfile::tempdir().expect("src");
4820 assert!(git_ok(src.path(), &["init", "-q", "-b", "main"]));
4821 for i in 0..20 {
4822 std::fs::write(src.path().join("f.txt"), format!("payload {i}\n")).unwrap();
4823 assert!(git_ok(src.path(), &["add", "f.txt"]));
4824 assert!(git_ok(src.path(), &["commit", "-qm", &format!("c{i}")],));
4825 }
4826 assert!(git_ok(
4827 src.path(),
4828 &["push", "-q", remote.path().to_str().unwrap(), "main:main"],
4829 ));
4830 assert!(git_ok(remote.path(), &["repack", "-a", "-d", "-f"],));
4831
4832 let local = tempfile::tempdir().expect("local");
4833 init_repository(local.path(), false, "main", None, RefStorageFormat::Files)
4834 .expect("init");
4835
4836 let opts = {
4837 let mut opts = FetchOptions::new();
4838 opts.refspecs = vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()];
4839 opts.tags = TagMode::None;
4840 opts.link_source_objects = true;
4841 opts = opts.initial_remote_fetch(true);
4842 opts
4843 };
4844 let mut rec = PhaseRecorder {
4845 want_received,
4846 ..Default::default()
4847 };
4848 let outcome = fetch_local_with_progress(
4849 &local.path().join(".git"),
4850 remote.path(),
4851 &opts,
4852 &mut rec,
4853 &crate::progress::NeverCancel,
4854 )
4855 .expect("fetch_local");
4856
4857 if want_received {
4858 assert!(
4859 !rec.received.is_empty(),
4860 "expected received_objects when enabled"
4861 );
4862 } else {
4863 assert!(
4864 rec.received.is_empty(),
4865 "expected no received_objects when disabled"
4866 );
4867 }
4868 assert_copy_progress_within_total(&rec, outcome.transfer.objects as u64);
4869 assert_ne!(
4870 copying_phase_total(&rec),
4871 Some(1),
4872 "total must be object count, not ref count: {:?}",
4873 rec.started
4874 );
4875 }
4876 }
4877
4878 #[cfg_attr(
4879 not(feature = "full-tests"),
4880 ignore = "starts system git or is heavy; run with --features full-tests"
4881 )]
4882 #[test]
4883 fn fetch_local_reports_copy_progress() {
4884 let remote = tempfile::tempdir().expect("remote");
4885 assert!(git_ok(
4886 remote.path(),
4887 &["init", "-q", "-b", "main", "--bare"]
4888 ));
4889 let src = tempfile::tempdir().expect("src");
4890 assert!(git_ok(src.path(), &["init", "-q", "-b", "main"]));
4891 std::fs::write(src.path().join("f.txt"), "payload\n").unwrap();
4892 assert!(git_ok(src.path(), &["add", "f.txt"]));
4893 assert!(git_ok(src.path(), &["commit", "-qm", "c1"]));
4894 assert!(git_ok(
4895 src.path(),
4896 &["push", "-q", remote.path().to_str().unwrap(), "main:main",],
4897 ));
4898
4899 let local = tempfile::tempdir().expect("local");
4900 init_repository(local.path(), false, "main", None, RefStorageFormat::Files).expect("init");
4901
4902 let opts = {
4903 let mut opts = FetchOptions::new();
4904 opts.refspecs = vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()];
4905 opts.tags = TagMode::None;
4906 opts = opts.initial_remote_fetch(true);
4907 opts
4908 };
4909 let mut rec = PhaseRecorder::default();
4910 fetch_local_with_progress(
4911 &local.path().join(".git"),
4912 remote.path(),
4913 &opts,
4914 &mut rec,
4915 &crate::progress::NeverCancel,
4916 )
4917 .expect("fetch_local");
4918
4919 assert!(
4920 rec.started
4921 .iter()
4922 .any(|(p, _)| *p == ProgressPhase::CopyingObjects),
4923 "expected CopyingObjects phase"
4924 );
4925 assert!(
4926 rec.finished.contains(&ProgressPhase::CopyingObjects),
4927 "expected CopyingObjects finish"
4928 );
4929 assert!(!rec.counts.is_empty(), "expected copy progress counts");
4930 assert_monotonic(&rec.counts);
4931 let first_progress = rec.events.iter().find(|e| **e != "start").copied();
4932 assert_eq!(
4933 rec.events.first().copied(),
4934 Some("start"),
4935 "CopyingObjects must start before link/copy work reports progress"
4936 );
4937 assert!(
4938 first_progress.is_some(),
4939 "expected set/inc after start during copy"
4940 );
4941 }
4942
4943 #[test]
4944 fn transfer_stats_add_up_and_keep_kinds_only_when_known() {
4945 use crate::index_pack::ObjectKindCounts;
4946 let kinds = |commits, blobs| ObjectKindCounts {
4947 commits,
4948 trees: 0,
4949 blobs,
4950 tags: 0,
4951 };
4952 let mut total = TransferStats::default();
4953 total += TransferStats {
4954 objects: 3,
4955 bytes: 100,
4956 kinds: Some(kinds(1, 2)),
4957 };
4958 total += TransferStats::default();
4960 total += TransferStats {
4961 objects: 2,
4962 bytes: 50,
4963 kinds: Some(kinds(1, 1)),
4964 };
4965 assert_eq!(
4966 total,
4967 TransferStats {
4968 objects: 5,
4969 bytes: 150,
4970 kinds: Some(kinds(2, 3)),
4971 }
4972 );
4973
4974 total += TransferStats {
4976 objects: 4,
4977 bytes: 10,
4978 kinds: None,
4979 };
4980 assert_eq!(total.objects, 9);
4981 assert_eq!(total.bytes, 160);
4982 assert_eq!(total.kinds, None);
4983 }
4984
4985 #[cfg_attr(
4986 not(feature = "full-tests"),
4987 ignore = "starts system git or is heavy; run with --features full-tests"
4988 )]
4989 #[test]
4990 fn transfer_stats_for_local_operation_counts_new_pack_index() {
4991 let tmp = tempfile::tempdir().expect("tempdir");
4992 let repo = tmp.path().join("repo");
4993 std::fs::create_dir_all(&repo).unwrap();
4994 assert!(git_ok(&repo, &["init", "-q", "-b", "main", "."]));
4995 std::fs::write(repo.join("blob.txt"), b"payload\n").unwrap();
4996 assert!(git_ok(&repo, &["add", "blob.txt"]));
4997 assert!(git_ok(&repo, &["commit", "-qm", "c"]));
4998 assert!(git_ok(&repo, &["repack", "-a", "-d", "-f"]));
4999
5000 let git_dir = repo.join(".git");
5001 let pack_dir = git_dir.join("objects/pack");
5002 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
5003 let stats =
5004 transfer_stats_for_local_operation(&odb, &HashSet::new(), None, &HashSet::new())
5005 .expect("stats");
5006 assert!(stats.bytes > 0);
5007 assert!(stats.objects > 0);
5008
5009 let pack_path = std::fs::read_dir(&pack_dir)
5010 .expect("read pack dir")
5011 .filter_map(|e| e.ok())
5012 .map(|e| e.path())
5013 .find(|p| p.extension().is_some_and(|e| e == "pack"))
5014 .expect("pack file");
5015 let idx = pack_path.with_extension("idx");
5016 let index = crate::pack::read_pack_index_no_verify(&idx).expect("idx");
5017 assert_eq!(
5018 stats.objects,
5019 u64::try_from(index.len()).expect("count"),
5020 "object count must match the on-disk index entry count"
5021 );
5022 }
5023
5024 #[test]
5025 fn update_mode_modified_repo_excludes_rejected_tag_updates() {
5026 assert!(UpdateMode::New.modified_repo());
5027 assert!(UpdateMode::DeletedMissing.modified_repo());
5028 assert!(!UpdateMode::TagUpdateRejected.modified_repo());
5029 assert!(!UpdateMode::UpToDate.modified_repo());
5030 }
5031
5032 #[test]
5033 fn update_mode_report_classification_covers_every_mode() {
5034 use UpdateMode::*;
5035 for (mode, reported, rejected) in [
5036 (New, true, false),
5037 (FastForward, true, false),
5038 (Forced, true, false),
5039 (DeletedMissing, true, false),
5040 (NonFastForwardRejected, true, true),
5041 (TagUpdateRejected, true, true),
5042 (SourceObjectNotFound, true, true),
5043 (UpToDate, false, false),
5044 (NoChangeNeeded, false, false),
5045 (Unborn, false, false),
5046 ] {
5047 assert_eq!(mode.is_reported(), reported, "{mode:?}");
5048 assert_eq!(mode.is_rejected(), rejected, "{mode:?}");
5049 assert!(!(mode.modified_repo() && mode.is_rejected()), "{mode:?}");
5050 }
5051 }
5052
5053 #[test]
5054 fn fetch_outcome_summarizes_reported_changed_and_rejected_refs() {
5055 let update = |name: &str, mode| RefUpdate {
5056 remote_ref: format!("refs/heads/{name}"),
5057 local_ref: Some(format!("refs/remotes/origin/{name}")),
5058 old_oid: None,
5059 new_oid: None,
5060 mode,
5061 note: None,
5062 };
5063 let outcome = FetchOutcome {
5064 updates: vec![
5065 update("same", UpdateMode::UpToDate),
5066 update("new", UpdateMode::New),
5067 update("ff", UpdateMode::FastForward),
5068 update("noop", UpdateMode::NoChangeNeeded),
5069 ],
5070 ..Default::default()
5071 };
5072 let reported: Vec<&str> = outcome
5073 .reported_updates()
5074 .map(|u| u.remote_ref.as_str())
5075 .collect();
5076 assert_eq!(reported, ["refs/heads/new", "refs/heads/ff"]);
5077 assert_eq!(outcome.changed_ref_count(), 2);
5078 assert!(!outcome.has_rejections());
5079
5080 let rejected = FetchOutcome {
5081 updates: vec![update("diverged", UpdateMode::NonFastForwardRejected)],
5082 ..Default::default()
5083 };
5084 assert_eq!(rejected.changed_ref_count(), 0);
5085 assert!(rejected.has_rejections());
5086 assert_eq!(rejected.reported_updates().count(), 1);
5087 assert!(FetchOutcome::default().reported_updates().next().is_none());
5088 }
5089
5090 #[test]
5091 fn update_mode_kind_covers_every_mode() {
5092 use UpdateMode::*;
5093 for (mode, kind) in [
5094 (New, UpdateKind::New),
5095 (FastForward, UpdateKind::FastForward),
5096 (Forced, UpdateKind::Forced),
5097 (DeletedMissing, UpdateKind::Pruned),
5098 (NonFastForwardRejected, UpdateKind::Rejected),
5099 (TagUpdateRejected, UpdateKind::Rejected),
5100 (SourceObjectNotFound, UpdateKind::Rejected),
5101 (RefLockRejected, UpdateKind::Rejected),
5102 (UpToDate, UpdateKind::UpToDate),
5103 (NoChangeNeeded, UpdateKind::UpToDate),
5104 (Unborn, UpdateKind::UpToDate),
5105 ] {
5106 assert_eq!(mode.kind(), kind, "{mode:?}");
5107 assert_eq!(kind.changes_ref(), mode.modified_repo(), "{mode:?}");
5108 assert_eq!(kind == UpdateKind::Rejected, mode.is_rejected(), "{mode:?}");
5109 }
5110 }
5111
5112 #[test]
5113 fn push_outcome_reports_updates_and_rejections() {
5114 let result = |status| PushRefResult {
5115 local_ref: Some("refs/heads/main".into()),
5116 remote_ref: "refs/heads/main".into(),
5117 old_oid: None,
5118 new_oid: None,
5119 forced: false,
5120 deletion: false,
5121 status,
5122 message: None,
5123 };
5124 let outcome = |statuses: Vec<PushRefStatus>| PushOutcome {
5125 results: statuses.into_iter().map(result).collect(),
5126 remote_messages: Vec::new(),
5127 };
5128
5129 let empty = PushOutcome::default();
5130 assert!(!empty.has_updates());
5131 assert!(!empty.has_rejections());
5132
5133 let up_to_date = outcome(vec![PushRefStatus::UpToDate]);
5134 assert!(!up_to_date.has_updates());
5135 assert!(!up_to_date.has_rejections());
5136
5137 let pushed = outcome(vec![PushRefStatus::UpToDate, PushRefStatus::Ok]);
5138 assert!(pushed.has_updates());
5139 assert!(!pushed.has_rejections());
5140
5141 let mixed = outcome(vec![PushRefStatus::Ok, PushRefStatus::RemoteRejected]);
5142 assert!(mixed.has_updates());
5143 assert!(mixed.has_rejections());
5144
5145 let refused = outcome(vec![PushRefStatus::AtomicPushFailed]);
5146 assert!(!refused.has_updates());
5147 assert!(refused.has_rejections());
5148 }
5149}