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