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 pub(crate) fn fetch_reflog_reason(self) -> Option<&'static str> {
103 match self {
104 Self::New => Some("storing head"),
105 Self::FastForward => Some("fast-forward"),
106 Self::Forced => Some("forced-update"),
107 _ => None,
108 }
109 }
110}
111
112pub(crate) struct FetchApplyReflog<'a> {
114 pub action: &'static str,
115 pub identity: &'a str,
116}
117
118#[derive(Clone, Debug)]
120#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
121pub struct RefUpdate {
122 pub remote_ref: String,
124 pub local_ref: Option<String>,
126 pub old_oid: Option<ObjectId>,
128 pub new_oid: Option<ObjectId>,
130 pub mode: UpdateMode,
132 pub note: Option<String>,
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
138pub enum TagMode {
139 None,
141 #[default]
160 Following,
161 All,
164}
165
166#[derive(Clone, Copy, Debug, PartialEq, Eq)]
168pub enum FetchDepth {
169 Depth(NonZeroU32),
172 Deepen(NonZeroU32),
175 Unshallow,
177}
178
179impl FetchDepth {
180 #[must_use]
182 pub fn depth(n: u32) -> Option<Self> {
183 NonZeroU32::new(n).map(Self::Depth)
184 }
185
186 #[must_use]
188 pub fn deepen(n: u32) -> Option<Self> {
189 NonZeroU32::new(n).map(Self::Deepen)
190 }
191}
192
193#[non_exhaustive]
195pub struct FetchOptions {
196 pub refspecs: Vec<String>,
198 pub negative_refspecs: Vec<String>,
200 pub tags: TagMode,
202 pub prune: bool,
204 pub dry_run: bool,
206 pub fetch_depth: Option<FetchDepth>,
209 pub deepen_since: Option<String>,
214 pub deepen_not: Vec<String>,
218 pub(crate) initial_remote_fetch: bool,
221 pub remote_name: Option<String>,
223 pub fetch_source: Option<String>,
226 pub configured_refspecs: Vec<String>,
229 pub clone_reflog: Option<CloneReflog>,
231 pub(crate) reflog_action: &'static str,
233 pub force: bool,
237 pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
239 pub network_trace: bool,
241 pub index_pack_threads: Option<usize>,
243 pub thread_policy: crate::parallel::ThreadPolicy,
245 pub environment: crate::environment::Environment,
247 pub(crate) pack_spool_only: bool,
249 pub http_protocol_version: Option<u8>,
251 pub remote_progress: bool,
254 pub filter: Option<ObjectFilterSpec>,
257 pub auto_filter: bool,
263 pub(crate) mark_received_pack_promisor: bool,
268 pub(crate) object_wants: Vec<ObjectId>,
270 pub(crate) promisor_object_fetch: bool,
272 pub link_source_objects: bool,
283}
284
285impl Default for FetchOptions {
286 fn default() -> Self {
287 Self {
288 refspecs: Vec::new(),
289 negative_refspecs: Vec::new(),
290 tags: TagMode::Following,
291 prune: false,
292 dry_run: false,
293 fetch_depth: None,
294 deepen_since: None,
295 deepen_not: Vec::new(),
296 initial_remote_fetch: false,
297 remote_name: None,
298 fetch_source: None,
299 configured_refspecs: Vec::new(),
300 clone_reflog: None,
301 reflog_action: "fetch",
302 diagnostics: None,
303 network_trace: false,
304 index_pack_threads: None,
305 thread_policy: crate::parallel::ThreadPolicy::default(),
306 environment: crate::environment::Environment::empty(),
307 pack_spool_only: false,
308 http_protocol_version: None,
309 remote_progress: false,
310 filter: None,
311 auto_filter: false,
312 mark_received_pack_promisor: false,
313 object_wants: Vec::new(),
314 promisor_object_fetch: false,
315 link_source_objects: false,
316 force: false,
317 }
318 }
319}
320
321#[derive(Clone, Debug)]
323pub struct CloneReflog {
324 pub identity: crate::ident::Signature,
326 pub message: String,
328}
329
330impl Clone for FetchOptions {
331 fn clone(&self) -> Self {
332 Self {
333 refspecs: self.refspecs.clone(),
334 negative_refspecs: self.negative_refspecs.clone(),
335 tags: self.tags,
336 prune: self.prune,
337 dry_run: self.dry_run,
338 fetch_depth: self.fetch_depth,
339 deepen_since: self.deepen_since.clone(),
340 deepen_not: self.deepen_not.clone(),
341 initial_remote_fetch: self.initial_remote_fetch,
342 remote_name: self.remote_name.clone(),
343 fetch_source: self.fetch_source.clone(),
344 configured_refspecs: self.configured_refspecs.clone(),
345 clone_reflog: self.clone_reflog.clone(),
346 reflog_action: self.reflog_action,
347 diagnostics: self.diagnostics.clone(),
348 network_trace: self.network_trace,
349 index_pack_threads: self.index_pack_threads,
350 thread_policy: self.thread_policy,
351 environment: self.environment.clone(),
352 pack_spool_only: self.pack_spool_only,
353 http_protocol_version: self.http_protocol_version,
354 remote_progress: self.remote_progress,
355 filter: self.filter,
356 auto_filter: self.auto_filter,
357 mark_received_pack_promisor: self.mark_received_pack_promisor,
358 object_wants: self.object_wants.clone(),
359 promisor_object_fetch: self.promisor_object_fetch,
360 link_source_objects: self.link_source_objects,
361 force: self.force,
362 }
363 }
364}
365
366impl FetchOptions {
367 #[must_use]
369 pub fn new() -> Self {
370 Self::default()
371 }
372
373 #[must_use]
375 pub fn initial_remote_fetch(mut self, enabled: bool) -> Self {
376 self.initial_remote_fetch = enabled;
377 self
378 }
379
380 #[doc(hidden)]
382 #[must_use]
383 pub fn pack_spool_only(mut self, enabled: bool) -> Self {
384 self.pack_spool_only = enabled;
385 self
386 }
387
388 #[doc(hidden)]
390 #[must_use]
391 pub fn promisor_object_wants(mut self, wants: Vec<ObjectId>) -> Self {
392 self.object_wants = wants;
393 self.promisor_object_fetch = true;
394 self
395 }
396
397 #[must_use]
403 pub fn has_deepen_request(&self) -> bool {
404 self.fetch_depth.is_some()
405 || self
406 .deepen_since
407 .as_deref()
408 .is_some_and(|v| !v.trim().is_empty())
409 || self.deepen_not.iter().any(|v| !v.trim().is_empty())
410 }
411
412 #[must_use]
414 pub(crate) fn wire_deepen(&self) -> Option<(u32, bool)> {
415 match self.fetch_depth {
416 Some(FetchDepth::Depth(n)) => Some((n.get(), false)),
417 Some(FetchDepth::Deepen(n)) => Some((n.get(), true)),
418 Some(FetchDepth::Unshallow) => Some((crate::shallow::INFINITE_DEPTH, false)),
419 None => None,
420 }
421 }
422
423 #[must_use]
425 pub(crate) fn absolute_depth_for_tags(&self) -> Option<u32> {
426 match self.fetch_depth {
427 Some(FetchDepth::Depth(n)) => Some(n.get()),
428 Some(FetchDepth::Deepen(_)) | Some(FetchDepth::Unshallow) | None => None,
429 }
430 }
431}
432
433pub fn validate_fetch_depth_local(local_git_dir: &Path, opts: &FetchOptions) -> Result<()> {
440 if matches!(opts.fetch_depth, Some(FetchDepth::Unshallow)) {
441 let local = crate::shallow::load_shallow_oids(local_git_dir)?;
442 if local.is_empty() {
443 return Err(Error::FetchUnshallowNotShallow);
444 }
445 }
446 Ok(())
447}
448
449pub(crate) fn validate_fetch_depth_server(
451 opts: &FetchOptions,
452 v0_caps: Option<&str>,
453 v2_caps: Option<&[String]>,
454) -> Result<()> {
455 let needs_shallow = opts.has_deepen_request()
456 || opts.wire_deepen().is_some()
457 || opts
458 .deepen_since
459 .as_deref()
460 .is_some_and(|s| !s.trim().is_empty())
461 || opts.deepen_not.iter().any(|s| !s.trim().is_empty());
462 if !needs_shallow {
463 return Ok(());
464 }
465 if let Some(caps) = v0_caps {
466 if !caps.contains("shallow") {
467 return Err(Error::FetchShallowUnsupported);
468 }
469 if opts.wire_deepen().is_some_and(|(_, rel)| rel) && !caps.contains("deepen-relative") {
470 return Err(Error::FetchDeepenRelativeUnsupported);
471 }
472 } else if let Some(caps) = v2_caps {
473 if !crate::protocol_v2::fetch_supports_feature(caps, "shallow") {
474 return Err(Error::FetchShallowUnsupported);
475 }
476 }
477 Ok(())
478}
479
480pub fn resolve_fetch_prune(
490 config: &crate::config::ConfigSet,
491 remote: &str,
492 cli_prune: Option<bool>,
493) -> Result<bool> {
494 if let Some(enabled) = cli_prune {
495 return Ok(enabled);
496 }
497 let remote_key = format!("remote.{remote}.prune");
498 if let Some(value) = optional_config_bool(config, &remote_key)? {
499 return Ok(value);
500 }
501 if let Some(value) = optional_config_bool(config, "fetch.prune")? {
502 return Ok(value);
503 }
504 Ok(false)
505}
506
507#[must_use]
514pub fn resolve_fetch_tags(
515 config: &crate::config::ConfigSet,
516 remote: &str,
517 cli_tags: Option<bool>,
518) -> TagMode {
519 if let Some(all) = cli_tags {
520 return if all { TagMode::All } else { TagMode::None };
521 }
522 let key = format!("remote.{remote}.tagOpt");
523 match config.get(&key).as_deref().map(str::trim) {
524 Some("--tags") | Some("tags") | Some("all") => TagMode::All,
525 Some("--no-tags") | Some("no-tags") | Some("no") | Some("never") => TagMode::None,
526 _ => TagMode::Following,
527 }
528}
529
530fn optional_config_bool(config: &crate::config::ConfigSet, key: &str) -> Result<Option<bool>> {
531 match config.get_bool(key) {
532 None => Ok(None),
533 Some(Ok(value)) => Ok(Some(value)),
534 Some(Err(message)) => Err(Error::Config(crate::error::ConfigError::Other(format!(
535 "invalid boolean for '{key}': {message}"
536 )))),
537 }
538}
539
540#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
542#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
543pub struct TransferStats {
544 pub objects: u64,
546 pub bytes: u64,
548 pub kinds: Option<crate::index_pack::ObjectKindCounts>,
551}
552
553impl TransferStats {
554 #[must_use]
557 pub fn from_ingested_pack(ingested: &crate::index_pack::IngestedPack) -> Self {
558 Self {
559 objects: u64::try_from(ingested.object_ids.len()).unwrap_or(u64::MAX),
560 bytes: ingested.pack_bytes,
561 kinds: Some(ingested.object_counts),
562 }
563 }
564}
565
566impl std::ops::AddAssign for TransferStats {
571 fn add_assign(&mut self, other: Self) {
572 self.kinds = match (self.kinds, other.kinds) {
573 (Some(mut mine), Some(theirs)) => {
574 mine += theirs;
575 Some(mine)
576 }
577 (mine, None) if other.objects == 0 => mine,
578 (None, theirs) if self.objects == 0 => theirs,
579 _ => None,
580 };
581 self.objects += other.objects;
582 self.bytes += other.bytes;
583 }
584}
585
586#[derive(Clone, Debug, Default)]
588#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
589pub struct FetchOutcome {
590 pub updates: Vec<RefUpdate>,
592 pub default_branch: Option<String>,
594 pub new_shallow: Vec<ObjectId>,
598 pub new_unshallow: Vec<ObjectId>,
602 pub transfer: TransferStats,
604 pub filter_sent: bool,
609 pub promisor: Option<crate::promisor_remote::PromisorHandshake>,
612}
613
614impl FetchOutcome {
615 #[must_use]
617 pub fn branch_count(&self) -> u64 {
618 self.updates
619 .iter()
620 .filter(|u| u.remote_ref.starts_with("refs/heads/"))
621 .count() as u64
622 }
623
624 #[must_use]
626 pub fn tag_count(&self) -> u64 {
627 self.updates
628 .iter()
629 .filter(|u| u.remote_ref.starts_with("refs/tags/") && !u.remote_ref.ends_with("^{}"))
630 .count() as u64
631 }
632
633 pub fn reported_updates(&self) -> impl Iterator<Item = &RefUpdate> {
635 self.updates.iter().filter(|u| u.mode.is_reported())
636 }
637
638 #[must_use]
643 pub fn changed_ref_count(&self) -> usize {
644 self.updates
645 .iter()
646 .filter(|u| u.mode.modified_repo())
647 .count()
648 }
649
650 #[must_use]
652 pub fn has_rejections(&self) -> bool {
653 self.updates.iter().any(|u| u.mode.is_rejected())
654 }
655}
656
657fn object_kind_counts_for_oids(
658 odb: &Odb,
659 oids: &HashSet<ObjectId>,
660) -> Result<crate::index_pack::ObjectKindCounts> {
661 use crate::index_pack::ObjectKindCounts;
662 let mut kinds = ObjectKindCounts::default();
663 for oid in oids {
664 kinds.add(odb.read_info(oid)?.kind);
665 }
666 Ok(kinds)
667}
668
669fn loose_transfer_stats(odb: &Odb, oids: &HashSet<ObjectId>) -> (u64, u64) {
670 let mut objects = 0u64;
671 let mut bytes = 0u64;
672 for size in oids.iter().filter_map(|oid| odb.local_loose_file_size(oid)) {
673 objects += 1;
674 bytes += size;
675 }
676 (objects, bytes)
677}
678
679fn pack_file_basenames(pack_dir: &Path) -> HashSet<String> {
680 let mut names = HashSet::new();
681 let Ok(entries) = fs::read_dir(pack_dir) else {
682 return names;
683 };
684 for entry in entries.flatten() {
685 let path = entry.path();
686 if path.extension().is_some_and(|e| e == "pack") {
687 if let Some(name) = path.file_name() {
688 names.insert(name.to_string_lossy().into_owned());
689 }
690 }
691 }
692 names
693}
694
695const LOCAL_LINK_KINDS_OID_PROBE_MAX: u64 = 4096;
696
697fn oids_from_newly_linked_pack_indexes(
698 local_odb: &Odb,
699 pack_dir_before: &HashSet<String>,
700) -> Result<Vec<ObjectId>> {
701 let pack_dir = local_odb.objects_dir().join("pack");
702 let mut oids = Vec::new();
703 if !pack_dir.is_dir() {
704 return Ok(oids);
705 }
706 for entry in fs::read_dir(&pack_dir).map_err(Error::Io)? {
707 let path = entry.map_err(Error::Io)?.path();
708 if path.extension().is_none_or(|e| e != "pack") {
709 continue;
710 }
711 let Some(name) = path.file_name() else {
712 continue;
713 };
714 if pack_dir_before.contains(name.to_string_lossy().as_ref()) {
715 continue;
716 }
717 let idx = path.with_extension("idx");
718 if !idx.is_file() {
719 continue;
720 }
721 let index = crate::pack::read_pack_index_no_verify(&idx)?;
722 for entry in index.iter() {
723 let oid = ObjectId::from_bytes(entry.oid())?;
724 oids.push(oid);
725 }
726 }
727 Ok(oids)
728}
729
730fn enrich_local_link_transfer_kinds(
731 local_odb: &Odb,
732 pack_dir_before: &HashSet<String>,
733 loose_reachable: &HashSet<ObjectId>,
734 transfer: &mut TransferStats,
735) -> Result<()> {
736 if transfer.kinds.is_some() || transfer.objects == 0 {
737 return Ok(());
738 }
739 if transfer.objects > LOCAL_LINK_KINDS_OID_PROBE_MAX {
740 return Ok(());
741 }
742 let mut probe: HashSet<ObjectId> = loose_reachable.clone();
743 for oid in oids_from_newly_linked_pack_indexes(local_odb, pack_dir_before)? {
744 probe.insert(oid);
745 }
746 if u64::try_from(probe.len()).unwrap_or(u64::MAX) != transfer.objects {
747 return Ok(());
748 }
749 transfer.kinds = Some(object_kind_counts_for_oids(local_odb, &probe)?);
750 Ok(())
751}
752
753fn transfer_stats_for_local_operation(
758 local_odb: &Odb,
759 installed: &HashSet<ObjectId>,
760 loose_candidates: Option<&HashSet<ObjectId>>,
761 pack_dir_before: &HashSet<String>,
762) -> Result<TransferStats> {
763 let objects_dir = local_odb.objects_dir();
764 let pack_dir = objects_dir.join("pack");
765 let mut bytes = 0u64;
766 let mut objects_from_new_packs = 0u64;
767 if pack_dir.is_dir() {
768 for entry in fs::read_dir(&pack_dir).map_err(Error::Io)? {
769 let path = entry.map_err(Error::Io)?.path();
770 if path.extension().is_none_or(|e| e != "pack") {
771 continue;
772 }
773 let Some(name) = path.file_name() else {
774 continue;
775 };
776 let name = name.to_string_lossy();
777 if pack_dir_before.contains(name.as_ref()) {
778 continue;
779 }
780 bytes += fs::metadata(&path).map_err(Error::Io)?.len();
781 let idx = path.with_extension("idx");
782 if idx.is_file() {
783 let index = crate::pack::read_pack_index_no_verify(&idx)?;
784 objects_from_new_packs += u64::try_from(index.len()).unwrap_or(u64::MAX);
785 }
786 }
787 }
788 let (loose_objects, loose_bytes) =
789 loose_transfer_stats(local_odb, loose_candidates.unwrap_or(installed));
790 bytes = bytes.saturating_add(loose_bytes);
791 let installed_count = u64::try_from(installed.len()).unwrap_or(u64::MAX);
792 let objects = if objects_from_new_packs > 0 {
793 objects_from_new_packs.saturating_add(loose_objects)
794 } else {
795 installed_count
796 };
797 let kinds = if objects == installed_count && installed_count <= LOCAL_LINK_KINDS_OID_PROBE_MAX {
798 Some(object_kind_counts_for_oids(local_odb, installed)?)
799 } else {
800 None
801 };
802 Ok(TransferStats {
803 objects,
804 bytes,
805 kinds,
806 })
807}
808
809#[derive(Clone, Debug, Default)]
811pub struct PushRefSpec {
812 pub src: Option<ObjectId>,
814 pub src_ref: Option<String>,
816 pub dst: String,
818 pub force: bool,
820 pub delete: bool,
822 pub expected_old: Option<ObjectId>,
825 pub expect_absent: bool,
830 pub hook_src_ref: Option<String>,
834}
835
836#[must_use]
838pub(crate) fn push_spec_local_ref(spec: &PushRefSpec) -> Option<String> {
839 spec.src_ref.clone()
840}
841
842#[must_use]
848pub fn resolve_push_delete_ref(
849 git_dir: &Path,
850 remote_ref_names: &HashSet<String>,
851 spec: &str,
852) -> String {
853 if spec.starts_with("refs/") {
854 return spec.to_owned();
855 }
856 let branch = format!("refs/heads/{spec}");
857 let tag = format!("refs/tags/{spec}");
858 if crate::refs::resolve_ref(git_dir, &branch).is_ok() {
859 return branch;
860 }
861 if remote_ref_names.contains(&branch) {
862 return branch;
863 }
864 if crate::refs::resolve_ref(git_dir, &tag).is_ok() {
865 return tag;
866 }
867 if remote_ref_names.contains(&tag) {
868 return tag;
869 }
870 branch
871}
872
873#[derive(Clone, Default)]
875pub struct PushOptions {
876 pub atomic: bool,
883 pub dry_run: bool,
885 pub tracking_remote: Option<String>,
888 pub push_options: Vec<String>,
902 pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
904 pub network_trace: bool,
906 pub remote_ref_tips: Option<Vec<ObjectId>>,
914 pub resumable_threshold: Option<u64>,
919 pub no_verify: bool,
927}
928
929pub(crate) struct PrePushGate<'a> {
931 pub(crate) repo: &'a crate::repo::Repository,
933 pub(crate) remote: &'a str,
935 pub(crate) url: &'a str,
937}
938
939impl PrePushGate<'_> {
940 pub(crate) fn check<'r>(
947 &self,
948 updates: impl IntoIterator<Item = (&'r PushRefSpec, &'r PushRefResult)>,
949 ) -> Result<()> {
950 let refs: Vec<crate::hooks::PrePushRef> = updates
951 .into_iter()
952 .filter(|(_, r)| {
953 !matches!(
954 r.status,
955 PushRefStatus::UpToDate
956 | PushRefStatus::RejectNonFastForward
957 | PushRefStatus::RejectStale
958 | PushRefStatus::RejectRemoteRefMissing
959 )
960 })
961 .map(|(spec, r)| crate::hooks::PrePushRef {
962 local_ref: if r.deletion {
963 None
964 } else {
965 spec.hook_src_ref.clone().or_else(|| r.local_ref.clone())
966 },
967 local_oid: r.new_oid,
968 remote_ref: r.remote_ref.clone(),
969 remote_oid: r.old_oid,
970 })
971 .collect();
972 crate::hooks::run_pre_push_hook(self.repo, self.remote, self.url, &refs)?;
973 Ok(())
974 }
975}
976
977#[derive(Clone, Debug, Default)]
979#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
980pub struct PushOutcome {
981 pub results: Vec<PushRefResult>,
983 pub remote_messages: Vec<String>,
988}
989
990pub use crate::pack_objects::{
991 build_pack, build_pack_from_send_list, reachable_closure, PackBuildOptions, PackObjects,
992 PackObjectsOptions, PackStats,
993};
994
995pub(crate) use crate::pack_objects::write_pack_for_local_fetch;
996
997fn pack_build_options_for_send(
998 source_odb: &Odb,
999 oids: &[ObjectId],
1000 base: &PackBuildOptions,
1001) -> PackBuildOptions {
1002 if oids.is_empty() || oids.len() > 512 {
1003 return *base;
1004 }
1005 let commits_tags_only = oids.iter().all(|oid| {
1006 source_odb
1007 .read(oid)
1008 .map(|obj| matches!(obj.kind, ObjectKind::Commit | ObjectKind::Tag))
1009 .unwrap_or(false)
1010 });
1011 if !commits_tags_only {
1012 return *base;
1013 }
1014 let mut fast = *base;
1015 fast.delta = false;
1016 fast.reuse_deltas = false;
1017 fast.thin = false;
1018 fast
1019}
1020
1021fn ingest_pack_options_for_fetch(
1022 local_git_dir: &Path,
1023 fetch_opts: &FetchOptions,
1024) -> Result<crate::index_pack::IngestPackOptions> {
1025 let pack_config =
1026 crate::config::ConfigSet::load(&fetch_opts.environment, Some(local_git_dir), true)?;
1027 let index_threads = crate::parallel::index_pack_threads_for_ingest(
1028 fetch_opts.thread_policy,
1029 &pack_config,
1030 fetch_opts.index_pack_threads,
1031 )?;
1032 Ok(crate::index_pack::IngestPackOptions {
1033 fix_thin: true,
1034 skip_post_index_verify: true,
1035 threads: index_threads,
1036 merged_config: Some(pack_config),
1037 environment: fetch_opts.environment.clone(),
1038 mark_promisor_pack: fetch_opts.mark_received_pack_promisor,
1039 keep_message: None,
1040 trusted_index_rows: None,
1041 })
1042}
1043
1044struct LocalCopyProgress {
1046 total: u64,
1047 done: u64,
1048 last_reported: u64,
1049 phase_active: bool,
1050}
1051
1052struct CopyingLooseLink<'a> {
1054 copy: &'a mut LocalCopyProgress,
1055 progress: &'a mut dyn Progress,
1056}
1057
1058impl ProgressSink for CopyingLooseLink<'_> {
1059 fn inc(&mut self, n: u64) {
1060 self.copy.add(self.progress, n);
1061 }
1062}
1063
1064impl LocalCopyProgress {
1065 fn new(total: u64) -> Self {
1066 Self {
1067 total,
1068 done: 0,
1069 last_reported: 0,
1070 phase_active: false,
1071 }
1072 }
1073
1074 fn ensure_started(&mut self, progress: &mut dyn Progress) {
1075 if !self.phase_active {
1076 progress.start(ProgressPhase::CopyingObjects, Some(self.total));
1077 self.phase_active = true;
1078 }
1079 }
1080
1081 fn add(&mut self, progress: &mut dyn Progress, n: u64) {
1082 if n == 0 {
1083 return;
1084 }
1085 self.ensure_started(progress);
1086 self.done = self.done.saturating_add(n).min(self.total);
1087 if self.last_reported < self.done {
1088 crate::progress::report_object_progress_stride(
1089 progress,
1090 self.done,
1091 &mut self.last_reported,
1092 Some(self.total),
1093 );
1094 }
1095 if progress.wants_received_objects() {
1096 progress.received_objects(ReceivedObjects {
1097 received: self.done,
1098 total: self.total,
1099 deltas: 0,
1100 });
1101 }
1102 }
1103
1104 fn finish(&mut self, progress: &mut dyn Progress) {
1105 if !self.phase_active {
1106 return;
1107 }
1108 if self.last_reported < self.done {
1109 progress.set(self.done);
1110 self.last_reported = self.done;
1111 }
1112 progress.finish();
1113 self.phase_active = false;
1114 }
1115}
1116
1117#[allow(clippy::too_many_arguments)]
1123fn copy_objects_for_local_fetch(
1124 local_git_dir: &Path,
1125 remote_odb: &Odb,
1126 local_odb: &Odb,
1127 wants: &[ObjectId],
1128 haves: &[ObjectId],
1129 source_shallow: &HashSet<ObjectId>,
1130 have_shallow: &HashSet<ObjectId>,
1131 try_pack_link: bool,
1132 pack_build: &PackBuildOptions,
1133 fetch_opts: &FetchOptions,
1134 progress: &mut dyn Progress,
1135 cancel: &dyn crate::progress::Cancel,
1136 prep_enumerating: bool,
1137) -> Result<(HashSet<ObjectId>, TransferStats)> {
1138 use crate::local_object_copy::{
1139 count_store_objects_for_link, link_loose_objects, link_loose_objects_for_oids,
1140 link_packs_within, link_remote_pack_files, linked_object_set, refresh_local_odb_after_link,
1141 };
1142
1143 let remote_objects = remote_odb.objects_dir();
1144 let local_objects = local_odb.objects_dir();
1145 let pack_dir_before = pack_file_basenames(&local_objects.join("pack"));
1146 let remote_has_alternates = !crate::pack::read_alternates_recursive(remote_objects)?.is_empty();
1147
1148 if local_odb.uses_files_primary()
1149 && fetch_opts.link_source_objects
1150 && !remote_has_alternates
1151 && !pack_build.thin
1152 && try_pack_link
1153 && haves.is_empty()
1154 && !wants.is_empty()
1155 {
1156 crate::progress::poll_cancel(progress, cancel)?;
1157 let copy_total = count_store_objects_for_link(remote_objects)? as u64;
1158 let mut copy = LocalCopyProgress::new(copy_total);
1159 let linked_packs = link_remote_pack_files(
1160 remote_objects,
1161 local_objects,
1162 &mut crate::progress::NullProgress,
1163 cancel,
1164 0,
1165 )?;
1166 if linked_packs.objects > 0 {
1167 refresh_local_odb_after_link(local_odb);
1168 copy.add(progress, linked_packs.objects as u64);
1169 }
1170 let pack_alone_satisfies_wants =
1171 linked_packs.objects > 0 && wants.iter().all(|oid| local_odb.exists(oid));
1172 let linked_loose = link_loose_objects(
1173 remote_objects,
1174 local_objects,
1175 &mut crate::progress::NullProgress,
1176 cancel,
1177 )?;
1178 if linked_loose > 0 {
1179 refresh_local_odb_after_link(local_odb);
1180 copy.add(progress, linked_loose as u64);
1181 }
1182 if (linked_packs.files > 0 || linked_loose > 0)
1183 && wants.iter().all(|oid| local_odb.exists(oid))
1184 {
1185 copy.finish(progress);
1186 let stats_loose = if pack_alone_satisfies_wants {
1187 HashSet::new()
1188 } else {
1189 crate::local_object_copy::loose_reachable_from_roots(
1190 local_odb,
1191 wants,
1192 source_shallow,
1193 cancel,
1194 )?
1195 };
1196 let oids = if stats_loose.is_empty() {
1197 linked_object_set(local_odb, wants)
1198 } else {
1199 stats_loose.clone()
1200 };
1201 let mut transfer = transfer_stats_for_local_operation(
1202 local_odb,
1203 &stats_loose,
1204 None,
1205 &pack_dir_before,
1206 )?;
1207 enrich_local_link_transfer_kinds(
1208 local_odb,
1209 &pack_dir_before,
1210 &stats_loose,
1211 &mut transfer,
1212 )?;
1213 return Ok((oids, transfer));
1214 }
1215 copy.finish(progress);
1216 }
1217
1218 let stop = crate::progress::CancelSignal::new(cancel);
1219 let have_tips: HashSet<ObjectId> = haves.iter().copied().collect();
1220 if !prep_enumerating {
1221 progress.start(ProgressPhase::EnumeratingObjects, None);
1222 }
1223 let mut enum_last = 0u64;
1224 let send_set = collect_reachable_excluding_peer(
1225 remote_odb,
1226 wants,
1227 local_odb,
1228 &have_tips,
1229 false,
1230 source_shallow,
1231 &stop,
1232 progress,
1233 &mut enum_last,
1234 )?;
1235 let send = &send_set.objects;
1236 let send_len = send.len() as u64;
1237 progress.set(send_len);
1238
1239 let will_scan_packs =
1240 local_odb.uses_files_primary() && !pack_build.thin && try_pack_link && !send.is_empty();
1241 if !will_scan_packs {
1242 progress.finish();
1243 }
1244
1245 let mut copy = LocalCopyProgress::new(send_len);
1246
1247 if will_scan_packs {
1248 let local_has = |oid: &ObjectId| local_odb.exists_materialized_local_without_rescan(oid);
1249 let mut pack_scan = send_len;
1250 let mut pack_scan_last = send_len;
1251 let mut on_pack_scan = |n: u64| {
1252 pack_scan = pack_scan.saturating_add(n);
1253 crate::progress::report_object_progress_stride(
1254 progress,
1255 pack_scan,
1256 &mut pack_scan_last,
1257 None,
1258 );
1259 };
1260 let linked = link_packs_within(
1261 remote_objects,
1262 local_objects,
1263 send,
1264 &local_has,
1265 &mut crate::progress::NullProgress,
1266 cancel,
1267 Some(&mut on_pack_scan),
1268 )?;
1269 progress.finish();
1270 if linked > 0 {
1271 refresh_local_odb_after_link(local_odb);
1272 copy.add(progress, linked as u64);
1273 }
1274 }
1275
1276 let mut still_missing = objects_missing_from_peer(local_odb, send);
1277 if still_missing.is_empty() {
1278 copy.finish(progress);
1279 let oids: HashSet<ObjectId> = send.iter().copied().collect();
1281 let transfer = transfer_stats_for_local_operation(
1282 local_odb,
1283 &oids,
1284 Some(&HashSet::new()),
1285 &pack_dir_before,
1286 )?;
1287 return Ok((oids, transfer));
1288 }
1289
1290 let loose_candidates: HashSet<ObjectId> = still_missing.iter().copied().collect();
1294 if local_odb.uses_files_primary() {
1295 let mut loose_link = CopyingLooseLink {
1296 copy: &mut copy,
1297 progress,
1298 };
1299 let linked_loose = link_loose_objects_for_oids(
1300 remote_objects,
1301 local_objects,
1302 &still_missing,
1303 &mut loose_link,
1304 cancel,
1305 )?;
1306 if linked_loose > 0 {
1307 refresh_local_odb_after_link(local_odb);
1308 copy.add(progress, linked_loose as u64);
1309 still_missing = local_odb.missing_oids(still_missing)?;
1310 }
1311 }
1312 if still_missing.is_empty() {
1313 copy.finish(progress);
1314 let oids: HashSet<ObjectId> = send.iter().copied().collect();
1316 let transfer = transfer_stats_for_local_operation(
1317 local_odb,
1318 &oids,
1319 Some(&loose_candidates),
1320 &pack_dir_before,
1321 )?;
1322 return Ok((oids, transfer));
1323 }
1324
1325 let pack_opts = pack_build_options_for_send(remote_odb, &still_missing, pack_build);
1326 crate::progress::poll_cancel(progress, cancel)?;
1327 let mut spool = crate::pack_receive::TempPackReceive::create_in_pack_dir(local_objects)?;
1330 write_pack_for_local_transfer(
1331 remote_odb,
1332 &send_set,
1333 &still_missing,
1334 local_odb,
1335 haves,
1336 source_shallow,
1337 have_shallow,
1338 &pack_opts,
1339 &stop,
1340 &mut spool,
1341 Some((&mut copy, progress)),
1342 )?;
1343 copy.finish(progress);
1344 crate::progress::poll_cancel(progress, cancel)?;
1345 let ingested = match spool.finish()? {
1346 Some(pack_path) => {
1347 let ingest_opts = ingest_pack_options_for_fetch(local_git_dir, fetch_opts)?;
1348 crate::index_pack::ingest_received_pack_path_with_progress(
1349 pack_path,
1350 local_odb,
1351 &ingest_opts,
1352 Some(progress),
1353 Some(cancel),
1354 )?
1355 }
1356 None => crate::index_pack::IngestedPack::default(),
1357 };
1358 refresh_local_odb_after_link(local_odb);
1359 let mut out: HashSet<ObjectId> = send.iter().copied().collect();
1361 out.extend(ingested.object_ids);
1362 let transfer = transfer_stats_for_local_operation(local_odb, &out, None, &pack_dir_before)?;
1363 Ok((out, transfer))
1364}
1365
1366#[allow(clippy::too_many_arguments)]
1378fn copy_objects_for_local_push(
1379 local_odb: &Odb,
1380 peer_odb: &Odb,
1381 dest_odb: &Odb,
1382 wants: &[ObjectId],
1383 haves: &[ObjectId],
1384 source_shallow: &HashSet<ObjectId>,
1385 have_shallow: &HashSet<ObjectId>,
1386 opts: &PackBuildOptions,
1387 cancel: &dyn crate::progress::Cancel,
1388) -> Result<()> {
1389 use crate::local_object_copy::{
1390 link_loose_objects_for_oids, link_packs_within, refresh_local_odb_after_link,
1391 };
1392
1393 let local_objects = local_odb.objects_dir();
1394 let dest_objects = dest_odb.objects_dir();
1395 let stop = crate::progress::CancelSignal::new(cancel);
1396
1397 let have_tips: HashSet<ObjectId> = haves.iter().copied().collect();
1398 let mut enum_last = 0u64;
1399 let mut enum_sink = crate::progress::NullProgress;
1400 let send_set = collect_reachable_excluding_peer(
1401 local_odb,
1402 wants,
1403 peer_odb,
1404 &have_tips,
1405 false,
1406 source_shallow,
1407 &stop,
1408 &mut enum_sink,
1409 &mut enum_last,
1410 )?;
1411 let send = &send_set.objects;
1412
1413 let mut progress = crate::progress::NullProgress;
1414 let dest_has = |oid: &ObjectId| dest_odb.exists_materialized_local_without_rescan(oid);
1415 if local_odb.uses_files_primary()
1416 && !opts.thin
1417 && link_packs_within(
1418 local_objects,
1419 dest_objects,
1420 send,
1421 &dest_has,
1422 &mut progress,
1423 cancel,
1424 None,
1425 )? > 0
1426 {
1427 refresh_local_odb_after_link(dest_odb);
1428 }
1429
1430 let mut still_missing = objects_missing_from_peer(dest_odb, send);
1431 if still_missing.is_empty() {
1432 return Ok(());
1433 }
1434
1435 if local_odb.uses_files_primary() && !haves.is_empty() {
1436 let linked_loose = link_loose_objects_for_oids(
1437 local_objects,
1438 dest_objects,
1439 &still_missing,
1440 &mut progress,
1441 cancel,
1442 )?;
1443 if linked_loose > 0 {
1444 refresh_local_odb_after_link(dest_odb);
1445 still_missing = dest_odb.missing_oids(still_missing)?;
1446 }
1447 if still_missing.is_empty() {
1448 return Ok(());
1449 }
1450 }
1451
1452 let pack_opts = pack_build_options_for_send(local_odb, &still_missing, opts);
1453 let mut spool = crate::pack_receive::TempPackReceive::create_in_pack_dir(dest_objects)?;
1454 write_pack_for_local_transfer(
1455 local_odb,
1456 &send_set,
1457 &still_missing,
1458 peer_odb,
1459 haves,
1460 source_shallow,
1461 have_shallow,
1462 &pack_opts,
1463 &stop,
1464 &mut spool,
1465 None,
1466 )?;
1467 let Some(pack_path) = spool.finish()? else {
1468 return Ok(());
1469 };
1470 crate::index_pack::ingest_received_pack_path_with_progress(
1473 pack_path,
1474 dest_odb,
1475 &crate::index_pack::IngestPackOptions {
1476 fix_thin: pack_opts.thin,
1477 skip_post_index_verify: true,
1478 ..Default::default()
1479 },
1480 None,
1481 Some(cancel),
1482 )?;
1483 refresh_local_odb_after_link(dest_odb);
1484 Ok(())
1485}
1486
1487fn objects_missing_from_peer(peer_odb: &Odb, send: &[ObjectId]) -> Vec<ObjectId> {
1493 peer_odb.reprepare_local_packs();
1494 send.iter()
1495 .copied()
1496 .filter(|oid| !peer_odb.exists_materialized_local_without_rescan(oid))
1497 .collect()
1498}
1499
1500#[allow(clippy::too_many_arguments)]
1511fn write_pack_for_local_transfer(
1512 source_odb: &Odb,
1513 send_set: &PeerSendSet,
1514 missing: &[ObjectId],
1515 peer_odb: &Odb,
1516 haves: &[ObjectId],
1517 source_shallow: &HashSet<ObjectId>,
1518 have_shallow: &HashSet<ObjectId>,
1519 opts: &PackBuildOptions,
1520 stop: &crate::progress::CancelSignal<'_>,
1521 w: &mut dyn Write,
1522 copy: Option<(&mut LocalCopyProgress, &mut dyn Progress)>,
1523) -> Result<()> {
1524 if !opts.thin && have_shallow.is_empty() {
1525 let pack_budget = missing.len() as u64;
1526 if let Some((copy, progress)) = copy {
1527 let mut pack_reported = 0u64;
1528 let mut on_pack = |n: u64| {
1529 let add = n.min(pack_budget.saturating_sub(pack_reported));
1530 if add > 0 {
1531 pack_reported += add;
1532 copy.add(progress, add);
1533 }
1534 };
1535 crate::pack_objects::write_pack_from_send_list(
1536 source_odb,
1537 missing,
1538 opts,
1539 &send_set.name_hashes,
1540 stop,
1541 w,
1542 Some(&mut on_pack),
1543 )?;
1544 } else {
1545 crate::pack_objects::write_pack_from_send_list(
1546 source_odb,
1547 missing,
1548 opts,
1549 &send_set.name_hashes,
1550 stop,
1551 w,
1552 None,
1553 )?;
1554 }
1555 } else {
1556 write_pack_for_local_fetch(
1557 source_odb,
1558 missing,
1559 peer_odb,
1560 haves,
1561 source_shallow,
1562 have_shallow,
1563 opts,
1564 stop,
1565 w,
1566 )?;
1567 }
1568 Ok(())
1569}
1570
1571struct PeerSendSet {
1573 objects: Vec<ObjectId>,
1575 name_hashes: HashMap<ObjectId, u32>,
1577}
1578
1579#[allow(clippy::too_many_arguments)]
1584fn collect_reachable_excluding_peer(
1585 source_odb: &Odb,
1586 roots: &[ObjectId],
1587 peer_odb: &Odb,
1588 peer_boundary: &HashSet<ObjectId>,
1589 skip_missing: bool,
1590 shallow_grafts: &HashSet<ObjectId>,
1591 stop: &crate::progress::CancelSignal<'_>,
1592 enum_sink: &mut dyn ProgressSink,
1593 enum_last: &mut u64,
1594) -> Result<PeerSendSet> {
1595 use crate::pack_name_hash::pack_name_hash_extend;
1596
1597 stop.poll()?;
1598 if peer_boundary.is_empty() && shallow_grafts.is_empty() {
1599 if let Some(send_set) = bitmap_send_set(source_odb, roots, peer_odb, enum_sink, enum_last)?
1600 {
1601 stop.poll()?;
1602 return Ok(send_set);
1603 }
1604 }
1605
1606 let mut visited: HashSet<ObjectId> = HashSet::new();
1607 let mut ordered: Vec<ObjectId> = Vec::new();
1608 let mut name_hashes: HashMap<ObjectId, u32> = HashMap::new();
1609 let mut queue: VecDeque<(ObjectId, u32)> = VecDeque::new();
1611
1612 peer_odb.reprepare_local_packs();
1615 let at_boundary = |oid: ObjectId| {
1616 peer_boundary.contains(&oid) || peer_odb.exists_materialized_local_without_rescan(&oid)
1617 };
1618
1619 let enqueue = |oid: ObjectId,
1623 prefix_hash: Option<u32>,
1624 queue: &mut VecDeque<(ObjectId, u32)>,
1625 visited: &mut HashSet<ObjectId>,
1626 ordered: &mut Vec<ObjectId>|
1627 -> bool {
1628 if !visited.insert(oid) || at_boundary(oid) {
1629 return false;
1630 }
1631 ordered.push(oid);
1632 if let Some(prefix_hash) = prefix_hash {
1633 queue.push_back((oid, prefix_hash));
1634 }
1635 true
1636 };
1637
1638 for &root in roots {
1639 enqueue(root, Some(0), &mut queue, &mut visited, &mut ordered);
1640 }
1641
1642 while let Some((oid, prefix_hash)) = queue.pop_front() {
1643 stop.poll()?;
1644 let obj = match source_odb.read(&oid) {
1645 Ok(o) => o,
1646 Err(_) if skip_missing => continue,
1647 Err(e) => return Err(e),
1648 };
1649 match obj.kind {
1650 ObjectKind::Commit => {
1651 let commit = parse_commit(&obj.data)?;
1652 if !shallow_grafts.contains(&oid) {
1653 for parent in commit.parents {
1654 enqueue(parent, Some(0), &mut queue, &mut visited, &mut ordered);
1655 }
1656 }
1657 enqueue(commit.tree, Some(0), &mut queue, &mut visited, &mut ordered);
1658 }
1659 ObjectKind::Tree => {
1660 for entry in parse_tree(&obj.data)? {
1661 if entry.mode == 0o160000 {
1662 continue;
1663 }
1664 let path_hash = pack_name_hash_extend(prefix_hash, &entry.name);
1665 let queued = if entry.mode & 0o170000 == 0o040000 {
1666 let dir_hash = pack_name_hash_extend(path_hash, b"/");
1667 enqueue(
1668 entry.oid,
1669 Some(dir_hash),
1670 &mut queue,
1671 &mut visited,
1672 &mut ordered,
1673 )
1674 } else {
1675 enqueue(entry.oid, None, &mut queue, &mut visited, &mut ordered)
1676 };
1677 if queued {
1678 name_hashes.insert(entry.oid, path_hash);
1679 }
1680 }
1681 }
1682 ObjectKind::Tag => {
1683 let tag = parse_tag(&obj.data)?;
1684 enqueue(
1685 tag.object,
1686 Some(prefix_hash),
1687 &mut queue,
1688 &mut visited,
1689 &mut ordered,
1690 );
1691 }
1692 ObjectKind::Blob => {}
1693 }
1694 let n = ordered.len() as u64;
1695 crate::progress::report_object_progress_stride(enum_sink, n, enum_last, None);
1696 }
1697
1698 Ok(PeerSendSet {
1699 objects: ordered,
1700 name_hashes,
1701 })
1702}
1703
1704fn bitmap_send_set(
1709 source_odb: &Odb,
1710 roots: &[ObjectId],
1711 peer_odb: &Odb,
1712 enum_sink: &mut dyn ProgressSink,
1713 enum_last: &mut u64,
1714) -> Result<Option<PeerSendSet>> {
1715 let no_haves = HashSet::new();
1716 let Some((reachable, _)) = crate::pack_object_select::try_bitmap_reachable_for_upload(
1717 source_odb,
1718 roots,
1719 &[],
1720 None,
1721 &no_haves,
1722 )?
1723 else {
1724 return Ok(None);
1725 };
1726 peer_odb.reprepare_local_packs();
1727 let mut objects: Vec<ObjectId> = Vec::new();
1728 for oid in reachable
1729 .iter_grouped_by_kind()
1730 .filter(|oid| !peer_odb.exists_materialized_local_without_rescan(oid))
1731 {
1732 objects.push(oid);
1733 let n = objects.len() as u64;
1734 crate::progress::report_object_progress_stride(enum_sink, n, enum_last, None);
1735 }
1736 let name_hashes = crate::pack_object_select::name_hashes_for_send(
1737 reachable.bitmap_index(),
1738 &objects,
1739 &no_haves,
1740 );
1741 Ok(Some(PeerSendSet {
1742 objects,
1743 name_hashes,
1744 }))
1745}
1746
1747pub fn fetch_local(
1749 local_git_dir: &Path,
1750 remote_git_dir: &Path,
1751 opts: &FetchOptions,
1752) -> Result<FetchOutcome> {
1753 fetch_local_with_progress(
1754 local_git_dir,
1755 remote_git_dir,
1756 opts,
1757 &mut NoProgress,
1758 &crate::progress::NeverCancel,
1759 )
1760}
1761
1762pub(crate) struct FetchMatchSet {
1764 pub(crate) matched: Vec<MatchedRef>,
1765 pub(crate) positive: Vec<RefspecItem>,
1766 pub(crate) negatives: Vec<RefspecItem>,
1767 pub(crate) explicit_refspec_fetch: bool,
1768}
1769
1770pub(crate) fn build_fetch_matched_set(
1772 opts: &FetchOptions,
1773 remote_refs: &[(String, ObjectId)],
1774 remote_head_oid: Option<ObjectId>,
1775) -> Result<FetchMatchSet> {
1776 use crate::fetch_request::{FetchRefspecInput, FetchRefspecPlan, FetchRemoteAdvertisement};
1777
1778 let remote = FetchRemoteAdvertisement {
1779 refs: remote_refs,
1780 head_oid: remote_head_oid,
1781 };
1782 let plan = FetchRefspecPlan::resolve(
1783 &FetchRefspecInput {
1784 user_refspecs: opts.refspecs.clone(),
1785 configured_refspecs: opts.configured_refspecs.clone(),
1786 remote_name: opts.remote_name.clone(),
1787 allow_missing_explicit_for_prune: opts.prune,
1788 },
1789 remote,
1790 )?;
1791 let explicit_refspec_fetch = plan.is_explicit();
1792
1793 let mut negatives = match &plan {
1794 FetchRefspecPlan::Pattern { negatives, .. } | FetchRefspecPlan::Mixed { negatives, .. } => {
1795 negatives.clone()
1796 }
1797 };
1798 for spec in &opts.negative_refspecs {
1799 let item = parse_fetch_refspec(spec)
1800 .map_err(|e| Error::Message(format!("invalid negative refspec '{spec}': {e}")))?;
1801 negatives.push(item);
1802 }
1803
1804 let mut positive = match &plan {
1805 FetchRefspecPlan::Pattern { positive, .. } => positive.clone(),
1806 FetchRefspecPlan::Mixed {
1807 pattern_positive, ..
1808 } => {
1809 let mut positive = pattern_positive.clone();
1810 for spec in &opts.refspecs {
1811 if let Ok(item) = parse_fetch_refspec(spec) {
1812 if !item.negative && !item.pattern && !item.matching {
1813 positive.push(item);
1814 }
1815 }
1816 }
1817 positive
1818 }
1819 };
1820 if opts.prune {
1821 crate::fetch_request::supplement_positive_for_explicit_prune(
1822 &mut positive,
1823 &opts.configured_refspecs,
1824 opts.remote_name.as_deref(),
1825 );
1826 }
1827
1828 let mut matched =
1829 crate::fetch_request::matched_refs_with_negatives(&plan, remote, &negatives, opts.prune)?;
1830 if opts.force {
1831 for m in &mut matched {
1832 if !m.auto_followed {
1833 m.force = true;
1834 }
1835 }
1836 }
1837 Ok(FetchMatchSet {
1838 matched,
1839 positive,
1840 negatives,
1841 explicit_refspec_fetch,
1842 })
1843}
1844
1845pub(crate) fn enforce_fetch_ref_update(
1847 local_ref: &str,
1848 old: Option<ObjectId>,
1849 new: ObjectId,
1850 mode: UpdateMode,
1851 opts: &FetchOptions,
1852 explicit_refspec_fetch: bool,
1853 local_repo: Option<&crate::repo::Repository>,
1854) -> Result<()> {
1855 if opts.dry_run {
1856 return Ok(());
1857 }
1858 let would_write = matches!(
1859 mode,
1860 UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
1861 );
1862 if let Some(short) = local_ref.strip_prefix("refs/heads/") {
1863 if old.is_some()
1864 && old != Some(new)
1865 && (would_write || mode == UpdateMode::NonFastForwardRejected)
1866 {
1867 if let Some(repo) = local_repo {
1868 if crate::worktree::find_worktree_with_branch_checked_out(repo, short, None)?
1869 .is_some()
1870 {
1871 return Err(Error::FetchIntoCheckedOutBranch {
1872 branch: short.to_owned(),
1873 });
1874 }
1875 }
1876 }
1877 }
1878 let _ = explicit_refspec_fetch;
1879 Ok(())
1880}
1881
1882pub(crate) fn ensure_local_remote_repository(remote_git_dir: &Path) -> Result<()> {
1886 if crate::submodule_gitdir::is_valid_git_dir(remote_git_dir) {
1887 return Ok(());
1888 }
1889 Err(Error::remote_unreadable(
1890 remote_git_dir.display().to_string(),
1891 RemoteUnreadableReason::RepositoryNotFound,
1892 ))
1893}
1894
1895pub fn fetch_local_with_progress(
1900 local_git_dir: &Path,
1901 remote_git_dir: &Path,
1902 opts: &FetchOptions,
1903 progress: &mut dyn Progress,
1904 cancel: &dyn crate::progress::Cancel,
1905) -> Result<FetchOutcome> {
1906 crate::progress::poll_cancel(progress, cancel)?;
1907 ensure_local_remote_repository(remote_git_dir)?;
1908 let local = crate::fetch::open_fetch_target(local_git_dir, opts)?;
1909 fetch_local_into(&local, remote_git_dir, opts, progress, cancel)
1910}
1911
1912pub(crate) fn fetch_local_into(
1914 local: &crate::repo::Repository,
1915 remote_git_dir: &Path,
1916 opts: &FetchOptions,
1917 progress: &mut dyn Progress,
1918 cancel: &dyn crate::progress::Cancel,
1919) -> Result<FetchOutcome> {
1920 crate::progress::poll_cancel(progress, cancel)?;
1921 ensure_local_remote_repository(remote_git_dir)?;
1922 let local_git_dir = local.git_dir();
1923 validate_fetch_depth_local(local_git_dir, opts)?;
1924
1925 let remote_odb = open_odb(remote_git_dir);
1926 let remote_algo = remote_odb.hash_algo();
1927 let reopened = sync_empty_destination_object_format(local_git_dir, remote_algo, opts)?
1928 .then(|| reopen_after_format_change(local))
1929 .transpose()?;
1930 let local = reopened.as_ref().unwrap_or(local);
1931 let local_git_dir = local.git_dir();
1932 let local_odb = local.odb();
1933
1934 let remote_entries = crate::remote::list_refs_from_git_dir_for_fetch(
1936 remote_git_dir,
1937 Some(&remote_odb),
1938 &opts.refspecs,
1939 )
1940 .map_err(crate::error::Error::from)?;
1941
1942 let mut default_branch = None;
1943 let mut remote_head_oid = None;
1944 let mut remote_refs: Vec<(String, ObjectId)> = Vec::new();
1946 for entry in &remote_entries {
1947 if entry.name == "HEAD" {
1948 remote_head_oid = Some(entry.oid);
1949 default_branch = entry
1950 .symref_target
1951 .as_ref()
1952 .map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned());
1953 continue;
1954 }
1955 if entry.name.ends_with("^{}") {
1956 continue;
1957 }
1958 if !crate::refs::is_valid_fetch_advertised_ref(&entry.name) {
1959 continue;
1960 }
1961 remote_refs.push((entry.name.clone(), entry.oid));
1962 }
1963
1964 let FetchMatchSet {
1965 mut matched,
1966 positive,
1967 negatives,
1968 explicit_refspec_fetch,
1969 } = build_fetch_matched_set(opts, &remote_refs, remote_head_oid)?;
1970 let mut matched_oids: HashSet<ObjectId> = matched.iter().map(|m| m.oid).collect();
1971 let mut seen_remote_ref: HashSet<String> =
1972 matched.iter().map(|m| m.remote_ref.clone()).collect();
1973
1974 let local_shallow = crate::shallow::load_shallow_boundaries(local_git_dir);
1975 let remote_shallow = crate::shallow::load_shallow_boundaries(remote_git_dir);
1976
1977 let mut prep_enumerating = false;
1978 if !opts.dry_run {
1979 progress.start(ProgressPhase::EnumeratingObjects, None);
1980 prep_enumerating = true;
1981 }
1982
1983 if opts.tags == TagMode::All {
1986 apply_tag_mode(
1987 opts.tags,
1988 &remote_refs,
1989 &remote_odb,
1990 &remote_shallow,
1991 &negatives,
1992 &HashSet::new(),
1993 opts.force,
1994 &mut matched,
1995 &mut matched_oids,
1996 &mut seen_remote_ref,
1997 )?;
1998 }
1999 let shallow_response = if opts.has_deepen_request() {
2000 let shallow_wants: HashSet<ObjectId> = matched
2001 .iter()
2002 .map(|m| shallow_want_commit_tip(&remote_odb, m))
2003 .collect::<Result<_>>()?;
2004 let remote_repo =
2005 crate::repo::Repository::open(remote_git_dir, &RepositoryOptions::empty())?;
2006 let (wire_depth, deepen_relative) = opts
2007 .wire_deepen()
2008 .map(|(d, r)| (Some(d), r))
2009 .unwrap_or((None, false));
2010 let req = crate::serve::ShallowRequest {
2011 client_shallow: local_shallow.iter().copied().collect(),
2012 depth: wire_depth,
2013 deepen_since: opts
2014 .deepen_since
2015 .as_deref()
2016 .and_then(|s| s.parse::<i64>().ok()),
2017 deepen_not: Vec::new(),
2018 deepen_relative,
2019 };
2020 Some(crate::serve::compute_shallow_response(
2021 &remote_repo,
2022 &shallow_wants.into_iter().collect::<Vec<_>>(),
2023 &req,
2024 )?)
2025 } else {
2026 None
2027 };
2028 let copy_source_shallow = shallow_response
2029 .as_ref()
2030 .map(|resp| resp.pack_shallow_grafts.clone())
2031 .unwrap_or_else(|| remote_shallow.clone());
2032 let mut tag_shallow = copy_source_shallow.clone();
2033 if shallow_response.is_none() {
2034 tag_shallow.extend(local_shallow.iter().copied());
2035 }
2036
2037 let following_only = if opts.tags == TagMode::Following {
2038 let tags_present = crate::fetch::tags_already_local(local.refs(), &remote_refs);
2039 apply_tag_mode(
2040 opts.tags,
2041 &remote_refs,
2042 &remote_odb,
2043 &tag_shallow,
2044 &negatives,
2045 &tags_present,
2046 opts.force,
2047 &mut matched,
2048 &mut matched_oids,
2049 &mut seen_remote_ref,
2050 )?
2051 } else {
2052 HashSet::new()
2053 };
2054
2055 let mut following_tag_wants = if shallow_response.is_some() {
2056 local_odb
2059 .missing_oids(
2060 matched
2061 .iter()
2062 .filter(|m| {
2063 following_only.contains(&m.oid)
2064 && m.advertised_peel.is_some_and(|peel| peel != m.oid)
2065 })
2066 .map(|m| m.oid),
2067 )?
2068 .into_iter()
2069 .collect()
2070 } else {
2071 crate::fetch::following_tag_wants_with_local_peel(&matched, &following_only, local_odb)?
2072 };
2073 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
2074 if shallow_request && shallow_response.is_none() {
2075 let fetch_commit_oids = matched
2076 .iter()
2077 .filter(|m| !m.is_tag)
2078 .map(|m| m.oid)
2079 .collect();
2080 let peel_set = crate::fetch::following_peel_commit_set_for_shallow(
2081 &remote_odb,
2082 &fetch_commit_oids,
2083 opts.absolute_depth_for_tags(),
2084 &tag_shallow,
2085 )?;
2086 following_tag_wants.extend(
2087 crate::fetch::following_tag_wants_peel_among_fetched_commits(
2088 &matched,
2089 &following_only,
2090 &peel_set,
2091 ),
2092 );
2093 }
2094
2095 let mut wants: Vec<ObjectId> = if shallow_request {
2097 matched_oids
2098 .iter()
2099 .copied()
2100 .filter(|oid| !following_only.contains(oid) || following_tag_wants.contains(oid))
2101 .collect()
2102 } else {
2103 local_odb.missing_oids(matched_oids.iter().copied())?
2104 };
2105 let (shallow_new, shallow_unshallow) = if let Some(resp) = shallow_response {
2106 for oid in resp.extra_wants.iter().chain(resp.shallow.iter()).copied() {
2107 if !wants.contains(&oid) {
2108 wants.push(oid);
2109 }
2110 }
2111 (resp.shallow, resp.unshallow)
2112 } else {
2113 (Vec::new(), Vec::new())
2114 };
2115 let mut pack_oids: HashSet<ObjectId> = HashSet::new();
2116 let mut transfer = TransferStats::default();
2117 if wants.is_empty() && prep_enumerating {
2118 progress.finish();
2119 prep_enumerating = false;
2120 }
2121 if !wants.is_empty() && !opts.dry_run {
2122 let mut haves: Vec<ObjectId> = Vec::new();
2124 let mut have_seen: HashSet<ObjectId> = HashSet::new();
2125 for m in &matched {
2126 if let Some(local_ref) = &m.local_ref {
2127 if let Ok(old) = local.resolve_ref_oid(local_ref) {
2128 if have_seen.insert(old) {
2129 haves.push(old);
2130 }
2131 }
2132 }
2133 }
2134
2135 let remote_cfg = config_for_git_dir(remote_git_dir);
2136 let pack_opts = PackBuildOptions::for_local_copy(remote_cfg.as_ref());
2137 let (oids, fetch_transfer) = copy_objects_for_local_fetch(
2138 local_git_dir,
2139 &remote_odb,
2140 local_odb,
2141 &wants,
2142 &haves,
2143 ©_source_shallow,
2144 &local_shallow,
2145 !opts.has_deepen_request(),
2146 &pack_opts,
2147 opts,
2148 progress,
2149 cancel,
2150 prep_enumerating,
2151 )?;
2152 pack_oids = oids;
2153 transfer = fetch_transfer;
2154 }
2155
2156 if opts.initial_remote_fetch
2157 && !remote_shallow.is_empty()
2158 && !opts.has_deepen_request()
2159 && !opts.dry_run
2160 {
2161 let remote_shallow_vec: Vec<ObjectId> = remote_shallow.iter().copied().collect();
2162 crate::shallow::apply_shallow_updates(local_git_dir, &remote_shallow_vec, &[])?;
2163 }
2164 if (!shallow_new.is_empty() || !shallow_unshallow.is_empty()) && !opts.dry_run {
2165 crate::shallow::apply_shallow_updates(local_git_dir, &shallow_new, &shallow_unshallow)?;
2166 }
2167
2168 if opts.tags == TagMode::Following {
2169 let boundaries = crate::shallow::load_shallow_boundaries(local_git_dir);
2170 crate::fetch::retain_following_tags(local_odb, &mut matched, &pack_oids, &boundaries)?;
2171 }
2172
2173 let dry_run_remote = if opts.dry_run {
2176 crate::repo::Repository::open(remote_git_dir, &RepositoryOptions::empty()).ok()
2177 } else {
2178 None
2179 };
2180 let classify_repo = if opts.dry_run {
2181 dry_run_remote.as_ref()
2182 } else {
2183 Some(local)
2184 };
2185
2186 let mut updates: Vec<RefUpdate> = Vec::new();
2187
2188 if opts.prune {
2193 prune_tracking_refs(local, &positive, &remote_refs, opts.dry_run, &mut updates)?;
2194 }
2195
2196 let mut store_batch: Vec<(String, crate::refs::store::RawRef)> =
2197 Vec::with_capacity(matched.len());
2198 for m in &matched {
2199 let Some(local_ref) = &m.local_ref else {
2200 updates.push(RefUpdate {
2202 remote_ref: m.remote_ref.clone(),
2203 local_ref: None,
2204 old_oid: None,
2205 new_oid: Some(m.oid),
2206 mode: UpdateMode::NoChangeNeeded,
2207 note: Some("not stored (empty destination)".to_owned()),
2208 });
2209 continue;
2210 };
2211
2212 let old = local.resolve_ref_oid(local_ref).ok();
2213 let mode = classify_update(
2214 old.as_ref(),
2215 &m.oid,
2216 m.force,
2217 m.dest_is_tag,
2218 classify_repo,
2219 None,
2220 );
2221
2222 let write = matches!(
2223 mode,
2224 UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
2225 );
2226 enforce_fetch_ref_update(
2227 local_ref,
2228 old,
2229 m.oid,
2230 mode,
2231 opts,
2232 explicit_refspec_fetch,
2233 classify_repo,
2234 )?;
2235 if write && !opts.dry_run {
2236 if !crate::refs::is_valid_storable_ref_name(local_ref) {
2237 updates.push(RefUpdate {
2238 remote_ref: m.remote_ref.clone(),
2239 local_ref: Some(local_ref.clone()),
2240 old_oid: old,
2241 new_oid: Some(m.oid),
2242 mode,
2243 note: Some("skipped (invalid ref name)".to_owned()),
2244 });
2245 continue;
2246 }
2247 if m.dest_is_tag && !ref_target_exists(local_odb, &remote_odb, m.oid)? {
2248 updates.push(RefUpdate {
2249 remote_ref: m.remote_ref.clone(),
2250 local_ref: Some(local_ref.clone()),
2251 old_oid: old,
2252 new_oid: Some(m.oid),
2253 mode: UpdateMode::NoChangeNeeded,
2254 note: Some("skipped (tag target not present locally)".to_owned()),
2255 });
2256 continue;
2257 }
2258 store_batch.push((local_ref.clone(), crate::refs::store::RawRef::Direct(m.oid)));
2259 }
2260
2261 updates.push(RefUpdate {
2262 remote_ref: m.remote_ref.clone(),
2263 local_ref: Some(local_ref.clone()),
2264 old_oid: old,
2265 new_oid: Some(m.oid),
2266 mode,
2267 note: None,
2268 });
2269 }
2270
2271 if let Some(remote) = opts.remote_name.as_deref() {
2272 if !opts.dry_run {
2273 if let Some(branch) = default_branch.as_deref() {
2274 let head_ref = format!("refs/remotes/{remote}/HEAD");
2275 if crate::refs::resolve_ref_in(local, &head_ref).is_err() {
2276 let target = format!("refs/remotes/{remote}/{branch}");
2277 let target_will_exist = store_batch.iter().any(|(name, _)| *name == target)
2278 || crate::refs::resolve_ref_in(local, &target).is_ok();
2279 if target_will_exist {
2280 store_batch.push((head_ref, crate::refs::store::RawRef::Symbolic(target)));
2281 }
2282 }
2283 }
2284 }
2285 }
2286 if !opts.dry_run && !store_batch.is_empty() {
2287 let fetch_reflog = crate::fetch::prepare_fetch_apply_reflog(local, opts)?;
2288 let fetch_reflog_ctx = fetch_reflog
2289 .as_ref()
2290 .map(crate::fetch::PreparedFetchReflog::context);
2291 apply_fetch_ref_updates(
2292 local,
2293 store_batch,
2294 &mut updates,
2295 FetchRefRejectHint {
2296 prune: opts.prune,
2297 positive: &positive,
2298 remote_refs: &remote_refs,
2299 remote_name: opts.remote_name.as_deref(),
2300 },
2301 opts.initial_remote_fetch,
2302 fetch_reflog_ctx.as_ref(),
2303 )?;
2304 }
2305
2306 crate::fetch::finish_initial_remote_fetch_layout(local, opts, default_branch.as_deref())?;
2307 if let Some(remote) = opts.remote_name.as_deref() {
2308 crate::fetch::ensure_remote_tracking_head_symref(
2309 local,
2310 remote,
2311 default_branch.as_deref(),
2312 opts.dry_run,
2313 )?;
2314 }
2315 finish_anonymous_url_fetch(
2316 local_git_dir,
2317 opts,
2318 explicit_refspec_fetch,
2319 default_branch.as_deref(),
2320 &remote_refs,
2321 &matched,
2322 )?;
2323 let new_shallow = crate::shallow::dedupe_shallow_list(if opts.has_deepen_request() {
2324 shallow_new
2325 } else if opts.initial_remote_fetch {
2326 remote_shallow.iter().copied().collect()
2327 } else {
2328 Vec::new()
2329 });
2330 Ok(FetchOutcome {
2331 updates,
2332 default_branch,
2333 new_shallow,
2334 new_unshallow: shallow_unshallow,
2335 transfer,
2336 filter_sent: false,
2337 promisor: None,
2338 })
2339}
2340
2341pub fn push_local(
2408 local_git_dir: &Path,
2409 remote_git_dir: &Path,
2410 refs: &[PushRefSpec],
2411 opts: &PushOptions,
2412 cancel: &dyn crate::progress::Cancel,
2413) -> Result<PushOutcome> {
2414 let local = crate::repo::Repository::open(local_git_dir, &RepositoryOptions::empty())?;
2415 push_local_from(
2416 &local,
2417 remote_git_dir,
2418 refs,
2419 opts,
2420 &mut crate::fetch::NoProgress,
2421 cancel,
2422 None,
2423 )
2424}
2425
2426pub(crate) fn push_local_from(
2431 local: &crate::repo::Repository,
2432 remote_git_dir: &Path,
2433 refs: &[PushRefSpec],
2434 opts: &PushOptions,
2435 progress: &mut dyn crate::fetch::Progress,
2436 cancel: &dyn crate::progress::Cancel,
2437 pre_push: Option<&PrePushGate<'_>>,
2438) -> Result<PushOutcome> {
2439 let local_git_dir = local.git_dir();
2440 let mut progress_sink = crate::progress::NullProgress;
2441 crate::progress::poll_cancel(&mut progress_sink, cancel)?;
2442 ensure_local_remote_repository(remote_git_dir)?;
2443 let remote_repo =
2444 crate::repo::Repository::open(remote_git_dir, &local.peer_repository_options())?;
2445 let remote_cfg = remote_repo
2446 .config()
2447 .unwrap_or_else(|_| std::sync::Arc::new(crate::config::ConfigSet::new()));
2448 if !opts.push_options.is_empty()
2449 && !remote_cfg
2450 .get_bool("receive.advertisepushoptions")
2451 .and_then(|r| r.ok())
2452 .unwrap_or(false)
2453 {
2454 return Err(Error::PushOptionsUnsupported);
2455 }
2456
2457 let local_odb = local.odb();
2458
2459 let remote_have_tips: Vec<ObjectId> = match &opts.remote_ref_tips {
2462 Some(tips) => tips.clone(),
2463 None => crate::refs::list_refs(remote_git_dir, "refs/")?
2464 .into_iter()
2465 .map(|(_, oid)| oid)
2466 .collect(),
2467 };
2468
2469 let mut decisions: Vec<PushDecision> = Vec::with_capacity(refs.len());
2471 for spec in refs {
2472 decisions.push(decide_push(spec, local_odb, remote_git_dir, Some(local))?);
2473 }
2474 if let Some(gate) = pre_push {
2475 gate.check(refs.iter().zip(decisions.iter().map(|d| &d.result)))?;
2476 }
2477
2478 let push_wants: Vec<ObjectId> = decisions
2479 .iter()
2480 .filter(|d| d.apply && !opts.dry_run)
2481 .filter_map(|d| match &d.action {
2482 PushAction::Update(src) => Some(*src),
2483 _ => None,
2484 })
2485 .collect();
2486 let mut quarantine = None;
2489 if !push_wants.is_empty() {
2490 crate::progress::poll_cancel(&mut progress_sink, cancel)?;
2491 let local_cfg = config_for_git_dir(local_git_dir);
2492 let pack_opts = if remote_have_tips.is_empty() {
2493 PackBuildOptions::for_local_copy(local_cfg.as_ref())
2494 } else {
2495 PackBuildOptions::for_local_push(local_cfg.as_ref())
2496 };
2497 let empty_shallow = HashSet::new();
2498 let remote_shallow = crate::shallow::load_shallow_boundaries(remote_git_dir);
2499 let q = crate::receive_quarantine::ReceiveQuarantine::create(
2500 remote_repo.odb(),
2501 remote_git_dir,
2502 )?;
2503 let q_odb = q.odb();
2504 copy_objects_for_local_push(
2505 local_odb,
2506 remote_repo.odb(),
2507 &q_odb,
2508 &push_wants,
2509 &remote_have_tips,
2510 &empty_shallow,
2511 &remote_shallow,
2512 &pack_opts,
2513 cancel,
2514 )?;
2515 quarantine = Some((q, q_odb));
2516 }
2517
2518 let received: Vec<crate::serve::RefCommand> = if opts.dry_run {
2521 Vec::new()
2522 } else {
2523 decisions
2524 .iter()
2525 .filter(|d| d.apply)
2526 .filter_map(PushDecision::ref_command)
2527 .collect()
2528 };
2529
2530 #[cfg(test)]
2531 tests::run_push_hook(tests::PushHookPoint::BeforeReceivePolicy);
2532
2533 apply_remote_receive_policy(
2534 &remote_repo,
2535 local_git_dir,
2536 opts.dry_run,
2537 quarantine.as_ref().map(|(_, odb)| odb),
2538 &mut decisions,
2539 )?;
2540
2541 if opts.atomic && decisions.iter().any(|d| d.result.status.is_error()) {
2542 fail_atomic_push(&mut decisions);
2543 return Ok(PushOutcome {
2544 results: decisions.into_iter().map(|d| d.result).collect(),
2545 remote_messages: Vec::new(),
2546 });
2547 }
2548
2549 let scripts =
2550 crate::serve::ReceiveHookScripts::new(&remote_repo, &opts.push_options, Vec::new());
2551 let mut remote_messages = Vec::new();
2552 if !received.is_empty() {
2553 let commands: Vec<&crate::serve::RefCommand> = received.iter().collect();
2554 let pre = scripts.pre_receive(&commands, quarantine.as_ref().map(|(q, _)| q));
2555 relay_hook_output(progress, &mut remote_messages, &pre);
2556 if !pre.is_ok() {
2557 for d in decisions.iter_mut().filter(|d| d.apply) {
2558 d.refuse(&crate::serve::RefUpdateRefusal::PreReceiveHookDeclined);
2559 }
2560 }
2561 }
2562 let mut migrated_pack_keeps = Vec::new();
2563 if decisions.iter().any(|d| d.apply) && !opts.dry_run {
2564 if let Some((mut q, _)) = quarantine.take() {
2565 migrated_pack_keeps = q.migrate()?;
2566 remote_repo.odb().invalidate_packs();
2567 }
2568 for d in decisions.iter_mut().filter(|d| d.apply) {
2569 let Some(cmd) = d.ref_command() else {
2570 continue;
2571 };
2572 let update = scripts.update(&cmd);
2573 relay_hook_output(progress, &mut remote_messages, &update);
2574 if !update.is_ok() {
2575 d.refuse(&crate::serve::RefUpdateRefusal::UpdateHookDeclined);
2576 let line = format!("error: hook declined to update {}", d.result.remote_ref);
2579 progress.message(format!("{line}\n").as_bytes());
2580 remote_messages.push(line);
2581 }
2582 }
2583 if opts.atomic && decisions.iter().any(|d| d.result.status.is_error()) {
2584 fail_atomic_push(&mut decisions);
2585 }
2586 }
2587
2588 #[cfg(test)]
2589 tests::run_push_hook(tests::PushHookPoint::BeforeRefCommit);
2590
2591 let mut ref_batch: Vec<crate::refs::store::RefUpdate> = Vec::new();
2593 let mut non_atomic_batch: Vec<(usize, crate::refs::store::RefUpdate)> = Vec::new();
2594 for (idx, d) in decisions.iter_mut().enumerate() {
2595 if !d.apply || opts.dry_run {
2596 continue;
2597 }
2598 match &d.action {
2599 PushAction::Update(src) => {
2600 let update = push_store_ref_update(
2601 d.result.remote_ref.as_str(),
2602 Some(*src),
2603 d.result.old_oid,
2604 );
2605 if opts.atomic {
2606 ref_batch.push(update);
2607 } else {
2608 non_atomic_batch.push((idx, update));
2609 }
2610 }
2611 PushAction::Delete => {
2612 let update =
2613 push_store_ref_update(d.result.remote_ref.as_str(), None, d.result.old_oid);
2614 if opts.atomic {
2615 ref_batch.push(update);
2616 } else {
2617 non_atomic_batch.push((idx, update));
2618 }
2619 }
2620 PushAction::None => {}
2621 }
2622 }
2623 if opts.atomic && !opts.dry_run && !ref_batch.is_empty() {
2624 if let Err(err) = crate::refs::commit_ref_store_batch(remote_git_dir, &ref_batch) {
2625 apply_atomic_batch_ref_store_error(&mut decisions, &ref_batch, err);
2626 }
2627 } else if !opts.dry_run && !non_atomic_batch.is_empty() {
2628 let indices: Vec<_> = non_atomic_batch.iter().map(|(idx, _)| *idx).collect();
2629 let updates: Vec<_> = non_atomic_batch.into_iter().map(|(_, u)| u).collect();
2630 match crate::refs::commit_ref_updates_non_atomic_on_store(remote_repo.refs(), updates) {
2631 Ok(outcomes) => {
2632 for (idx, outcome) in indices.into_iter().zip(outcomes) {
2633 if let Err(err) = outcome.result {
2634 apply_ref_store_error_to_push_decision(
2635 &mut decisions[idx],
2636 &Error::RefStore(err),
2637 false,
2638 );
2639 }
2640 }
2641 }
2642 Err(err) => {
2643 for idx in indices {
2644 apply_ref_store_error_to_push_decision(&mut decisions[idx], &err, false);
2645 }
2646 }
2647 }
2648 }
2649
2650 if !opts.dry_run {
2651 let applied: Vec<crate::serve::RefCommand> = decisions
2652 .iter()
2653 .filter(|d| d.apply)
2654 .filter_map(PushDecision::ref_command)
2655 .collect();
2656 if !applied.is_empty() {
2657 let applied: Vec<&crate::serve::RefCommand> = applied.iter().collect();
2658 relay_hook_output(
2659 progress,
2660 &mut remote_messages,
2661 &scripts.post_receive(&applied),
2662 );
2663 relay_hook_output(
2664 progress,
2665 &mut remote_messages,
2666 &scripts.post_update(&applied),
2667 );
2668 }
2669 }
2670
2671 let results: Vec<_> = decisions.into_iter().map(|d| d.result).collect();
2672 if !opts.dry_run {
2673 if let Some(remote) = opts.tracking_remote.as_deref() {
2674 crate::branch_tracking::apply_push_remote_tracking_updates_in(local, remote, &results)?;
2675 }
2676 }
2677
2678 drop(migrated_pack_keeps);
2679
2680 Ok(PushOutcome {
2681 results,
2682 remote_messages,
2683 })
2684}
2685
2686fn fail_atomic_push(decisions: &mut [PushDecision]) {
2689 for d in decisions {
2690 if matches!(d.result.status, PushRefStatus::Ok) {
2691 d.result.status = PushRefStatus::AtomicPushFailed;
2692 d.apply = false;
2693 }
2694 }
2695}
2696
2697fn relay_hook_output(
2701 progress: &mut dyn crate::fetch::Progress,
2702 messages: &mut Vec<String>,
2703 result: &crate::hooks::HookResult,
2704) {
2705 use crate::sideband_lines::{RemoteLine, SidebandLines};
2706
2707 let Some(output) = result.output() else {
2708 return;
2709 };
2710 let mut record = |line: RemoteLine| {
2711 if let RemoteLine::Line(text) = line {
2712 if !text.is_empty() {
2713 messages.push(text);
2714 }
2715 }
2716 };
2717 for stream in [&output.stdout, &output.stderr] {
2718 if !stream.is_empty() {
2719 progress.message(stream);
2720 }
2721 let mut lines = SidebandLines::new();
2722 lines.feed(stream, &mut record);
2723 lines.finish(&mut record);
2724 }
2725}
2726
2727fn push_policy_object_lookup(remote_git_dir: &Path, local_git_dir: &Path) -> Odb {
2734 Odb::new(&objects_dir(local_git_dir)).with_env_alternate_dirs(vec![objects_dir(remote_git_dir)])
2735}
2736
2737fn apply_remote_receive_policy(
2742 remote_repo: &crate::repo::Repository,
2743 local_git_dir: &Path,
2744 dry_run: bool,
2745 quarantine: Option<&Odb>,
2746 decisions: &mut [PushDecision],
2747) -> Result<()> {
2748 if !decisions.iter().any(|d| d.apply) {
2749 return Ok(());
2750 }
2751 let cfg = remote_repo
2752 .config()
2753 .unwrap_or_else(|_| std::sync::Arc::new(crate::config::ConfigSet::new()));
2754 let policy = crate::serve::ReceivePolicy::from_config(cfg.as_ref());
2755 let hidden_refs = crate::hide_refs::hide_ref_patterns_receive(cfg.as_ref());
2756 let head_branch = crate::serve::head_branch(remote_repo);
2757 let checked_out = crate::serve::checked_out_branch(remote_repo);
2758 let branches = crate::serve::HeadBranches {
2759 head: head_branch.as_deref(),
2760 checked_out: checked_out.as_deref(),
2761 };
2762 let pre_copy_lookup =
2763 dry_run.then(|| push_policy_object_lookup(remote_repo.git_dir(), local_git_dir));
2764 let object_lookup = pre_copy_lookup
2765 .as_ref()
2766 .or(quarantine)
2767 .unwrap_or(remote_repo.odb());
2768
2769 for d in decisions.iter_mut().filter(|d| d.apply) {
2770 let Some(cmd) = d.ref_command() else {
2771 continue;
2772 };
2773 let refusal = match crate::serve::validate_ref_update(
2774 remote_repo,
2775 Some(object_lookup),
2776 &cmd,
2777 &policy,
2778 &hidden_refs,
2779 branches,
2780 ) {
2781 Ok(r) => r,
2782 Err(crate::serve::ServeError::Repository(e)) => return Err(e),
2783 Err(e) => return Err(Error::Message(e.to_string())),
2784 };
2785 if let Some(reason) = refusal {
2786 d.result.status = match (&reason, cmd.old) {
2787 (crate::serve::RefUpdateRefusal::StaleInfo, None) => {
2788 PushRefStatus::RejectAlreadyExists
2789 }
2790 (crate::serve::RefUpdateRefusal::StaleInfo, Some(_)) => PushRefStatus::RejectStale,
2791 _ => PushRefStatus::RemoteRejected,
2792 };
2793 d.result.message = Some(match d.result.status {
2794 PushRefStatus::RejectAlreadyExists => "already exists".to_owned(),
2795 _ => reason.to_string(),
2796 });
2797 d.apply = false;
2798 d.action = PushAction::None;
2799 }
2800 }
2801 Ok(())
2802}
2803
2804fn push_store_ref_update(
2809 refname: &str,
2810 new_oid: Option<ObjectId>,
2811 observed_old: Option<ObjectId>,
2812) -> crate::refs::store::RefUpdate {
2813 crate::refs::store::RefUpdate {
2814 name: refname.to_owned(),
2815 new_value: new_oid.map(crate::refs::store::RawRef::Direct),
2816 expected: match observed_old {
2817 Some(oid) => crate::refs::store::Expected::Oid(oid),
2818 None => crate::refs::store::Expected::Missing,
2819 },
2820 reflog: None,
2821 flags: Default::default(),
2822 }
2823}
2824
2825fn push_status_for_ref_store_error(err: &Error) -> PushRefStatus {
2826 match err {
2827 Error::RefStore(crate::refs::store::RefStoreError::ExpectedMismatch {
2828 expected: crate::refs::store::Expected::Missing,
2829 ..
2830 }) => PushRefStatus::RejectAlreadyExists,
2831 Error::RefStore(crate::refs::store::RefStoreError::ExpectedMismatch { .. }) => {
2832 PushRefStatus::RejectStale
2833 }
2834 _ => PushRefStatus::RemoteRejected,
2835 }
2836}
2837
2838fn push_message_for_ref_store_error(err: &Error) -> String {
2840 match push_status_for_ref_store_error(err) {
2841 PushRefStatus::RejectAlreadyExists => "already exists".to_owned(),
2842 _ => err.to_string(),
2843 }
2844}
2845
2846fn ref_store_error_refname(err: &Error) -> Option<String> {
2847 let Error::RefStore(store_err) = err else {
2848 return None;
2849 };
2850 Some(match store_err {
2851 crate::refs::store::RefStoreError::ExpectedMismatch { name, .. }
2852 | crate::refs::store::RefStoreError::DuplicateUpdate { name }
2853 | crate::refs::store::RefStoreError::LockHeld { name }
2854 | crate::refs::store::RefStoreError::InvalidRefName { name }
2855 | crate::refs::store::RefStoreError::InvalidSymrefTarget { name, .. }
2856 | crate::refs::store::RefStoreError::NullOid { name }
2857 | crate::refs::store::RefStoreError::MissingObject { name, .. }
2858 | crate::refs::store::RefStoreError::NotACommit { name, .. }
2859 | crate::refs::store::RefStoreError::ObjectLookup { name, .. }
2860 | crate::refs::store::RefStoreError::NotFound { name }
2861 | crate::refs::store::RefStoreError::ReflogRewriteStale { name } => name.clone(),
2862 crate::refs::store::RefStoreError::NameUnavailable { reason } => match reason {
2863 crate::refs::RefnameUnavailable::SameBatch { refname, .. }
2864 | crate::refs::RefnameUnavailable::AncestorExists {
2865 new_ref: refname, ..
2866 }
2867 | crate::refs::RefnameUnavailable::DescendantExists {
2868 new_ref: refname, ..
2869 } => refname.clone(),
2870 },
2871 crate::refs::store::RefStoreError::SymrefLoop
2872 | crate::refs::store::RefStoreError::Corrupt(_)
2873 | crate::refs::store::RefStoreError::GitDirPathIo { .. }
2874 | crate::refs::store::RefStoreError::InvalidReflog(_)
2875 | crate::refs::store::RefStoreError::Backend(_)
2876 | crate::refs::store::RefStoreError::Hook(_) => return None,
2877 })
2878}
2879
2880fn apply_ref_store_error_to_push_decision(d: &mut PushDecision, err: &Error, _atomic: bool) {
2881 d.result.status = push_status_for_ref_store_error(err);
2882 d.result.message = Some(push_message_for_ref_store_error(err));
2883 d.apply = false;
2884}
2885
2886fn apply_atomic_batch_ref_store_error(
2887 decisions: &mut [PushDecision],
2888 batch: &[crate::refs::store::RefUpdate],
2889 err: Error,
2890) {
2891 let fail_ref = ref_store_error_refname(&err);
2892 let message = push_message_for_ref_store_error(&err);
2893 let batch_names: HashSet<&str> = batch.iter().map(|u| u.name.as_str()).collect();
2894 let in_batch = decisions
2895 .iter_mut()
2896 .filter(|d| d.apply && batch_names.contains(d.result.remote_ref.as_str()));
2897 if let Some(fail_name) = fail_ref {
2898 for d in in_batch {
2899 if d.result.remote_ref == fail_name {
2900 d.result.status = push_status_for_ref_store_error(&err);
2901 d.result.message = Some(message.clone());
2902 } else {
2903 d.result.status = PushRefStatus::AtomicPushFailed;
2904 d.result.message = Some("atomic push failed".to_owned());
2905 }
2906 d.apply = false;
2907 }
2908 } else {
2909 let mut first = true;
2910 for d in in_batch {
2911 if first {
2912 d.result.status = push_status_for_ref_store_error(&err);
2913 d.result.message = Some(message.clone());
2914 first = false;
2915 } else {
2916 d.result.status = PushRefStatus::AtomicPushFailed;
2917 d.result.message = Some("atomic push failed".to_owned());
2918 }
2919 d.apply = false;
2920 }
2921 }
2922}
2923
2924enum PushAction {
2926 Update(ObjectId),
2928 Delete,
2930 None,
2932}
2933
2934struct PushDecision {
2936 result: PushRefResult,
2937 action: PushAction,
2938 apply: bool,
2940}
2941
2942impl PushDecision {
2943 fn ref_command(&self) -> Option<crate::serve::RefCommand> {
2946 let new = match self.action {
2947 PushAction::Update(src) => Some(src),
2948 PushAction::Delete => None,
2949 PushAction::None => return None,
2950 };
2951 Some(crate::serve::RefCommand {
2952 refname: self.result.remote_ref.clone(),
2953 old: self.result.old_oid,
2954 new,
2955 })
2956 }
2957
2958 fn refuse(&mut self, reason: &crate::serve::RefUpdateRefusal) {
2960 self.result.status = PushRefStatus::RemoteRejected;
2961 self.result.message = Some(reason.to_string());
2962 self.apply = false;
2963 self.action = PushAction::None;
2964 }
2965}
2966
2967fn decide_push(
2969 spec: &PushRefSpec,
2970 local_odb: &Odb,
2971 remote_git_dir: &Path,
2972 local_repo: Option<&crate::repo::Repository>,
2973) -> Result<PushDecision> {
2974 let remote_current = crate::refs::resolve_ref(remote_git_dir, &spec.dst).ok();
2975
2976 if !spec.delete {
2982 if let Some(src) = spec.src {
2983 if remote_current == Some(src) {
2984 return Ok(PushDecision {
2985 result: PushRefResult {
2986 local_ref: push_spec_local_ref(spec),
2987 remote_ref: spec.dst.clone(),
2988 old_oid: remote_current,
2989 new_oid: Some(src),
2990 forced: false,
2991 deletion: false,
2992 status: PushRefStatus::UpToDate,
2993 message: None,
2994 },
2995 action: PushAction::None,
2996 apply: false,
2997 });
2998 }
2999 }
3000 }
3001
3002 if spec.expect_absent && remote_current.is_some() {
3006 return Ok(PushDecision {
3007 result: PushRefResult {
3008 local_ref: push_spec_local_ref(spec),
3009 remote_ref: spec.dst.clone(),
3010 old_oid: remote_current,
3011 new_oid: spec.src,
3012 forced: false,
3013 deletion: spec.delete,
3014 status: PushRefStatus::RejectStale,
3015 message: Some("stale info".to_owned()),
3016 },
3017 action: PushAction::None,
3018 apply: false,
3019 });
3020 }
3021
3022 if let Some(expected) = spec.expected_old {
3026 if remote_current != Some(expected) {
3027 return Ok(PushDecision {
3028 result: PushRefResult {
3029 local_ref: None,
3030 remote_ref: spec.dst.clone(),
3031 old_oid: remote_current,
3032 new_oid: spec.src,
3033 forced: false,
3034 deletion: spec.delete,
3035 status: PushRefStatus::RejectStale,
3036 message: Some("stale info".to_owned()),
3037 },
3038 action: PushAction::None,
3039 apply: false,
3040 });
3041 }
3042 }
3043
3044 if spec.delete {
3045 let (status, action, apply, message) = match remote_current {
3046 Some(_) => (PushRefStatus::Ok, PushAction::Delete, true, None),
3047 None => (
3048 PushRefStatus::RejectRemoteRefMissing,
3049 PushAction::None,
3050 false,
3051 Some("remote ref does not exist".to_owned()),
3052 ),
3053 };
3054 return Ok(PushDecision {
3055 result: PushRefResult {
3056 local_ref: push_spec_local_ref(spec),
3057 remote_ref: spec.dst.clone(),
3058 old_oid: remote_current,
3059 new_oid: None,
3060 forced: false,
3061 deletion: true,
3062 status,
3063 message,
3064 },
3065 action,
3066 apply,
3067 });
3068 }
3069
3070 let Some(src) = spec.src else {
3072 return Err(Error::Message(format!(
3073 "push to '{}' has no source object and is not a deletion",
3074 spec.dst
3075 )));
3076 };
3077 if !local_odb.contains(&src)? {
3078 return Err(Error::Message(format!(
3079 "source object {src} for '{}' is missing from the local object store",
3080 spec.dst
3081 )));
3082 }
3083
3084 if remote_current == Some(src) {
3086 return Ok(PushDecision {
3087 result: PushRefResult {
3088 local_ref: push_spec_local_ref(spec),
3089 remote_ref: spec.dst.clone(),
3090 old_oid: remote_current,
3091 new_oid: Some(src),
3092 forced: false,
3093 deletion: false,
3094 status: PushRefStatus::UpToDate,
3095 message: None,
3096 },
3097 action: PushAction::None,
3098 apply: false,
3099 });
3100 }
3101
3102 let Some(old) = remote_current else {
3104 return Ok(PushDecision {
3105 result: PushRefResult {
3106 local_ref: push_spec_local_ref(spec),
3107 remote_ref: spec.dst.clone(),
3108 old_oid: None,
3109 new_oid: Some(src),
3110 forced: false,
3111 deletion: false,
3112 status: PushRefStatus::Ok,
3113 message: None,
3114 },
3115 action: PushAction::Update(src),
3116 apply: true,
3117 });
3118 };
3119
3120 let is_ff = local_repo
3124 .map(|r| crate::merge_base::is_ancestor(r, old, src).unwrap_or(false))
3125 .unwrap_or(false);
3126
3127 if is_ff {
3128 Ok(PushDecision {
3129 result: PushRefResult {
3130 local_ref: push_spec_local_ref(spec),
3131 remote_ref: spec.dst.clone(),
3132 old_oid: Some(old),
3133 new_oid: Some(src),
3134 forced: false,
3135 deletion: false,
3136 status: PushRefStatus::Ok,
3137 message: None,
3138 },
3139 action: PushAction::Update(src),
3140 apply: true,
3141 })
3142 } else if spec.force {
3143 Ok(PushDecision {
3144 result: PushRefResult {
3145 local_ref: push_spec_local_ref(spec),
3146 remote_ref: spec.dst.clone(),
3147 old_oid: Some(old),
3148 new_oid: Some(src),
3149 forced: true,
3150 deletion: false,
3151 status: PushRefStatus::Ok,
3152 message: None,
3153 },
3154 action: PushAction::Update(src),
3155 apply: true,
3156 })
3157 } else {
3158 Ok(PushDecision {
3159 result: PushRefResult {
3160 local_ref: push_spec_local_ref(spec),
3161 remote_ref: spec.dst.clone(),
3162 old_oid: Some(old),
3163 new_oid: Some(src),
3164 forced: false,
3165 deletion: false,
3166 status: PushRefStatus::RejectNonFastForward,
3167 message: Some("non-fast-forward".to_owned()),
3168 },
3169 action: PushAction::None,
3170 apply: false,
3171 })
3172 }
3173}
3174
3175pub(crate) struct MatchedRef {
3177 pub(crate) remote_ref: String,
3178 pub(crate) local_ref: Option<String>,
3180 pub(crate) oid: ObjectId,
3181 pub(crate) force: bool,
3182 pub(crate) is_tag: bool,
3183 pub(crate) auto_followed: bool,
3187 pub(crate) dest_is_tag: bool,
3189 pub(crate) advertised_peel: Option<ObjectId>,
3191 pub(crate) fetch_head: crate::fetch_request::FetchHeadPolicy,
3193}
3194
3195fn objects_dir(git_dir: &Path) -> PathBuf {
3200 crate::repo::common_git_dir_for_config(git_dir).join("objects")
3201}
3202
3203pub(crate) fn open_odb(git_dir: &Path) -> Odb {
3204 Odb::new(&objects_dir(git_dir)).with_config_git_dir(git_dir.to_path_buf())
3205}
3206
3207pub(crate) fn remote_hash_algo_from_advertisement(
3209 capability_blocks: &[&[String]],
3210 remote_refs: &[(String, ObjectId)],
3211) -> HashAlgo {
3212 for caps in capability_blocks {
3213 for c in *caps {
3214 if let Some(name) = c.strip_prefix("object-format=") {
3215 if let Some(algo) = HashAlgo::from_name(name) {
3216 return algo;
3217 }
3218 }
3219 }
3220 }
3221 remote_refs
3222 .iter()
3223 .find_map(|(_, oid)| HashAlgo::from_len(oid.as_bytes().len()))
3224 .unwrap_or(HashAlgo::Sha1)
3225}
3226
3227pub(crate) fn sync_empty_destination_object_format(
3231 local_git_dir: &Path,
3232 remote_algo: HashAlgo,
3233 opts: &FetchOptions,
3234) -> Result<bool> {
3235 if opts.dry_run || !opts.initial_remote_fetch {
3236 return Ok(false);
3237 }
3238 let local_odb = open_odb(local_git_dir);
3239 if local_odb.hash_algo() == remote_algo {
3240 return Ok(false);
3241 }
3242 if local_odb.approximate_object_count()? > 0 {
3243 return Err(Error::Message(format!(
3244 "unsupported object format: destination uses {} but remote uses {}; re-clone into an empty directory with a matching format",
3245 local_odb.hash_algo().name(),
3246 remote_algo.name()
3247 )));
3248 }
3249 write_empty_destination_object_format(local_git_dir, remote_algo)?;
3250 Ok(true)
3251}
3252
3253pub(crate) fn reopen_after_format_change(
3256 local: &crate::repo::Repository,
3257) -> Result<crate::repo::Repository> {
3258 crate::repo::Repository::open(
3259 local.git_dir(),
3260 &RepositoryOptions::with_environment(local.environment().clone()),
3261 )
3262}
3263
3264fn write_empty_destination_object_format(git_dir: &Path, algo: HashAlgo) -> Result<()> {
3265 use crate::config::{ConfigFile, ConfigScope};
3266 let config_path = crate::repo::local_config_write_path(git_dir);
3267 let content = fs::read_to_string(&config_path).unwrap_or_default();
3268 let mut file = ConfigFile::parse(&config_path, &content, ConfigScope::Local)?;
3269 if algo == HashAlgo::Sha1 {
3270 file.unset("extensions.objectformat")?;
3271 file.set("core.repositoryformatversion", "0")?;
3272 } else {
3273 file.set("core.repositoryformatversion", "1")?;
3274 file.set("extensions.objectformat", algo.name())?;
3275 }
3276 file.write()?;
3277 Ok(())
3278}
3279
3280fn config_for_git_dir(git_dir: &Path) -> Option<crate::config::ConfigSet> {
3281 crate::config::ConfigSet::load(
3282 &crate::environment::Environment::empty(),
3283 Some(git_dir),
3284 true,
3285 )
3286 .ok()
3287}
3288
3289pub(crate) fn finish_anonymous_url_fetch(
3291 git_dir: &Path,
3292 opts: &FetchOptions,
3293 explicit_refspec_fetch: bool,
3294 default_branch: Option<&str>,
3295 remote_refs: &[(String, ObjectId)],
3296 matched: &[MatchedRef],
3297) -> Result<()> {
3298 if opts.dry_run {
3299 return Ok(());
3300 }
3301 let write_fetch_head = opts.remote_name.is_none() || explicit_refspec_fetch;
3302 if !write_fetch_head {
3303 return Ok(());
3304 }
3305 let source = opts.fetch_source.clone().or_else(|| {
3306 let name = opts.remote_name.as_deref()?;
3307 let config = config_for_git_dir(git_dir)?;
3308 config
3309 .get(&format!("remote.{name}.url"))
3310 .filter(|url| !url.trim().is_empty())
3311 });
3312 let Some(source) = source.as_deref() else {
3313 return Ok(());
3314 };
3315 let lines = fetch_head_lines_for_ad_hoc_fetch(
3316 matched,
3317 remote_refs,
3318 &opts.refspecs,
3319 default_branch,
3320 source,
3321 );
3322 if lines.is_empty() {
3323 return Ok(());
3324 }
3325 crate::fetch_head::write_fetch_head(git_dir, &lines)
3326}
3327
3328fn fetch_head_lines_for_ad_hoc_fetch(
3329 matched: &[MatchedRef],
3330 remote_refs: &[(String, ObjectId)],
3331 refspecs: &[String],
3332 default_branch: Option<&str>,
3333 source: &str,
3334) -> Vec<crate::fetch_head::FetchHeadLine> {
3335 use crate::fetch_head::FetchHeadLine;
3336 use crate::remote::Remote;
3337
3338 let anonymous_only = refspecs.len() == 1
3339 && refspecs[0].trim_start_matches('+') == Remote::ANONYMOUS_FETCH_REFSPEC;
3340
3341 if anonymous_only {
3342 if let Some(branch) = default_branch {
3343 let head_ref = format!("refs/heads/{branch}");
3344 if let Some(m) = matched
3345 .iter()
3346 .find(|m| m.remote_ref == head_ref && !m.is_tag)
3347 {
3348 return vec![FetchHeadLine {
3349 oid: m.oid,
3350 for_merge: true,
3351 description: source.to_owned(),
3352 }];
3353 }
3354 }
3355 if let Some((_, oid)) = remote_refs.iter().find(|(name, _)| name == "HEAD") {
3356 return vec![FetchHeadLine {
3357 oid: *oid,
3358 for_merge: true,
3359 description: source.to_owned(),
3360 }];
3361 }
3362 return Vec::new();
3363 }
3364
3365 matched
3366 .iter()
3367 .filter(|m| m.fetch_head == crate::fetch_request::FetchHeadPolicy::ForMerge)
3368 .map(|m| FetchHeadLine {
3369 oid: m.oid,
3370 for_merge: true,
3371 description: crate::fetch_request::fetch_head_description(
3372 &m.remote_ref,
3373 m.is_tag,
3374 source,
3375 ),
3376 })
3377 .collect()
3378}
3379
3380#[must_use]
3382pub(crate) fn remote_head_oid_from_advertised(
3383 advertised: &[(String, ObjectId)],
3384) -> Option<ObjectId> {
3385 advertised
3386 .iter()
3387 .find(|(name, _)| name == "HEAD")
3388 .map(|(_, oid)| *oid)
3389}
3390
3391pub(crate) fn match_positive(refname: &str, positive: &[RefspecItem]) -> Option<Option<String>> {
3396 for item in positive {
3397 let Some(src) = item.src.as_deref() else {
3398 continue;
3399 };
3400 if let Some(dst) = apply_refspec(src, item.dst.as_deref(), refname) {
3401 if dst.is_empty() {
3403 return Some(None);
3404 }
3405 return Some(Some(dst));
3406 }
3407 }
3408 None
3409}
3410
3411pub(crate) fn refspecs_force(refname: &str, positive: &[RefspecItem]) -> bool {
3413 positive.iter().any(|item| {
3414 item.force
3415 && item
3416 .src
3417 .as_deref()
3418 .is_some_and(|src| apply_refspec(src, item.dst.as_deref(), refname).is_some())
3419 })
3420}
3421
3422pub(crate) fn ref_excluded(refname: &str, negatives: &[RefspecItem]) -> bool {
3424 negatives.iter().any(|item| {
3425 item.src
3426 .as_deref()
3427 .is_some_and(|src| glob_matches(src, refname))
3428 })
3429}
3430
3431pub(crate) fn apply_refspec(src: &str, dst: Option<&str>, refname: &str) -> Option<String> {
3437 match src.find('*') {
3438 Some(star) => {
3439 let prefix = &src[..star];
3440 let suffix = &src[star + 1..];
3441 if !refname.starts_with(prefix)
3442 || !refname.ends_with(suffix)
3443 || refname.len() < prefix.len() + suffix.len()
3444 {
3445 return None;
3446 }
3447 let middle = &refname[prefix.len()..refname.len() - suffix.len()];
3448 match dst {
3449 None => Some(refname.to_owned()),
3450 Some("") => Some(String::new()),
3451 Some(d) => Some(d.replacen('*', middle, 1)),
3452 }
3453 }
3454 None => {
3455 if src != refname {
3456 return None;
3457 }
3458 match dst {
3459 None => Some(refname.to_owned()),
3460 Some("") => Some(String::new()),
3461 Some(d) => Some(d.to_owned()),
3462 }
3463 }
3464 }
3465}
3466
3467fn glob_matches(pattern: &str, refname: &str) -> bool {
3469 match pattern.find('*') {
3470 Some(star) => {
3471 let prefix = &pattern[..star];
3472 let suffix = &pattern[star + 1..];
3473 refname.starts_with(prefix)
3474 && refname.ends_with(suffix)
3475 && refname.len() >= prefix.len() + suffix.len()
3476 }
3477 None => pattern == refname,
3478 }
3479}
3480
3481#[allow(clippy::too_many_arguments)]
3487pub(crate) fn apply_tag_mode(
3488 mode: TagMode,
3489 remote_refs: &[(String, ObjectId)],
3490 remote_odb: &Odb,
3491 shallow_boundaries: &HashSet<ObjectId>,
3492 negatives: &[RefspecItem],
3493 already_local: &HashSet<String>,
3494 force_all_tags: bool,
3495 matched: &mut Vec<MatchedRef>,
3496 matched_oids: &mut HashSet<ObjectId>,
3497 seen_remote_ref: &mut HashSet<String>,
3498) -> Result<HashSet<ObjectId>> {
3499 let mut following_only = HashSet::new();
3500 if mode == TagMode::None {
3501 return Ok(following_only);
3502 }
3503
3504 let candidates: Vec<(&String, ObjectId, Option<ObjectId>)> = remote_refs
3507 .iter()
3508 .filter(|(name, _)| {
3509 name.starts_with("refs/tags/")
3510 && !seen_remote_ref.contains(name)
3511 && !ref_excluded(name, negatives)
3512 && !(mode == TagMode::Following && already_local.contains(name))
3513 })
3514 .map(|(name, oid)| {
3515 let peeled = match mode {
3516 TagMode::Following => remote_odb.peel_tags(*oid).ok().map(|(p, _)| p),
3517 _ => None,
3518 };
3519 (name, *oid, peeled)
3520 })
3521 .collect();
3522
3523 let following_closure: HashSet<ObjectId> = if mode == TagMode::Following {
3528 let roots: Vec<ObjectId> = matched
3529 .iter()
3530 .filter(|m| !m.is_tag)
3531 .map(|m| m.oid)
3532 .collect();
3533 let targets: HashSet<ObjectId> = candidates
3534 .iter()
3535 .map(|(_, oid, peeled)| peeled.unwrap_or(*oid))
3536 .collect();
3537 crate::fetch::reachable_commits(remote_odb, &roots, &targets, shallow_boundaries)?
3538 } else {
3539 HashSet::new()
3540 };
3541
3542 for (name, oid, peeled) in candidates {
3543 let keep = match mode {
3544 TagMode::All => true,
3545 TagMode::Following => {
3547 following_closure.contains(&oid)
3548 || peeled.is_some_and(|p| following_closure.contains(&p))
3549 }
3550 TagMode::None => false,
3551 };
3552 if keep && seen_remote_ref.insert(name.clone()) {
3553 if matched_oids.insert(oid) && mode == TagMode::Following {
3554 following_only.insert(oid);
3555 }
3556 matched.push(MatchedRef {
3557 remote_ref: name.clone(),
3558 local_ref: Some(name.clone()),
3559 oid,
3560 force: force_all_tags && mode == TagMode::All,
3561 is_tag: true,
3562 auto_followed: mode == TagMode::Following,
3563 dest_is_tag: true,
3564 advertised_peel: peeled,
3565 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
3566 });
3567 }
3568 }
3569 Ok(following_only)
3570}
3571
3572fn shallow_want_commit_tip(remote_odb: &Odb, matched: &MatchedRef) -> Result<ObjectId> {
3574 if !matched.is_tag {
3575 return Ok(matched.oid);
3576 }
3577 if let Some(peel) = matched.advertised_peel.filter(|p| !p.is_zero()) {
3578 return Ok(peel);
3579 }
3580 if remote_odb.contains(&matched.oid)? {
3581 let (peeled, _) = remote_odb.peel_tags(matched.oid)?;
3582 return Ok(peeled);
3583 }
3584 Ok(matched.oid)
3585}
3586
3587fn ref_target_exists(local_odb: &Odb, remote_odb: &Odb, tag_oid: ObjectId) -> Result<bool> {
3590 let peel_odb = if local_odb.contains(&tag_oid)? {
3591 local_odb
3592 } else {
3593 remote_odb
3594 };
3595 match peel_odb.peel_tags(tag_oid) {
3596 Ok((peeled, _)) => local_odb.contains(&peeled),
3597 Err(_) => Ok(false),
3598 }
3599}
3600
3601pub(crate) fn classify_update(
3607 old: Option<&ObjectId>,
3608 new: &ObjectId,
3609 force: bool,
3610 is_tag: bool,
3611 repo: Option<&crate::repo::Repository>,
3612 classify_odb: Option<&crate::odb::Odb>,
3613) -> UpdateMode {
3614 let Some(old) = old else {
3615 return UpdateMode::New;
3616 };
3617 if old == new {
3618 return UpdateMode::UpToDate;
3619 }
3620 let ff = if let Some(r) = repo {
3622 crate::merge_base::is_ancestor(r, *old, *new).unwrap_or(false)
3623 } else if let Some(odb) = classify_odb {
3624 crate::merge_base::is_ancestor_odb_walk(odb, *old, *new).unwrap_or(false)
3625 } else {
3626 false
3627 };
3628 if ff && !is_tag {
3629 return UpdateMode::FastForward;
3630 }
3631 if force {
3632 return UpdateMode::Forced;
3633 }
3634 if is_tag {
3635 return UpdateMode::TagUpdateRejected;
3636 }
3637 UpdateMode::NonFastForwardRejected
3638}
3639
3640fn is_default_head_under_tracking_prefix(refname: &str, tracking_prefix: &str) -> bool {
3644 refname == format!("{tracking_prefix}HEAD")
3645}
3646
3647pub fn tracking_refs_gone_on_remote(
3656 local_git_dir: &Path,
3657 positive: &[RefspecItem],
3658 remote_refs: &[(String, ObjectId)],
3659) -> Result<Vec<(String, ObjectId)>> {
3660 tracking_refs_gone_with(
3661 positive,
3662 remote_refs,
3663 |prefix| crate::refs::list_refs(local_git_dir, prefix),
3664 |name| crate::refs::resolve_ref(local_git_dir, name),
3665 )
3666}
3667
3668pub(crate) fn tracking_refs_gone_in(
3675 local: &crate::repo::Repository,
3676 positive: &[RefspecItem],
3677 remote_refs: &[(String, ObjectId)],
3678) -> Result<Vec<(String, ObjectId)>> {
3679 tracking_refs_gone_with(
3680 positive,
3681 remote_refs,
3682 |prefix| crate::refs::list_refs_in(local, prefix),
3683 |name| crate::refs::resolve_ref_in(local, name),
3684 )
3685}
3686
3687fn tracking_refs_gone_with(
3688 positive: &[RefspecItem],
3689 remote_refs: &[(String, ObjectId)],
3690 list: impl Fn(&str) -> Result<Vec<(String, ObjectId)>>,
3691 resolve: impl Fn(&str) -> Result<ObjectId>,
3692) -> Result<Vec<(String, ObjectId)>> {
3693 let mut live: HashSet<String> = HashSet::new();
3694 for (name, _) in remote_refs {
3695 if let Some(Some(dst)) = match_positive(name, positive) {
3696 live.insert(dst);
3697 }
3698 }
3699
3700 let mut pruned: HashMap<String, ObjectId> = HashMap::new();
3701 for item in positive {
3702 let Some(dst) = item.dst.as_deref() else {
3703 continue;
3704 };
3705 if let Some(star) = dst.find('*') {
3706 let prefix = &dst[..star];
3707 for (name, oid) in list(prefix)? {
3708 if !name.starts_with(prefix) {
3709 continue;
3710 }
3711 if is_default_head_under_tracking_prefix(&name, prefix) {
3712 continue;
3713 }
3714 if !live.contains(&name) {
3715 pruned.entry(name).or_insert(oid);
3716 }
3717 }
3718 } else if !live.contains(dst) {
3719 let exact_prefix = dst.strip_suffix("HEAD").filter(|p| p.ends_with('/'));
3720 if exact_prefix.is_some_and(|p| is_default_head_under_tracking_prefix(dst, p)) {
3721 continue;
3722 }
3723 if let Ok(oid) = resolve(dst) {
3724 pruned.entry(dst.to_owned()).or_insert(oid);
3725 }
3726 }
3727 }
3728 Ok(pruned.into_iter().collect())
3729}
3730
3731pub(crate) struct FetchRefRejectHint<'a> {
3733 pub prune: bool,
3735 pub positive: &'a [RefspecItem],
3737 pub remote_refs: &'a [(String, ObjectId)],
3739 pub remote_name: Option<&'a str>,
3741}
3742
3743fn fetch_ref_store_error_rejectable(err: &Error) -> bool {
3744 matches!(
3745 err,
3746 Error::RefStore(crate::refs::store::RefStoreError::NameUnavailable { .. })
3747 | Error::RefStore(crate::refs::store::RefStoreError::LockHeld { .. })
3748 )
3749}
3750
3751fn blocking_ref_from_unavailable(reason: &crate::refs::RefnameUnavailable) -> Option<&str> {
3752 match reason {
3753 crate::refs::RefnameUnavailable::AncestorExists { blocking, .. }
3754 | crate::refs::RefnameUnavailable::DescendantExists { blocking, .. } => Some(blocking),
3755 crate::refs::RefnameUnavailable::SameBatch { .. } => None,
3756 }
3757}
3758
3759fn append_fetch_prune_hint(
3760 note: &mut String,
3761 stale_tracking: &HashSet<String>,
3762 blocking: &str,
3763 remote_name: Option<&str>,
3764) {
3765 if stale_tracking.contains(blocking) {
3766 let remote = remote_name.unwrap_or("origin");
3767 note.push_str(&format!(
3768 " — stale remote-tracking ref; try `grit fetch --prune` or `grit remote prune {remote}`"
3769 ));
3770 }
3771}
3772
3773fn mark_fetch_ref_update_rejected(
3774 updates: &mut [RefUpdate],
3775 local_ref: &str,
3776 err: &Error,
3777 stale_tracking: &HashSet<String>,
3778 hint: &FetchRefRejectHint<'_>,
3779) {
3780 let Some(update) = updates
3781 .iter_mut()
3782 .find(|u| u.local_ref.as_deref() == Some(local_ref))
3783 else {
3784 return;
3785 };
3786 update.mode = UpdateMode::RefLockRejected;
3787 let mut note = match err {
3788 Error::RefStore(crate::refs::store::RefStoreError::NameUnavailable { reason }) => {
3789 let mut text = reason.lock_message_suffix();
3790 if let Some(blocking) = blocking_ref_from_unavailable(reason) {
3791 append_fetch_prune_hint(&mut text, stale_tracking, blocking, hint.remote_name);
3792 }
3793 text
3794 }
3795 Error::RefStore(crate::refs::store::RefStoreError::LockHeld { name }) => {
3796 format!("ref '{name}' is locked")
3797 }
3798 _ => err.to_string(),
3799 };
3800 if note.is_empty() {
3801 note = "unable to update local ref".to_owned();
3802 }
3803 update.note = Some(note);
3804}
3805
3806pub(crate) fn apply_fetch_ref_updates(
3827 local: &crate::repo::Repository,
3828 writes: Vec<(String, crate::refs::store::RawRef)>,
3829 updates: &mut [RefUpdate],
3830 hint: FetchRefRejectHint<'_>,
3831 initial: bool,
3832 fetch_reflog: Option<&FetchApplyReflog<'_>>,
3833) -> Result<()> {
3834 use crate::refs::store::{RawRef, ReflogUpdate};
3835 use std::collections::HashMap;
3836
3837 let store = local.refs();
3838 let config = local.config()?;
3839 let null_oid = store.reference_transaction_null_oid();
3840 let mode_by_local: HashMap<String, (UpdateMode, Option<ObjectId>, Option<ObjectId>)> = updates
3841 .iter()
3842 .filter_map(|u| {
3843 let local_ref = u.local_ref.as_ref()?;
3844 Some((local_ref.clone(), (u.mode, u.old_oid, u.new_oid)))
3845 })
3846 .collect();
3847
3848 let store_update = |(name, value): &(String, RawRef)| -> Result<crate::refs::store::RefUpdate> {
3849 let reflog = if initial {
3850 None
3851 } else if let Some(ctx) = fetch_reflog {
3852 let Some((mode, old_oid, new_oid)) = mode_by_local.get(name).copied() else {
3853 return Ok(crate::refs::store::RefUpdate {
3854 name: name.clone(),
3855 new_value: Some(value.clone()),
3856 expected: crate::refs::store::Expected::Any,
3857 reflog: None,
3858 flags: Default::default(),
3859 });
3860 };
3861 let Some(reason) = mode.fetch_reflog_reason() else {
3862 return Ok(crate::refs::store::RefUpdate {
3863 name: name.clone(),
3864 new_value: Some(value.clone()),
3865 expected: crate::refs::store::Expected::Any,
3866 reflog: None,
3867 flags: Default::default(),
3868 });
3869 };
3870 if !crate::refs::reflog_wanted(store, &config, name)? {
3871 return Ok(crate::refs::store::RefUpdate {
3872 name: name.clone(),
3873 new_value: Some(value.clone()),
3874 expected: crate::refs::store::Expected::Any,
3875 reflog: None,
3876 flags: Default::default(),
3877 });
3878 }
3879 let old = old_oid.unwrap_or(null_oid);
3880 let new = match value {
3881 RawRef::Direct(oid) => *oid,
3882 RawRef::Symbolic(_) => new_oid.unwrap_or(null_oid),
3883 };
3884 let message = format!("{}: {reason}", ctx.action);
3885 Some(ReflogUpdate {
3886 identity: ctx.identity.to_owned(),
3887 message,
3888 time: time::OffsetDateTime::UNIX_EPOCH,
3889 old_oid: Some(old),
3890 new_oid: Some(new),
3891 })
3892 } else {
3893 None
3894 };
3895 Ok(crate::refs::store::RefUpdate {
3896 name: name.clone(),
3897 new_value: Some(value.clone()),
3898 expected: crate::refs::store::Expected::Any,
3899 reflog,
3900 flags: Default::default(),
3901 })
3902 };
3903
3904 let mut pending: Vec<&(String, RawRef)> = writes.iter().collect();
3905 if initial {
3906 let (direct, rest): (Vec<_>, Vec<_>) = pending
3907 .iter()
3908 .copied()
3909 .partition(|(_, value)| matches!(value, RawRef::Direct(_)));
3910 let direct = direct
3911 .into_iter()
3912 .map(&store_update)
3913 .collect::<Result<Vec<_>>>()?;
3914 if crate::refs::commit_initial_ref_updates_on_store(store, direct).is_ok() {
3915 pending = rest;
3916 }
3917 }
3918 let batch = pending
3919 .iter()
3920 .copied()
3921 .map(&store_update)
3922 .collect::<Result<Vec<_>>>()?;
3923 if pending.is_empty() || crate::refs::commit_owned_ref_updates_on_store(store, batch).is_ok() {
3924 return Ok(());
3925 }
3926
3927 let stale_tracking: HashSet<String> = if hint.prune {
3928 HashSet::new()
3929 } else {
3930 tracking_refs_gone_in(local, hint.positive, hint.remote_refs)?
3931 .into_iter()
3932 .map(|(name, _)| name)
3933 .collect()
3934 };
3935
3936 for write in pending {
3937 match crate::refs::commit_ref_update_on_store(store, store_update(write)?) {
3938 Ok(()) => {}
3939 Err(err) if fetch_ref_store_error_rejectable(&err) => {
3940 mark_fetch_ref_update_rejected(updates, &write.0, &err, &stale_tracking, &hint);
3941 }
3942 Err(err) => return Err(err),
3943 }
3944 }
3945 Ok(())
3946}
3947
3948pub(crate) fn prune_tracking_refs(
3961 local: &crate::repo::Repository,
3962 positive: &[RefspecItem],
3963 remote_refs: &[(String, ObjectId)],
3964 dry_run: bool,
3965 updates: &mut Vec<RefUpdate>,
3966) -> Result<()> {
3967 let pruned: Vec<(String, ObjectId)> = tracking_refs_gone_in(local, positive, remote_refs)?;
3968
3969 let prune_delete_flags = crate::refs::store::RefUpdateFlags {
3970 no_deref: true,
3971 ..Default::default()
3972 };
3973 let mut batch: Vec<crate::refs::store::RefUpdate> = Vec::new();
3974 let mut pruned_names: Vec<String> = Vec::new();
3975 for (name, oid) in pruned.into_iter() {
3976 pruned_names.push(name.clone());
3977 if !dry_run {
3978 batch.push(crate::refs::store::RefUpdate {
3979 name: name.clone(),
3980 new_value: None,
3981 expected: crate::refs::store::Expected::Any,
3982 reflog: None,
3983 flags: prune_delete_flags,
3984 });
3985 }
3986 updates.push(RefUpdate {
3987 remote_ref: String::new(),
3988 local_ref: Some(name),
3989 old_oid: Some(oid),
3990 new_oid: None,
3991 mode: UpdateMode::DeletedMissing,
3992 note: Some("pruned (gone on remote)".to_owned()),
3993 });
3994 }
3995 let store = local.refs();
3996 crate::refs::commit_owned_ref_updates_on_store(store, batch)?;
3997 if !dry_run {
3998 for name in pruned_names {
3999 store.delete_reflog(&name)?;
4000 }
4001 }
4002 Ok(())
4003}
4004
4005#[cfg(test)]
4006mod tests {
4007 use super::*;
4008 use crate::progress::ProgressPhase;
4009 use crate::ref_storage::RefStorageFormat;
4010 use crate::repo::init_repository;
4011 use std::process::Command;
4012
4013 #[derive(Debug, Default)]
4014 struct PhaseRecorder {
4015 events: Vec<&'static str>,
4016 started: Vec<(ProgressPhase, Option<u64>)>,
4017 finished: Vec<ProgressPhase>,
4018 counts: Vec<u64>,
4019 enumerating_counts: Vec<u64>,
4020 copying_counts: Vec<u64>,
4021 active: Option<ProgressPhase>,
4022 current: u64,
4023 received: Vec<ReceivedObjects>,
4024 want_received: bool,
4025 }
4026
4027 impl ProgressSink for PhaseRecorder {
4028 fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
4029 self.events.push("start");
4030 self.started.push((phase, total));
4031 self.active = Some(phase);
4032 self.current = 0;
4033 }
4034 fn inc(&mut self, units: u64) {
4035 self.events.push("inc");
4036 self.current += units;
4037 self.record_count();
4038 }
4039 fn set(&mut self, current: u64) {
4040 self.events.push("set");
4041 self.current = current;
4042 self.record_count();
4043 }
4044 fn finish(&mut self) {
4045 self.events.push("finish");
4046 if let Some(phase) = self.active.take() {
4047 self.finished.push(phase);
4048 }
4049 }
4050 }
4051
4052 impl PhaseRecorder {
4053 fn record_count(&mut self) {
4054 self.counts.push(self.current);
4055 match self.active {
4056 Some(ProgressPhase::EnumeratingObjects) => {
4057 self.enumerating_counts.push(self.current);
4058 }
4059 Some(ProgressPhase::CopyingObjects) => {
4060 self.copying_counts.push(self.current);
4061 }
4062 _ => {}
4063 }
4064 }
4065 }
4066
4067 impl Progress for PhaseRecorder {
4068 fn wants_received_objects(&self) -> bool {
4069 self.want_received
4070 }
4071
4072 fn received_objects(&mut self, counts: ReceivedObjects) {
4073 assert_eq!(
4074 self.active,
4075 Some(ProgressPhase::CopyingObjects),
4076 "received_objects must run only during CopyingObjects (active={active:?}, counts={counts:?})",
4077 active = self.active,
4078 );
4079 assert!(
4080 counts.received <= counts.total,
4081 "received must not exceed total: {counts:?}"
4082 );
4083 self.received.push(counts);
4084 }
4085 }
4086
4087 fn git_ok(dir: &Path, args: &[&str]) -> bool {
4088 Command::new("git") .env("GIT_AUTHOR_NAME", "Test")
4090 .env("GIT_AUTHOR_EMAIL", "test@example.com")
4091 .env("GIT_COMMITTER_NAME", "Test")
4092 .env("GIT_COMMITTER_EMAIL", "test@example.com")
4093 .current_dir(dir)
4094 .args(args)
4095 .env("GIT_CONFIG_GLOBAL", "/dev/null")
4096 .env("GIT_CONFIG_SYSTEM", "/dev/null")
4097 .status()
4098 .map(|s| s.success())
4099 .unwrap_or(false)
4100 }
4101
4102 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
4104 pub(crate) enum PushHookPoint {
4105 BeforeReceivePolicy,
4108 BeforeRefCommit,
4110 }
4111
4112 type PushHook = (PushHookPoint, Box<dyn FnOnce()>);
4113
4114 thread_local! {
4116 static PUSH_HOOK: std::cell::RefCell<Option<PushHook>> =
4117 const { std::cell::RefCell::new(None) };
4118 }
4119
4120 pub(super) fn run_push_hook(point: PushHookPoint) {
4122 let hook = PUSH_HOOK.with(|slot| {
4123 let mut slot = slot.borrow_mut();
4124 match slot.as_ref() {
4125 Some((at, _)) if *at == point => slot.take().map(|(_, hook)| hook),
4126 _ => None,
4127 }
4128 });
4129 if let Some(hook) = hook {
4130 hook();
4131 }
4132 }
4133
4134 pub(crate) fn on_next_push(point: PushHookPoint, hook: impl FnOnce() + 'static) {
4136 PUSH_HOOK.with(|slot| *slot.borrow_mut() = Some((point, Box::new(hook))));
4137 }
4138
4139 fn git_stdout(dir: &Path, args: &[&str]) -> String {
4140 let out = Command::new("git") .env("GIT_AUTHOR_NAME", "Test")
4142 .env("GIT_AUTHOR_EMAIL", "test@example.com")
4143 .env("GIT_COMMITTER_NAME", "Test")
4144 .env("GIT_COMMITTER_EMAIL", "test@example.com")
4145 .env("GIT_CONFIG_GLOBAL", "/dev/null")
4146 .env("GIT_CONFIG_SYSTEM", "/dev/null")
4147 .current_dir(dir)
4148 .args(args)
4149 .output()
4150 .expect("run git");
4151 assert!(out.status.success(), "git {args:?} failed");
4152 String::from_utf8(out.stdout)
4153 .expect("utf8")
4154 .trim()
4155 .to_owned()
4156 }
4157
4158 fn racing_create_fixture(tmp: &Path) -> (PathBuf, PathBuf, ObjectId, ObjectId) {
4162 let remote = tmp.join("remote.git");
4163 let local = tmp.join("local");
4164 std::fs::create_dir_all(&remote).unwrap();
4165 std::fs::create_dir_all(&local).unwrap();
4166 assert!(git_ok(&remote, &["init", "-q", "--bare", "-b", "main"]));
4167 assert!(git_ok(&local, &["init", "-q", "-b", "main"]));
4168 assert!(git_ok(&local, &["commit", "--allow-empty", "-qm", "base"]));
4169 assert!(git_ok(
4170 &local,
4171 &["push", "-q", remote.to_str().unwrap(), "main"]
4172 ));
4173 assert!(git_ok(&local, &["checkout", "-q", "-b", "other"]));
4174 assert!(git_ok(&local, &["commit", "--allow-empty", "-qm", "other"]));
4175 assert!(git_ok(
4176 &local,
4177 &["push", "-q", remote.to_str().unwrap(), "other"]
4178 ));
4179 let other = ObjectId::from_hex(&git_stdout(&local, &["rev-parse", "HEAD"])).unwrap();
4180 assert!(git_ok(&local, &["checkout", "-q", "main"]));
4181 assert!(git_ok(&local, &["commit", "--allow-empty", "-qm", "mine"]));
4182 let tip = ObjectId::from_hex(&git_stdout(&local, &["rev-parse", "HEAD"])).unwrap();
4183 (remote, local.join(".git"), tip, other)
4184 }
4185
4186 #[cfg_attr(
4187 not(feature = "full-tests"),
4188 ignore = "starts system git or is heavy; run with --features full-tests"
4189 )]
4190 #[test]
4191 fn push_local_create_rejects_ref_created_after_decision() {
4192 let points = [
4193 PushHookPoint::BeforeReceivePolicy,
4194 PushHookPoint::BeforeRefCommit,
4195 ];
4196 let modes = [(false, false), (true, false), (false, true)];
4197 for (point, (force, atomic)) in points
4198 .into_iter()
4199 .flat_map(|p| modes.into_iter().map(move |m| (p, m)))
4200 {
4201 let tmp = tempfile::tempdir().unwrap();
4202 let (remote, local_git, tip, other) = racing_create_fixture(tmp.path());
4203
4204 let racer_remote = remote.clone();
4205 on_next_push(point, move || {
4206 assert!(git_ok(
4207 &racer_remote,
4208 &["update-ref", "refs/heads/shared", &other.to_hex(), ""]
4209 ));
4210 });
4211 let outcome = push_local(
4212 &local_git,
4213 &remote,
4214 &[PushRefSpec {
4215 src: Some(tip),
4216 dst: "refs/heads/shared".to_owned(),
4217 force,
4218 ..Default::default()
4219 }],
4220 &PushOptions {
4221 atomic,
4222 ..Default::default()
4223 },
4224 &crate::progress::NeverCancel,
4225 )
4226 .expect("push call");
4227
4228 let result = &outcome.results[0];
4229 let case = format!("{point:?} force={force} atomic={atomic}");
4230 assert_eq!(result.status, PushRefStatus::RejectAlreadyExists, "{case}");
4231 assert!(result.status.is_error(), "{case}");
4232 assert_eq!(result.message.as_deref(), Some("already exists"), "{case}");
4233 assert_eq!(
4234 git_stdout(&remote, &["rev-parse", "refs/heads/shared"]),
4235 other.to_hex(),
4236 "{case}: the racing creator's value must survive"
4237 );
4238 assert!(!git_ok(
4239 &remote,
4240 &[
4241 "merge-base",
4242 "--is-ancestor",
4243 &tip.to_hex(),
4244 &other.to_hex()
4245 ]
4246 ));
4247 }
4248 }
4249
4250 #[cfg_attr(
4251 not(feature = "full-tests"),
4252 ignore = "starts system git or is heavy; run with --features full-tests"
4253 )]
4254 #[test]
4255 fn push_local_update_rejects_ref_moved_after_decision() {
4256 for point in [
4257 PushHookPoint::BeforeReceivePolicy,
4258 PushHookPoint::BeforeRefCommit,
4259 ] {
4260 let tmp = tempfile::tempdir().unwrap();
4261 let (remote, local_git, tip, other) = racing_create_fixture(tmp.path());
4262
4263 let racer_remote = remote.clone();
4264 on_next_push(point, move || {
4265 assert!(git_ok(
4266 &racer_remote,
4267 &["update-ref", "refs/heads/main", &other.to_hex()]
4268 ));
4269 });
4270 let outcome = push_local(
4271 &local_git,
4272 &remote,
4273 &[PushRefSpec {
4274 src: Some(tip),
4275 dst: "refs/heads/main".to_owned(),
4276 ..Default::default()
4277 }],
4278 &PushOptions::default(),
4279 &crate::progress::NeverCancel,
4280 )
4281 .expect("push call");
4282
4283 assert_eq!(
4284 outcome.results[0].status,
4285 PushRefStatus::RejectStale,
4286 "{point:?}"
4287 );
4288 assert_eq!(
4289 git_stdout(&remote, &["rev-parse", "refs/heads/main"]),
4290 other.to_hex(),
4291 "{point:?}"
4292 );
4293 }
4294 }
4295
4296 fn assert_monotonic(counts: &[u64]) {
4297 for pair in counts.windows(2) {
4298 assert!(pair[1] >= pair[0], "progress counts must be monotonic");
4299 }
4300 }
4301
4302 fn copying_phase_total(rec: &PhaseRecorder) -> Option<u64> {
4303 rec.started
4304 .iter()
4305 .find(|(phase, _)| *phase == ProgressPhase::CopyingObjects)
4306 .and_then(|(_, total)| *total)
4307 }
4308
4309 fn assert_copy_progress_within_total(rec: &PhaseRecorder, copied_objects: u64) {
4310 let Some(total) = copying_phase_total(rec) else {
4311 panic!("missing CopyingObjects start: {:?}", rec.started);
4312 };
4313 assert!(
4314 total >= copied_objects,
4315 "progress total {total} smaller than copied objects {copied_objects}"
4316 );
4317 for &count in &rec.copying_counts {
4318 assert!(
4319 count <= total,
4320 "copy progress {count} exceeds phase total {total}"
4321 );
4322 }
4323 for snapshot in &rec.received {
4324 assert!(
4325 snapshot.received <= snapshot.total,
4326 "received {} exceeds snapshot total {}",
4327 snapshot.received,
4328 snapshot.total
4329 );
4330 assert!(
4331 snapshot.total <= total,
4332 "snapshot total {} exceeds phase total {}",
4333 snapshot.total,
4334 total
4335 );
4336 assert!(
4337 snapshot.received <= total,
4338 "received {} exceeds phase total {}",
4339 snapshot.received,
4340 total
4341 );
4342 }
4343 assert_eq!(
4344 rec.copying_counts.last().copied().unwrap_or(0),
4345 copied_objects,
4346 "final copy count should match transferred objects"
4347 );
4348 }
4349
4350 #[cfg_attr(
4351 not(feature = "full-tests"),
4352 ignore = "starts system git or is heavy; run with --features full-tests"
4353 )]
4354 #[test]
4355 fn local_fetch_classifies_through_repository_ref_store() {
4356 use crate::refs::store::{MemoryRefStore, RawRef};
4357 use std::sync::Arc;
4358
4359 let tmp = tempfile::tempdir().unwrap();
4360 assert!(git_ok(tmp.path(), &["init", "-q", "-b", "main"]));
4361 assert!(git_ok(
4362 tmp.path(),
4363 &["commit", "--allow-empty", "-qm", "base"]
4364 ));
4365 let repo =
4366 crate::repo::Repository::open(&tmp.path().join(".git"), &RepositoryOptions::empty())
4367 .unwrap();
4368 let tip = repo.resolve_ref_oid("HEAD").unwrap();
4369 let store = Arc::new(MemoryRefStore::new());
4370 store
4371 .set_ref("refs/remotes/origin/main", RawRef::Direct(tip))
4372 .unwrap();
4373 let repo = repo.with_ref_store(store);
4374 let opts = {
4375 let mut opts = FetchOptions::new();
4376 opts.refspecs = vec!["+refs/heads/*:refs/remotes/origin/*".into()];
4377 opts.tags = TagMode::None;
4378 opts.dry_run = true;
4379 opts
4380 };
4381 let outcome = fetch_local_into(
4382 &repo,
4383 &tmp.path().join(".git"),
4384 &opts,
4385 &mut PhaseRecorder::default(),
4386 &crate::progress::NeverCancel,
4387 )
4388 .unwrap();
4389 assert_eq!(outcome.transfer.objects, 0);
4390 assert_eq!(outcome.updates.len(), 1);
4391 assert_eq!(outcome.updates[0].old_oid, Some(tip));
4392 assert_eq!(outcome.updates[0].mode, UpdateMode::UpToDate);
4393 }
4394
4395 fn write_linear_commits(objects: &Path, depth: usize) -> (Odb, ObjectId) {
4396 use crate::objects::{serialize_commit, CommitData};
4397 let odb = Odb::new(objects);
4398 let mut parent = None;
4399 for i in 0..depth {
4400 let blob = odb
4401 .write(ObjectKind::Blob, format!("payload {i}\n").as_bytes())
4402 .expect("blob");
4403 let mut tree = b"100644 f.txt\0".to_vec();
4404 tree.extend_from_slice(blob.as_bytes());
4405 let tree_oid = odb.write(ObjectKind::Tree, &tree).expect("tree");
4406 let commit = CommitData {
4407 tree: tree_oid,
4408 parents: parent.into_iter().collect(),
4409 author: "T <t@e.com> 0 +0000".into(),
4410 committer: "T <t@e.com> 0 +0000".into(),
4411 author_raw: Vec::new(),
4412 committer_raw: Vec::new(),
4413 encoding: None,
4414 message: format!("c{i}\n"),
4415 raw_message: None,
4416 extra_headers: Vec::new(),
4417 };
4418 parent = Some(
4419 odb.write(ObjectKind::Commit, &serialize_commit(&commit))
4420 .expect("commit"),
4421 );
4422 }
4423 (odb, parent.expect("tip"))
4424 }
4425
4426 #[test]
4427 fn fetch_local_hardlinked_pack_reports_scan_progress() {
4428 let remote = tempfile::tempdir().expect("remote");
4429 assert!(git_ok(
4430 remote.path(),
4431 &["init", "-q", "-b", "main", "--bare"]
4432 ));
4433 let src = tempfile::tempdir().expect("src");
4434 assert!(git_ok(src.path(), &["init", "-q", "-b", "main"]));
4435 for i in 0..280 {
4436 std::fs::write(src.path().join("f.txt"), format!("payload {i}\n")).unwrap();
4437 assert!(git_ok(src.path(), &["add", "f.txt"]));
4438 assert!(git_ok(src.path(), &["commit", "-qm", &format!("c{i}")],));
4439 }
4440 assert!(git_ok(
4441 src.path(),
4442 &["push", "-q", remote.path().to_str().unwrap(), "main:main"],
4443 ));
4444 assert!(git_ok(remote.path(), &["repack", "-a", "-d", "-f"],));
4445
4446 let local = tempfile::tempdir().expect("local");
4447 init_repository(local.path(), false, "main", None, RefStorageFormat::Files).expect("init");
4448
4449 let opts = FetchOptions {
4450 refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
4451 initial_remote_fetch: true,
4452 tags: TagMode::None,
4453 ..Default::default()
4454 };
4455 let mut rec = PhaseRecorder {
4456 want_received: true,
4457 ..Default::default()
4458 };
4459 let outcome = fetch_local_with_progress(
4460 &local.path().join(".git"),
4461 remote.path(),
4462 &opts,
4463 &mut rec,
4464 &crate::progress::NeverCancel,
4465 )
4466 .expect("fetch_local");
4467 assert!(
4468 outcome.transfer.objects > 500,
4469 "expected a large hardlinked pack transfer"
4470 );
4471 assert!(
4472 rec.enumerating_counts.len() >= 3,
4473 "expected pack index scan progress under EnumeratingObjects, got {:?}",
4474 rec.enumerating_counts
4475 );
4476 assert_monotonic(&rec.enumerating_counts);
4477 assert_eq!(
4478 rec.started
4479 .iter()
4480 .filter(|(p, _)| *p == ProgressPhase::CopyingObjects)
4481 .count(),
4482 1,
4483 "expected a single CopyingObjects phase, got {:?}",
4484 rec.started
4485 );
4486 assert!(
4487 !rec.copying_counts.is_empty(),
4488 "expected copy progress after pack install, got {:?}",
4489 rec.copying_counts
4490 );
4491 assert_monotonic(&rec.copying_counts);
4492 assert!(
4493 rec.received.last().is_some_and(|s| s.received > 0),
4494 "expected received_objects only after objects were installed"
4495 );
4496 }
4497
4498 #[test]
4499 fn fetch_local_packed_remote_reports_incremental_copy_progress() {
4500 let remote = tempfile::tempdir().expect("remote");
4501 init_repository(remote.path(), true, "main", None, RefStorageFormat::Files)
4502 .expect("remote init");
4503 let objects = remote.path().join("objects");
4504 let (remote_odb, tip) = write_linear_commits(&objects, 280);
4505 crate::refs::write_ref(remote.path(), "refs/heads/main", &tip).expect("main ref");
4506 remote_odb
4507 .write(ObjectKind::Blob, b"unreachable orphan blob\n")
4508 .expect("orphan");
4509
4510 let local = tempfile::tempdir().expect("local");
4511 init_repository(local.path(), false, "main", None, RefStorageFormat::Files).expect("init");
4512
4513 let opts = {
4514 let mut opts = FetchOptions::new();
4515 opts.refspecs = vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()];
4516 opts.tags = TagMode::None;
4517 opts = opts.initial_remote_fetch(true);
4518 opts
4519 };
4520 let mut rec = PhaseRecorder {
4521 want_received: true,
4522 ..Default::default()
4523 };
4524 let outcome = fetch_local_with_progress(
4525 &local.path().join(".git"),
4526 remote.path(),
4527 &opts,
4528 &mut rec,
4529 &crate::progress::NeverCancel,
4530 )
4531 .expect("fetch_local");
4532 assert!(
4533 !rec.received.is_empty(),
4534 "expected received_objects callbacks"
4535 );
4536 for snapshot in &rec.received {
4537 assert!(snapshot.received <= snapshot.total);
4538 }
4539
4540 let enum_pos = rec
4541 .started
4542 .iter()
4543 .position(|(p, _)| *p == ProgressPhase::EnumeratingObjects)
4544 .expect("EnumeratingObjects");
4545 let copy_pos = rec
4546 .started
4547 .iter()
4548 .position(|(p, _)| *p == ProgressPhase::CopyingObjects)
4549 .expect("CopyingObjects");
4550 assert!(
4551 enum_pos < copy_pos,
4552 "enumerate before copy: {:?}",
4553 rec.started
4554 );
4555 assert_eq!(
4556 rec.started
4557 .iter()
4558 .filter(|(p, _)| *p == ProgressPhase::CopyingObjects)
4559 .count(),
4560 1,
4561 "expected a single CopyingObjects phase, got {:?}",
4562 rec.started
4563 );
4564 assert!(
4565 rec.counts.len() >= 3,
4566 "expected >=3 progress samples overall, got {:?}",
4567 rec.counts
4568 );
4569 assert!(
4570 rec.copying_counts.len() >= 3,
4571 "expected >=3 copy progress samples, got {:?}",
4572 rec.copying_counts
4573 );
4574 assert_monotonic(&rec.copying_counts);
4575 assert!(
4576 rec.copying_counts.last().copied().unwrap_or(0) >= outcome.transfer.objects as u64,
4577 "final copy count should reach transferred objects"
4578 );
4579 }
4580
4581 #[test]
4582 fn link_source_objects_progress_counts_objects_not_sidecar_files() {
4583 for want_received in [false, true] {
4584 let remote = tempfile::tempdir().expect("remote");
4585 assert!(git_ok(
4586 remote.path(),
4587 &["init", "-q", "-b", "main", "--bare"]
4588 ));
4589 let src = tempfile::tempdir().expect("src");
4590 assert!(git_ok(src.path(), &["init", "-q", "-b", "main"]));
4591 for i in 0..20 {
4592 std::fs::write(src.path().join("f.txt"), format!("payload {i}\n")).unwrap();
4593 assert!(git_ok(src.path(), &["add", "f.txt"]));
4594 assert!(git_ok(src.path(), &["commit", "-qm", &format!("c{i}")],));
4595 }
4596 assert!(git_ok(
4597 src.path(),
4598 &["push", "-q", remote.path().to_str().unwrap(), "main:main"],
4599 ));
4600 assert!(git_ok(remote.path(), &["repack", "-a", "-d", "-f"],));
4601
4602 let local = tempfile::tempdir().expect("local");
4603 init_repository(local.path(), false, "main", None, RefStorageFormat::Files)
4604 .expect("init");
4605
4606 let opts = {
4607 let mut opts = FetchOptions::new();
4608 opts.refspecs = vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()];
4609 opts.tags = TagMode::None;
4610 opts.link_source_objects = true;
4611 opts = opts.initial_remote_fetch(true);
4612 opts
4613 };
4614 let mut rec = PhaseRecorder {
4615 want_received,
4616 ..Default::default()
4617 };
4618 let outcome = fetch_local_with_progress(
4619 &local.path().join(".git"),
4620 remote.path(),
4621 &opts,
4622 &mut rec,
4623 &crate::progress::NeverCancel,
4624 )
4625 .expect("fetch_local");
4626
4627 if want_received {
4628 assert!(
4629 !rec.received.is_empty(),
4630 "expected received_objects when enabled"
4631 );
4632 } else {
4633 assert!(
4634 rec.received.is_empty(),
4635 "expected no received_objects when disabled"
4636 );
4637 }
4638 assert_copy_progress_within_total(&rec, outcome.transfer.objects as u64);
4639 assert_ne!(
4640 copying_phase_total(&rec),
4641 Some(1),
4642 "total must be object count, not ref count: {:?}",
4643 rec.started
4644 );
4645 }
4646 }
4647
4648 #[cfg_attr(
4649 not(feature = "full-tests"),
4650 ignore = "starts system git or is heavy; run with --features full-tests"
4651 )]
4652 #[test]
4653 fn fetch_local_reports_copy_progress() {
4654 let remote = tempfile::tempdir().expect("remote");
4655 assert!(git_ok(
4656 remote.path(),
4657 &["init", "-q", "-b", "main", "--bare"]
4658 ));
4659 let src = tempfile::tempdir().expect("src");
4660 assert!(git_ok(src.path(), &["init", "-q", "-b", "main"]));
4661 std::fs::write(src.path().join("f.txt"), "payload\n").unwrap();
4662 assert!(git_ok(src.path(), &["add", "f.txt"]));
4663 assert!(git_ok(src.path(), &["commit", "-qm", "c1"]));
4664 assert!(git_ok(
4665 src.path(),
4666 &["push", "-q", remote.path().to_str().unwrap(), "main:main",],
4667 ));
4668
4669 let local = tempfile::tempdir().expect("local");
4670 init_repository(local.path(), false, "main", None, RefStorageFormat::Files).expect("init");
4671
4672 let opts = {
4673 let mut opts = FetchOptions::new();
4674 opts.refspecs = vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()];
4675 opts.tags = TagMode::None;
4676 opts = opts.initial_remote_fetch(true);
4677 opts
4678 };
4679 let mut rec = PhaseRecorder::default();
4680 fetch_local_with_progress(
4681 &local.path().join(".git"),
4682 remote.path(),
4683 &opts,
4684 &mut rec,
4685 &crate::progress::NeverCancel,
4686 )
4687 .expect("fetch_local");
4688
4689 assert!(
4690 rec.started
4691 .iter()
4692 .any(|(p, _)| *p == ProgressPhase::CopyingObjects),
4693 "expected CopyingObjects phase"
4694 );
4695 assert!(
4696 rec.finished.contains(&ProgressPhase::CopyingObjects),
4697 "expected CopyingObjects finish"
4698 );
4699 assert!(!rec.counts.is_empty(), "expected copy progress counts");
4700 assert_monotonic(&rec.counts);
4701 let first_progress = rec.events.iter().find(|e| **e != "start").copied();
4702 assert_eq!(
4703 rec.events.first().copied(),
4704 Some("start"),
4705 "CopyingObjects must start before link/copy work reports progress"
4706 );
4707 assert!(
4708 first_progress.is_some(),
4709 "expected set/inc after start during copy"
4710 );
4711 }
4712
4713 #[test]
4714 fn transfer_stats_add_up_and_keep_kinds_only_when_known() {
4715 use crate::index_pack::ObjectKindCounts;
4716 let kinds = |commits, blobs| ObjectKindCounts {
4717 commits,
4718 trees: 0,
4719 blobs,
4720 tags: 0,
4721 };
4722 let mut total = TransferStats::default();
4723 total += TransferStats {
4724 objects: 3,
4725 bytes: 100,
4726 kinds: Some(kinds(1, 2)),
4727 };
4728 total += TransferStats::default();
4730 total += TransferStats {
4731 objects: 2,
4732 bytes: 50,
4733 kinds: Some(kinds(1, 1)),
4734 };
4735 assert_eq!(
4736 total,
4737 TransferStats {
4738 objects: 5,
4739 bytes: 150,
4740 kinds: Some(kinds(2, 3)),
4741 }
4742 );
4743
4744 total += TransferStats {
4746 objects: 4,
4747 bytes: 10,
4748 kinds: None,
4749 };
4750 assert_eq!(total.objects, 9);
4751 assert_eq!(total.bytes, 160);
4752 assert_eq!(total.kinds, None);
4753 }
4754
4755 #[cfg_attr(
4756 not(feature = "full-tests"),
4757 ignore = "starts system git or is heavy; run with --features full-tests"
4758 )]
4759 #[test]
4760 fn transfer_stats_for_local_operation_counts_new_pack_index() {
4761 let tmp = tempfile::tempdir().expect("tempdir");
4762 let repo = tmp.path().join("repo");
4763 std::fs::create_dir_all(&repo).unwrap();
4764 assert!(git_ok(&repo, &["init", "-q", "-b", "main", "."]));
4765 std::fs::write(repo.join("blob.txt"), b"payload\n").unwrap();
4766 assert!(git_ok(&repo, &["add", "blob.txt"]));
4767 assert!(git_ok(&repo, &["commit", "-qm", "c"]));
4768 assert!(git_ok(&repo, &["repack", "-a", "-d", "-f"]));
4769
4770 let git_dir = repo.join(".git");
4771 let pack_dir = git_dir.join("objects/pack");
4772 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
4773 let stats =
4774 transfer_stats_for_local_operation(&odb, &HashSet::new(), None, &HashSet::new())
4775 .expect("stats");
4776 assert!(stats.bytes > 0);
4777 assert!(stats.objects > 0);
4778
4779 let pack_path = std::fs::read_dir(&pack_dir)
4780 .expect("read pack dir")
4781 .filter_map(|e| e.ok())
4782 .map(|e| e.path())
4783 .find(|p| p.extension().is_some_and(|e| e == "pack"))
4784 .expect("pack file");
4785 let idx = pack_path.with_extension("idx");
4786 let index = crate::pack::read_pack_index_no_verify(&idx).expect("idx");
4787 assert_eq!(
4788 stats.objects,
4789 u64::try_from(index.len()).expect("count"),
4790 "object count must match the on-disk index entry count"
4791 );
4792 }
4793
4794 #[test]
4795 fn update_mode_modified_repo_excludes_rejected_tag_updates() {
4796 assert!(UpdateMode::New.modified_repo());
4797 assert!(UpdateMode::DeletedMissing.modified_repo());
4798 assert!(!UpdateMode::TagUpdateRejected.modified_repo());
4799 assert!(!UpdateMode::UpToDate.modified_repo());
4800 }
4801
4802 #[test]
4803 fn update_mode_report_classification_covers_every_mode() {
4804 use UpdateMode::*;
4805 for (mode, reported, rejected) in [
4806 (New, true, false),
4807 (FastForward, true, false),
4808 (Forced, true, false),
4809 (DeletedMissing, true, false),
4810 (NonFastForwardRejected, true, true),
4811 (TagUpdateRejected, true, true),
4812 (SourceObjectNotFound, true, true),
4813 (UpToDate, false, false),
4814 (NoChangeNeeded, false, false),
4815 (Unborn, false, false),
4816 ] {
4817 assert_eq!(mode.is_reported(), reported, "{mode:?}");
4818 assert_eq!(mode.is_rejected(), rejected, "{mode:?}");
4819 assert!(!(mode.modified_repo() && mode.is_rejected()), "{mode:?}");
4820 }
4821 }
4822
4823 #[test]
4824 fn fetch_outcome_summarizes_reported_changed_and_rejected_refs() {
4825 let update = |name: &str, mode| RefUpdate {
4826 remote_ref: format!("refs/heads/{name}"),
4827 local_ref: Some(format!("refs/remotes/origin/{name}")),
4828 old_oid: None,
4829 new_oid: None,
4830 mode,
4831 note: None,
4832 };
4833 let outcome = FetchOutcome {
4834 updates: vec![
4835 update("same", UpdateMode::UpToDate),
4836 update("new", UpdateMode::New),
4837 update("ff", UpdateMode::FastForward),
4838 update("noop", UpdateMode::NoChangeNeeded),
4839 ],
4840 ..Default::default()
4841 };
4842 let reported: Vec<&str> = outcome
4843 .reported_updates()
4844 .map(|u| u.remote_ref.as_str())
4845 .collect();
4846 assert_eq!(reported, ["refs/heads/new", "refs/heads/ff"]);
4847 assert_eq!(outcome.changed_ref_count(), 2);
4848 assert!(!outcome.has_rejections());
4849
4850 let rejected = FetchOutcome {
4851 updates: vec![update("diverged", UpdateMode::NonFastForwardRejected)],
4852 ..Default::default()
4853 };
4854 assert_eq!(rejected.changed_ref_count(), 0);
4855 assert!(rejected.has_rejections());
4856 assert_eq!(rejected.reported_updates().count(), 1);
4857 assert!(FetchOutcome::default().reported_updates().next().is_none());
4858 }
4859}