1use crate::environment::RepositoryOptions;
19use crate::partial_clone::ObjectFilterSpec;
20
21use std::collections::{HashMap, HashSet, VecDeque};
22use std::fs;
23use std::path::{Path, PathBuf};
24
25use crate::error::{Error, RemoteUnreadableReason, Result};
26use crate::fetch::{NoProgress, Progress};
27use crate::objects::{parse_commit, parse_tag, parse_tree, HashAlgo, ObjectId, ObjectKind};
28use crate::odb::Odb;
29use crate::progress::{ProgressPhase, ProgressSink};
30use crate::push_report::{PushRefResult, PushRefStatus};
31use crate::refspec::{parse_fetch_refspec, RefspecItem};
32use std::num::NonZeroU32;
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum UpdateMode {
40 New,
42 FastForward,
44 Forced,
46 UpToDate,
48 NoChangeNeeded,
50 NonFastForwardRejected,
52 TagUpdateRejected,
54 SourceObjectNotFound,
56 Unborn,
58 DeletedMissing,
60 RefLockRejected,
62}
63
64impl UpdateMode {
65 #[must_use]
70 pub fn modified_repo(self) -> bool {
71 matches!(
72 self,
73 Self::New | Self::FastForward | Self::Forced | Self::DeletedMissing
74 )
75 }
76
77 #[must_use]
80 pub fn is_rejected(self) -> bool {
81 matches!(
82 self,
83 Self::NonFastForwardRejected
84 | Self::TagUpdateRejected
85 | Self::SourceObjectNotFound
86 | Self::RefLockRejected
87 )
88 }
89
90 #[must_use]
95 pub fn is_reported(self) -> bool {
96 self.modified_repo() || self.is_rejected()
97 }
98}
99
100#[derive(Clone, Debug)]
102pub struct RefUpdate {
103 pub remote_ref: String,
105 pub local_ref: Option<String>,
107 pub old_oid: Option<ObjectId>,
109 pub new_oid: Option<ObjectId>,
111 pub mode: UpdateMode,
113 pub note: Option<String>,
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
119pub enum TagMode {
120 None,
122 #[default]
141 Following,
142 All,
145}
146
147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
149pub enum FetchDepth {
150 Depth(NonZeroU32),
153 Deepen(NonZeroU32),
156 Unshallow,
158}
159
160impl FetchDepth {
161 #[must_use]
163 pub fn depth(n: u32) -> Option<Self> {
164 NonZeroU32::new(n).map(Self::Depth)
165 }
166
167 #[must_use]
169 pub fn deepen(n: u32) -> Option<Self> {
170 NonZeroU32::new(n).map(Self::Deepen)
171 }
172}
173
174pub struct FetchOptions {
176 pub refspecs: Vec<String>,
178 pub negative_refspecs: Vec<String>,
180 pub tags: TagMode,
182 pub prune: bool,
184 pub dry_run: bool,
186 pub fetch_depth: Option<FetchDepth>,
189 pub deepen_since: Option<String>,
194 pub deepen_not: Vec<String>,
198 pub initial_remote_fetch: bool,
201 pub remote_name: Option<String>,
203 pub fetch_source: Option<String>,
206 pub configured_refspecs: Vec<String>,
209 pub clone_reflog: Option<CloneReflog>,
211 pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
213 pub network_trace: bool,
215 pub index_pack_threads: Option<usize>,
217 pub thread_policy: crate::parallel::ThreadPolicy,
219 pub environment: crate::environment::Environment,
221 pub pack_spool_only: bool,
223 pub http_protocol_version: Option<u8>,
225 pub remote_progress: bool,
228 pub filter: Option<ObjectFilterSpec>,
231 pub mark_received_pack_promisor: bool,
236 pub object_wants: Vec<ObjectId>,
238 pub promisor_object_fetch: bool,
240}
241
242impl Default for FetchOptions {
243 fn default() -> Self {
244 Self {
245 refspecs: Vec::new(),
246 negative_refspecs: Vec::new(),
247 tags: TagMode::Following,
248 prune: false,
249 dry_run: false,
250 fetch_depth: None,
251 deepen_since: None,
252 deepen_not: Vec::new(),
253 initial_remote_fetch: false,
254 remote_name: None,
255 fetch_source: None,
256 configured_refspecs: Vec::new(),
257 clone_reflog: None,
258 diagnostics: None,
259 network_trace: false,
260 index_pack_threads: None,
261 thread_policy: crate::parallel::ThreadPolicy::default(),
262 environment: crate::environment::Environment::empty(),
263 pack_spool_only: false,
264 http_protocol_version: None,
265 remote_progress: false,
266 filter: None,
267 mark_received_pack_promisor: false,
268 object_wants: Vec::new(),
269 promisor_object_fetch: false,
270 }
271 }
272}
273
274#[derive(Clone, Debug)]
276pub struct CloneReflog {
277 pub identity: crate::ident::Signature,
279 pub message: String,
281}
282
283impl Clone for FetchOptions {
284 fn clone(&self) -> Self {
285 Self {
286 refspecs: self.refspecs.clone(),
287 negative_refspecs: self.negative_refspecs.clone(),
288 tags: self.tags,
289 prune: self.prune,
290 dry_run: self.dry_run,
291 fetch_depth: self.fetch_depth,
292 deepen_since: self.deepen_since.clone(),
293 deepen_not: self.deepen_not.clone(),
294 initial_remote_fetch: self.initial_remote_fetch,
295 remote_name: self.remote_name.clone(),
296 fetch_source: self.fetch_source.clone(),
297 configured_refspecs: self.configured_refspecs.clone(),
298 clone_reflog: self.clone_reflog.clone(),
299 diagnostics: self.diagnostics.clone(),
300 network_trace: self.network_trace,
301 index_pack_threads: self.index_pack_threads,
302 thread_policy: self.thread_policy,
303 environment: self.environment.clone(),
304 pack_spool_only: self.pack_spool_only,
305 http_protocol_version: self.http_protocol_version,
306 remote_progress: self.remote_progress,
307 filter: self.filter,
308 mark_received_pack_promisor: self.mark_received_pack_promisor,
309 object_wants: self.object_wants.clone(),
310 promisor_object_fetch: self.promisor_object_fetch,
311 }
312 }
313}
314
315impl FetchOptions {
316 #[must_use]
322 pub fn has_deepen_request(&self) -> bool {
323 self.fetch_depth.is_some()
324 || self
325 .deepen_since
326 .as_deref()
327 .is_some_and(|v| !v.trim().is_empty())
328 || self.deepen_not.iter().any(|v| !v.trim().is_empty())
329 }
330
331 #[must_use]
333 pub(crate) fn wire_deepen(&self) -> Option<(u32, bool)> {
334 match self.fetch_depth {
335 Some(FetchDepth::Depth(n)) => Some((n.get(), false)),
336 Some(FetchDepth::Deepen(n)) => Some((n.get(), true)),
337 Some(FetchDepth::Unshallow) => Some((crate::shallow::INFINITE_DEPTH, false)),
338 None => None,
339 }
340 }
341
342 #[must_use]
344 pub(crate) fn absolute_depth_for_tags(&self) -> Option<u32> {
345 match self.fetch_depth {
346 Some(FetchDepth::Depth(n)) => Some(n.get()),
347 Some(FetchDepth::Deepen(_)) | Some(FetchDepth::Unshallow) | None => None,
348 }
349 }
350}
351
352pub fn validate_fetch_depth_local(local_git_dir: &Path, opts: &FetchOptions) -> Result<()> {
359 if matches!(opts.fetch_depth, Some(FetchDepth::Unshallow)) {
360 let local = crate::shallow::load_shallow_oids(local_git_dir)?;
361 if local.is_empty() {
362 return Err(Error::FetchUnshallowNotShallow);
363 }
364 }
365 Ok(())
366}
367
368pub(crate) fn validate_fetch_depth_server(
370 opts: &FetchOptions,
371 v0_caps: Option<&str>,
372 v2_caps: Option<&[String]>,
373) -> Result<()> {
374 let needs_shallow = opts.has_deepen_request()
375 || opts.wire_deepen().is_some()
376 || opts
377 .deepen_since
378 .as_deref()
379 .is_some_and(|s| !s.trim().is_empty())
380 || opts.deepen_not.iter().any(|s| !s.trim().is_empty());
381 if !needs_shallow {
382 return Ok(());
383 }
384 if let Some(caps) = v0_caps {
385 if !caps.contains("shallow") {
386 return Err(Error::FetchShallowUnsupported);
387 }
388 if opts.wire_deepen().is_some_and(|(_, rel)| rel) && !caps.contains("deepen-relative") {
389 return Err(Error::FetchDeepenRelativeUnsupported);
390 }
391 } else if let Some(caps) = v2_caps {
392 if !crate::protocol_v2::fetch_supports_feature(caps, "shallow") {
393 return Err(Error::FetchShallowUnsupported);
394 }
395 }
396 Ok(())
397}
398
399pub fn resolve_fetch_prune(
409 config: &crate::config::ConfigSet,
410 remote: &str,
411 cli_prune: Option<bool>,
412) -> Result<bool> {
413 if let Some(enabled) = cli_prune {
414 return Ok(enabled);
415 }
416 let remote_key = format!("remote.{remote}.prune");
417 if let Some(value) = optional_config_bool(config, &remote_key)? {
418 return Ok(value);
419 }
420 if let Some(value) = optional_config_bool(config, "fetch.prune")? {
421 return Ok(value);
422 }
423 Ok(false)
424}
425
426#[must_use]
433pub fn resolve_fetch_tags(
434 config: &crate::config::ConfigSet,
435 remote: &str,
436 cli_tags: Option<bool>,
437) -> TagMode {
438 if let Some(all) = cli_tags {
439 return if all { TagMode::All } else { TagMode::None };
440 }
441 let key = format!("remote.{remote}.tagOpt");
442 match config.get(&key).as_deref().map(str::trim) {
443 Some("--tags") | Some("tags") | Some("all") => TagMode::All,
444 Some("--no-tags") | Some("no-tags") | Some("no") | Some("never") => TagMode::None,
445 _ => TagMode::Following,
446 }
447}
448
449fn optional_config_bool(config: &crate::config::ConfigSet, key: &str) -> Result<Option<bool>> {
450 match config.get_bool(key) {
451 None => Ok(None),
452 Some(Ok(value)) => Ok(Some(value)),
453 Some(Err(message)) => Err(Error::Config(crate::error::ConfigError::Other(format!(
454 "invalid boolean for '{key}': {message}"
455 )))),
456 }
457}
458
459#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
461pub struct TransferStats {
462 pub objects: u64,
464 pub bytes: u64,
466 pub kinds: Option<crate::index_pack::ObjectKindCounts>,
469}
470
471impl TransferStats {
472 #[must_use]
475 pub fn from_ingested_pack(ingested: &crate::index_pack::IngestedPack) -> Self {
476 Self {
477 objects: u64::try_from(ingested.object_ids.len()).unwrap_or(u64::MAX),
478 bytes: ingested.pack_bytes,
479 kinds: Some(ingested.object_counts),
480 }
481 }
482}
483
484impl std::ops::AddAssign for TransferStats {
489 fn add_assign(&mut self, other: Self) {
490 self.kinds = match (self.kinds, other.kinds) {
491 (Some(mut mine), Some(theirs)) => {
492 mine += theirs;
493 Some(mine)
494 }
495 (mine, None) if other.objects == 0 => mine,
496 (None, theirs) if self.objects == 0 => theirs,
497 _ => None,
498 };
499 self.objects += other.objects;
500 self.bytes += other.bytes;
501 }
502}
503
504#[derive(Clone, Debug, Default)]
506pub struct FetchOutcome {
507 pub updates: Vec<RefUpdate>,
509 pub default_branch: Option<String>,
511 pub new_shallow: Vec<ObjectId>,
515 pub new_unshallow: Vec<ObjectId>,
519 pub transfer: TransferStats,
521}
522
523impl FetchOutcome {
524 #[must_use]
526 pub fn branch_count(&self) -> u64 {
527 self.updates
528 .iter()
529 .filter(|u| u.remote_ref.starts_with("refs/heads/"))
530 .count() as u64
531 }
532
533 #[must_use]
535 pub fn tag_count(&self) -> u64 {
536 self.updates
537 .iter()
538 .filter(|u| u.remote_ref.starts_with("refs/tags/") && !u.remote_ref.ends_with("^{}"))
539 .count() as u64
540 }
541
542 pub fn reported_updates(&self) -> impl Iterator<Item = &RefUpdate> {
544 self.updates.iter().filter(|u| u.mode.is_reported())
545 }
546
547 #[must_use]
552 pub fn changed_ref_count(&self) -> usize {
553 self.updates
554 .iter()
555 .filter(|u| u.mode.modified_repo())
556 .count()
557 }
558
559 #[must_use]
561 pub fn has_rejections(&self) -> bool {
562 self.updates.iter().any(|u| u.mode.is_rejected())
563 }
564}
565
566fn object_kind_counts_for_oids(
567 odb: &Odb,
568 oids: &HashSet<ObjectId>,
569) -> Result<crate::index_pack::ObjectKindCounts> {
570 use crate::index_pack::ObjectKindCounts;
571 let mut kinds = ObjectKindCounts::default();
572 for oid in oids {
573 kinds.add(odb.read_info(oid)?.kind);
574 }
575 Ok(kinds)
576}
577
578fn loose_transfer_stats(objects_dir: &Path, oids: &HashSet<ObjectId>) -> Result<(u64, u64)> {
579 let mut objects = 0u64;
580 let mut bytes = 0u64;
581 for oid in oids {
582 let path = oid.loose_path_in(objects_dir);
583 if path.is_file() {
584 objects += 1;
585 bytes += fs::metadata(&path).map_err(Error::Io)?.len();
586 }
587 }
588 Ok((objects, bytes))
589}
590
591fn pack_file_basenames(pack_dir: &Path) -> HashSet<String> {
592 let mut names = HashSet::new();
593 let Ok(entries) = fs::read_dir(pack_dir) else {
594 return names;
595 };
596 for entry in entries.flatten() {
597 let path = entry.path();
598 if path.extension().is_some_and(|e| e == "pack") {
599 if let Some(name) = path.file_name() {
600 names.insert(name.to_string_lossy().into_owned());
601 }
602 }
603 }
604 names
605}
606
607const LOCAL_LINK_KINDS_OID_PROBE_MAX: u64 = 4096;
608
609fn oids_from_newly_linked_pack_indexes(
610 local_odb: &Odb,
611 pack_dir_before: &HashSet<String>,
612) -> Result<Vec<ObjectId>> {
613 let pack_dir = local_odb.objects_dir().join("pack");
614 let mut oids = Vec::new();
615 if !pack_dir.is_dir() {
616 return Ok(oids);
617 }
618 for entry in fs::read_dir(&pack_dir).map_err(Error::Io)? {
619 let path = entry.map_err(Error::Io)?.path();
620 if path.extension().is_none_or(|e| e != "pack") {
621 continue;
622 }
623 let Some(name) = path.file_name() else {
624 continue;
625 };
626 if pack_dir_before.contains(name.to_string_lossy().as_ref()) {
627 continue;
628 }
629 let idx = path.with_extension("idx");
630 if !idx.is_file() {
631 continue;
632 }
633 let index = crate::pack::read_pack_index_no_verify(&idx)?;
634 for entry in index.iter() {
635 let oid = ObjectId::from_bytes(entry.oid())?;
636 oids.push(oid);
637 }
638 }
639 Ok(oids)
640}
641
642fn enrich_local_link_transfer_kinds(
643 local_odb: &Odb,
644 pack_dir_before: &HashSet<String>,
645 loose_reachable: &HashSet<ObjectId>,
646 transfer: &mut TransferStats,
647) -> Result<()> {
648 if transfer.kinds.is_some() || transfer.objects == 0 {
649 return Ok(());
650 }
651 if transfer.objects > LOCAL_LINK_KINDS_OID_PROBE_MAX {
652 return Ok(());
653 }
654 let mut probe: HashSet<ObjectId> = loose_reachable.clone();
655 for oid in oids_from_newly_linked_pack_indexes(local_odb, pack_dir_before)? {
656 probe.insert(oid);
657 }
658 if u64::try_from(probe.len()).unwrap_or(u64::MAX) != transfer.objects {
659 return Ok(());
660 }
661 transfer.kinds = Some(object_kind_counts_for_oids(local_odb, &probe)?);
662 Ok(())
663}
664
665fn transfer_stats_for_local_operation(
667 local_odb: &Odb,
668 installed: &HashSet<ObjectId>,
669 pack_dir_before: &HashSet<String>,
670) -> Result<TransferStats> {
671 let objects_dir = local_odb.objects_dir();
672 let pack_dir = objects_dir.join("pack");
673 let mut bytes = 0u64;
674 let mut objects_from_new_packs = 0u64;
675 if pack_dir.is_dir() {
676 for entry in fs::read_dir(&pack_dir).map_err(Error::Io)? {
677 let path = entry.map_err(Error::Io)?.path();
678 if path.extension().is_none_or(|e| e != "pack") {
679 continue;
680 }
681 let Some(name) = path.file_name() else {
682 continue;
683 };
684 let name = name.to_string_lossy();
685 if pack_dir_before.contains(name.as_ref()) {
686 continue;
687 }
688 bytes += fs::metadata(&path).map_err(Error::Io)?.len();
689 let idx = path.with_extension("idx");
690 if idx.is_file() {
691 let index = crate::pack::read_pack_index_no_verify(&idx)?;
692 objects_from_new_packs += u64::try_from(index.len()).unwrap_or(u64::MAX);
693 }
694 }
695 }
696 let (loose_objects, loose_bytes) = loose_transfer_stats(objects_dir, installed)?;
697 bytes = bytes.saturating_add(loose_bytes);
698 let installed_count = u64::try_from(installed.len()).unwrap_or(u64::MAX);
699 let objects = if objects_from_new_packs > 0 {
700 objects_from_new_packs.saturating_add(loose_objects)
701 } else {
702 installed_count
703 };
704 let kinds = if objects == installed_count {
705 Some(object_kind_counts_for_oids(local_odb, installed)?)
706 } else {
707 None
708 };
709 Ok(TransferStats {
710 objects,
711 bytes,
712 kinds,
713 })
714}
715
716#[derive(Clone, Debug, Default)]
718pub struct PushRefSpec {
719 pub src: Option<ObjectId>,
721 pub src_ref: Option<String>,
723 pub dst: String,
725 pub force: bool,
727 pub delete: bool,
729 pub expected_old: Option<ObjectId>,
732 pub expect_absent: bool,
737}
738
739#[must_use]
741pub(crate) fn push_spec_local_ref(spec: &PushRefSpec) -> Option<String> {
742 spec.src_ref.clone()
743}
744
745#[must_use]
751pub fn resolve_push_delete_ref(
752 git_dir: &Path,
753 remote_ref_names: &HashSet<String>,
754 spec: &str,
755) -> String {
756 if spec.starts_with("refs/") {
757 return spec.to_owned();
758 }
759 let branch = format!("refs/heads/{spec}");
760 let tag = format!("refs/tags/{spec}");
761 if crate::refs::resolve_ref(git_dir, &branch).is_ok() {
762 return branch;
763 }
764 if remote_ref_names.contains(&branch) {
765 return branch;
766 }
767 if crate::refs::resolve_ref(git_dir, &tag).is_ok() {
768 return tag;
769 }
770 if remote_ref_names.contains(&tag) {
771 return tag;
772 }
773 branch
774}
775
776#[derive(Clone, Default)]
778pub struct PushOptions {
779 pub atomic: bool,
781 pub dry_run: bool,
783 pub tracking_remote: Option<String>,
786 pub push_options: Vec<String>,
797 pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
799 pub network_trace: bool,
801 pub remote_ref_tips: Option<Vec<ObjectId>>,
809}
810
811#[derive(Clone, Debug, Default)]
813pub struct PushOutcome {
814 pub results: Vec<PushRefResult>,
816 pub remote_messages: Vec<String>,
820}
821
822pub use crate::pack_objects::{
823 build_pack, build_pack_from_send_list, reachable_closure, PackBuildOptions, PackObjects,
824 PackObjectsOptions, PackStats,
825};
826
827pub(crate) use crate::pack_objects::build_pack_for_local_fetch;
828
829fn pack_build_options_for_send(
830 source_odb: &Odb,
831 oids: &[ObjectId],
832 base: &PackBuildOptions,
833) -> PackBuildOptions {
834 if oids.is_empty() || oids.len() > 512 {
835 return *base;
836 }
837 let commits_tags_only = oids.iter().all(|oid| {
838 source_odb
839 .read(oid)
840 .map(|obj| matches!(obj.kind, ObjectKind::Commit | ObjectKind::Tag))
841 .unwrap_or(false)
842 });
843 if !commits_tags_only {
844 return *base;
845 }
846 let mut fast = *base;
847 fast.delta = false;
848 fast.reuse_deltas = false;
849 fast.thin = false;
850 fast
851}
852
853fn ingest_pack_options_for_fetch(
854 local_git_dir: &Path,
855 fetch_opts: &FetchOptions,
856) -> Result<crate::index_pack::IngestPackOptions> {
857 let pack_config =
858 crate::config::ConfigSet::load(&fetch_opts.environment, Some(local_git_dir), true)?;
859 let index_threads = crate::parallel::index_pack_threads_for_ingest(
860 fetch_opts.thread_policy,
861 &pack_config,
862 fetch_opts.index_pack_threads,
863 )?;
864 Ok(crate::index_pack::IngestPackOptions {
865 fix_thin: true,
866 skip_post_index_verify: true,
867 threads: index_threads,
868 merged_config: Some(pack_config),
869 environment: fetch_opts.environment.clone(),
870 mark_promisor_pack: fetch_opts.mark_received_pack_promisor,
871 trusted_index_rows: None,
872 })
873}
874
875#[allow(clippy::too_many_arguments)]
877fn copy_objects_for_local_fetch(
878 local_git_dir: &Path,
879 remote_odb: &Odb,
880 local_odb: &Odb,
881 wants: &[ObjectId],
882 haves: &[ObjectId],
883 source_shallow: &HashSet<ObjectId>,
884 have_shallow: &HashSet<ObjectId>,
885 try_pack_link: bool,
886 pack_build: &PackBuildOptions,
887 fetch_opts: &FetchOptions,
888 progress: &mut dyn ProgressSink,
889 cancel: &dyn crate::progress::Cancel,
890) -> Result<(HashSet<ObjectId>, TransferStats)> {
891 use crate::local_object_copy::{
892 link_loose_objects, link_loose_objects_for_oids, link_remote_pack_files, linked_object_set,
893 refresh_local_odb_after_link, try_satisfy_via_object_link,
894 };
895
896 let remote_objects = remote_odb.objects_dir();
897 let local_objects = local_odb.objects_dir();
898 let pack_dir_before = pack_file_basenames(&local_objects.join("pack"));
899 let remote_has_alternates = !crate::pack::read_alternates_recursive(remote_objects)?.is_empty();
900
901 if !remote_has_alternates
902 && !pack_build.thin
903 && try_pack_link
904 && haves.is_empty()
905 && !wants.is_empty()
906 {
907 progress.start(ProgressPhase::CopyingObjects, Some(wants.len() as u64));
908 crate::progress::poll_cancel(progress, cancel)?;
909 let linked_packs =
910 link_remote_pack_files(remote_objects, local_objects, progress, cancel, wants.len())?;
911 if linked_packs > 0 {
912 refresh_local_odb_after_link(local_odb);
913 }
914 let pack_alone_satisfies_wants =
915 linked_packs > 0 && wants.iter().all(|oid| local_odb.exists(oid));
916 let linked_loose = link_loose_objects(remote_objects, local_objects, progress, cancel)?;
917 if linked_packs > 0 || linked_loose > 0 {
918 refresh_local_odb_after_link(local_odb);
919 }
920 if (linked_packs > 0 || linked_loose > 0) && wants.iter().all(|oid| local_odb.exists(oid)) {
921 if linked_loose == 0 {
922 progress.set(wants.len() as u64);
923 }
924 progress.finish();
925 let stats_loose = if pack_alone_satisfies_wants {
926 HashSet::new()
927 } else {
928 crate::local_object_copy::loose_reachable_from_roots(
929 local_odb,
930 local_objects,
931 wants,
932 source_shallow,
933 cancel,
934 )?
935 };
936 let oids = if stats_loose.is_empty() {
937 linked_object_set(local_odb, wants)
938 } else {
939 stats_loose.clone()
940 };
941 let mut transfer =
942 transfer_stats_for_local_operation(local_odb, &stats_loose, &pack_dir_before)?;
943 enrich_local_link_transfer_kinds(
944 local_odb,
945 &pack_dir_before,
946 &stats_loose,
947 &mut transfer,
948 )?;
949 return Ok((oids, transfer));
950 }
951 progress.finish();
952 }
953
954 let have_tips: HashSet<ObjectId> = haves.iter().copied().collect();
955 let send_set = collect_reachable_excluding_peer(
956 remote_odb,
957 wants,
958 local_odb,
959 &have_tips,
960 false,
961 source_shallow,
962 )?;
963 let send = &send_set.objects;
964
965 if !pack_build.thin && try_pack_link {
966 progress.start(ProgressPhase::CopyingObjects, Some(send.len() as u64));
967 crate::progress::poll_cancel(progress, cancel)?;
968 if try_satisfy_via_object_link(
969 remote_objects,
970 local_objects,
971 local_odb,
972 send,
973 false,
974 progress,
975 cancel,
976 )? {
977 progress.set(send.len() as u64);
978 progress.finish();
979 let oids = linked_object_set(local_odb, send);
980 let transfer = transfer_stats_for_local_operation(local_odb, &oids, &pack_dir_before)?;
981 return Ok((oids, transfer));
982 }
983 progress.finish();
984 }
985
986 let mut still_missing = objects_missing_from_peer(local_odb, send);
987 if still_missing.is_empty() {
988 let oids = linked_object_set(local_odb, send);
989 let transfer = transfer_stats_for_local_operation(local_odb, &oids, &pack_dir_before)?;
990 return Ok((oids, transfer));
991 }
992
993 progress.start(
997 ProgressPhase::CopyingObjects,
998 Some(still_missing.len() as u64),
999 );
1000 let linked_loose = link_loose_objects_for_oids(
1001 remote_objects,
1002 local_objects,
1003 &still_missing,
1004 progress,
1005 cancel,
1006 )?;
1007 if linked_loose > 0 {
1008 refresh_local_odb_after_link(local_odb);
1009 still_missing = local_odb.missing_oids(still_missing)?;
1010 }
1011 progress.finish();
1012 if still_missing.is_empty() {
1013 let oids = linked_object_set(local_odb, send);
1014 let transfer = transfer_stats_for_local_operation(local_odb, &oids, &pack_dir_before)?;
1015 return Ok((oids, transfer));
1016 }
1017
1018 let pack_opts = pack_build_options_for_send(remote_odb, &still_missing, pack_build);
1019 progress.start(
1020 ProgressPhase::CopyingObjects,
1021 Some(still_missing.len() as u64),
1022 );
1023 crate::progress::poll_cancel(progress, cancel)?;
1024 let pack = build_pack_for_local_transfer(
1025 remote_odb,
1026 &send_set,
1027 &still_missing,
1028 local_odb,
1029 haves,
1030 source_shallow,
1031 have_shallow,
1032 &pack_opts,
1033 )?;
1034 crate::progress::poll_cancel(progress, cancel)?;
1035 let ingest_opts = ingest_pack_options_for_fetch(local_git_dir, fetch_opts)?;
1036 let ingested = crate::index_pack::ingest_received_pack_with_progress(
1037 pack,
1038 local_odb,
1039 &ingest_opts,
1040 None,
1041 Some(cancel),
1042 )?;
1043 progress.inc(ingested.object_ids.len() as u64);
1044 progress.finish();
1045 refresh_local_odb_after_link(local_odb);
1046 let mut out = linked_object_set(local_odb, send);
1047 out.extend(ingested.object_ids);
1048 let transfer = transfer_stats_for_local_operation(local_odb, &out, &pack_dir_before)?;
1049 Ok((out, transfer))
1050}
1051
1052#[allow(clippy::too_many_arguments)]
1054fn copy_objects_for_local_push(
1055 local_odb: &Odb,
1056 remote_odb: &Odb,
1057 wants: &[ObjectId],
1058 haves: &[ObjectId],
1059 source_shallow: &HashSet<ObjectId>,
1060 have_shallow: &HashSet<ObjectId>,
1061 opts: &PackBuildOptions,
1062) -> Result<()> {
1063 use crate::local_object_copy::{
1064 link_loose_objects_for_oids, refresh_local_odb_after_link, try_satisfy_via_object_link,
1065 };
1066
1067 let local_objects = local_odb.objects_dir();
1068 let remote_objects = remote_odb.objects_dir();
1069
1070 let have_tips: HashSet<ObjectId> = haves.iter().copied().collect();
1071 let send_set = collect_reachable_excluding_peer(
1072 local_odb,
1073 wants,
1074 remote_odb,
1075 &have_tips,
1076 false,
1077 source_shallow,
1078 )?;
1079 let send = &send_set.objects;
1080
1081 let mut progress = crate::progress::NullProgress;
1082 let cancel = crate::progress::NeverCancel;
1083 if !opts.thin
1084 && try_satisfy_via_object_link(
1085 local_objects,
1086 remote_objects,
1087 remote_odb,
1088 send,
1089 false,
1090 &mut progress,
1091 &cancel,
1092 )?
1093 {
1094 return Ok(());
1095 }
1096
1097 let mut still_missing = objects_missing_from_peer(remote_odb, send);
1098 if still_missing.is_empty() {
1099 return Ok(());
1100 }
1101
1102 if !haves.is_empty() {
1103 let linked_loose = link_loose_objects_for_oids(
1104 local_objects,
1105 remote_objects,
1106 &still_missing,
1107 &mut progress,
1108 &cancel,
1109 )?;
1110 if linked_loose > 0 {
1111 refresh_local_odb_after_link(remote_odb);
1112 still_missing = remote_odb.missing_oids(still_missing)?;
1113 }
1114 if still_missing.is_empty() {
1115 return Ok(());
1116 }
1117 }
1118
1119 let pack_opts = pack_build_options_for_send(local_odb, &still_missing, opts);
1120 let pack = build_pack_for_local_transfer(
1121 local_odb,
1122 &send_set,
1123 &still_missing,
1124 remote_odb,
1125 haves,
1126 source_shallow,
1127 have_shallow,
1128 &pack_opts,
1129 )?;
1130 crate::index_pack::ingest_received_pack(
1131 pack,
1132 remote_odb,
1133 &crate::index_pack::IngestPackOptions {
1134 fix_thin: pack_opts.thin,
1135 ..Default::default()
1136 },
1137 )?;
1138 refresh_local_odb_after_link(remote_odb);
1139 Ok(())
1140}
1141
1142fn objects_missing_from_peer(peer_odb: &Odb, send: &[ObjectId]) -> Vec<ObjectId> {
1148 peer_odb.reprepare_local_packs();
1149 send.iter()
1150 .copied()
1151 .filter(|oid| !peer_odb.exists_materialized_local_without_rescan(oid))
1152 .collect()
1153}
1154
1155#[allow(clippy::too_many_arguments)]
1164fn build_pack_for_local_transfer(
1165 source_odb: &Odb,
1166 send_set: &PeerSendSet,
1167 missing: &[ObjectId],
1168 peer_odb: &Odb,
1169 haves: &[ObjectId],
1170 source_shallow: &HashSet<ObjectId>,
1171 have_shallow: &HashSet<ObjectId>,
1172 opts: &PackBuildOptions,
1173) -> Result<Vec<u8>> {
1174 if haves.is_empty() && !opts.thin {
1175 return crate::pack_objects::build_pack_from_send_list(
1176 source_odb,
1177 missing,
1178 &HashSet::new(),
1179 opts,
1180 &send_set.name_hashes,
1181 );
1182 }
1183 build_pack_for_local_fetch(
1184 source_odb,
1185 missing,
1186 peer_odb,
1187 haves,
1188 source_shallow,
1189 have_shallow,
1190 opts,
1191 )
1192}
1193
1194struct PeerSendSet {
1196 objects: Vec<ObjectId>,
1198 name_hashes: HashMap<ObjectId, u32>,
1200}
1201
1202fn collect_reachable_excluding_peer(
1206 source_odb: &Odb,
1207 roots: &[ObjectId],
1208 peer_odb: &Odb,
1209 peer_boundary: &HashSet<ObjectId>,
1210 skip_missing: bool,
1211 shallow_grafts: &HashSet<ObjectId>,
1212) -> Result<PeerSendSet> {
1213 use crate::pack_name_hash::pack_name_hash_extend;
1214
1215 let mut visited: HashSet<ObjectId> = HashSet::new();
1216 let mut ordered: Vec<ObjectId> = Vec::new();
1217 let mut name_hashes: HashMap<ObjectId, u32> = HashMap::new();
1218 let mut queue: VecDeque<(ObjectId, u32)> = VecDeque::new();
1220
1221 peer_odb.reprepare_local_packs();
1224 let at_boundary = |oid: ObjectId| {
1225 peer_boundary.contains(&oid) || peer_odb.exists_materialized_local_without_rescan(&oid)
1226 };
1227
1228 let enqueue = |oid: ObjectId,
1232 prefix_hash: Option<u32>,
1233 queue: &mut VecDeque<(ObjectId, u32)>,
1234 visited: &mut HashSet<ObjectId>,
1235 ordered: &mut Vec<ObjectId>|
1236 -> bool {
1237 if !visited.insert(oid) || at_boundary(oid) {
1238 return false;
1239 }
1240 ordered.push(oid);
1241 if let Some(prefix_hash) = prefix_hash {
1242 queue.push_back((oid, prefix_hash));
1243 }
1244 true
1245 };
1246
1247 for &root in roots {
1248 enqueue(root, Some(0), &mut queue, &mut visited, &mut ordered);
1249 }
1250
1251 while let Some((oid, prefix_hash)) = queue.pop_front() {
1252 let obj = match source_odb.read(&oid) {
1253 Ok(o) => o,
1254 Err(_) if skip_missing => continue,
1255 Err(e) => return Err(e),
1256 };
1257 match obj.kind {
1258 ObjectKind::Commit => {
1259 let commit = parse_commit(&obj.data)?;
1260 if !shallow_grafts.contains(&oid) {
1261 for parent in commit.parents {
1262 enqueue(parent, Some(0), &mut queue, &mut visited, &mut ordered);
1263 }
1264 }
1265 enqueue(commit.tree, Some(0), &mut queue, &mut visited, &mut ordered);
1266 }
1267 ObjectKind::Tree => {
1268 for entry in parse_tree(&obj.data)? {
1269 if entry.mode == 0o160000 {
1270 continue;
1271 }
1272 let path_hash = pack_name_hash_extend(prefix_hash, &entry.name);
1273 let queued = if entry.mode & 0o170000 == 0o040000 {
1274 let dir_hash = pack_name_hash_extend(path_hash, b"/");
1275 enqueue(
1276 entry.oid,
1277 Some(dir_hash),
1278 &mut queue,
1279 &mut visited,
1280 &mut ordered,
1281 )
1282 } else {
1283 enqueue(entry.oid, None, &mut queue, &mut visited, &mut ordered)
1284 };
1285 if queued {
1286 name_hashes.insert(entry.oid, path_hash);
1287 }
1288 }
1289 }
1290 ObjectKind::Tag => {
1291 let tag = parse_tag(&obj.data)?;
1292 enqueue(
1293 tag.object,
1294 Some(prefix_hash),
1295 &mut queue,
1296 &mut visited,
1297 &mut ordered,
1298 );
1299 }
1300 ObjectKind::Blob => {}
1301 }
1302 }
1303
1304 Ok(PeerSendSet {
1305 objects: ordered,
1306 name_hashes,
1307 })
1308}
1309
1310pub fn fetch_local(
1312 local_git_dir: &Path,
1313 remote_git_dir: &Path,
1314 opts: &FetchOptions,
1315) -> Result<FetchOutcome> {
1316 fetch_local_with_progress(
1317 local_git_dir,
1318 remote_git_dir,
1319 opts,
1320 &mut NoProgress,
1321 &crate::progress::NeverCancel,
1322 )
1323}
1324
1325pub(crate) struct FetchMatchSet {
1327 pub(crate) matched: Vec<MatchedRef>,
1328 pub(crate) positive: Vec<RefspecItem>,
1329 pub(crate) negatives: Vec<RefspecItem>,
1330 pub(crate) explicit_refspec_fetch: bool,
1331}
1332
1333pub(crate) fn build_fetch_matched_set(
1335 opts: &FetchOptions,
1336 remote_refs: &[(String, ObjectId)],
1337 remote_head_oid: Option<ObjectId>,
1338) -> Result<FetchMatchSet> {
1339 use crate::fetch_request::{FetchRefspecInput, FetchRefspecPlan, FetchRemoteAdvertisement};
1340
1341 let remote = FetchRemoteAdvertisement {
1342 refs: remote_refs,
1343 head_oid: remote_head_oid,
1344 };
1345 let plan = FetchRefspecPlan::resolve(
1346 &FetchRefspecInput {
1347 user_refspecs: opts.refspecs.clone(),
1348 configured_refspecs: opts.configured_refspecs.clone(),
1349 remote_name: opts.remote_name.clone(),
1350 allow_missing_explicit_for_prune: opts.prune,
1351 },
1352 remote,
1353 )?;
1354 let explicit_refspec_fetch = plan.is_explicit();
1355
1356 let mut negatives = match &plan {
1357 FetchRefspecPlan::Pattern { negatives, .. } | FetchRefspecPlan::Mixed { negatives, .. } => {
1358 negatives.clone()
1359 }
1360 };
1361 for spec in &opts.negative_refspecs {
1362 let item = parse_fetch_refspec(spec)
1363 .map_err(|e| Error::Message(format!("invalid negative refspec '{spec}': {e}")))?;
1364 negatives.push(item);
1365 }
1366
1367 let mut positive = match &plan {
1368 FetchRefspecPlan::Pattern { positive, .. } => positive.clone(),
1369 FetchRefspecPlan::Mixed {
1370 pattern_positive, ..
1371 } => {
1372 let mut positive = pattern_positive.clone();
1373 for spec in &opts.refspecs {
1374 if let Ok(item) = parse_fetch_refspec(spec) {
1375 if !item.negative && !item.pattern && !item.matching {
1376 positive.push(item);
1377 }
1378 }
1379 }
1380 positive
1381 }
1382 };
1383 if opts.prune {
1384 crate::fetch_request::supplement_positive_for_explicit_prune(
1385 &mut positive,
1386 &opts.configured_refspecs,
1387 opts.remote_name.as_deref(),
1388 );
1389 }
1390
1391 let matched =
1392 crate::fetch_request::matched_refs_with_negatives(&plan, remote, &negatives, opts.prune)?;
1393 Ok(FetchMatchSet {
1394 matched,
1395 positive,
1396 negatives,
1397 explicit_refspec_fetch,
1398 })
1399}
1400
1401pub(crate) fn enforce_fetch_ref_update(
1403 local_ref: &str,
1404 old: Option<ObjectId>,
1405 new: ObjectId,
1406 mode: UpdateMode,
1407 opts: &FetchOptions,
1408 explicit_refspec_fetch: bool,
1409 local_repo: Option<&crate::repo::Repository>,
1410) -> Result<()> {
1411 if opts.dry_run {
1412 return Ok(());
1413 }
1414 let would_write = matches!(
1415 mode,
1416 UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
1417 );
1418 if let Some(short) = local_ref.strip_prefix("refs/heads/") {
1419 if old.is_some()
1420 && old != Some(new)
1421 && (would_write || mode == UpdateMode::NonFastForwardRejected)
1422 {
1423 if let Some(repo) = local_repo {
1424 if crate::worktree::find_worktree_with_branch_checked_out(repo, short, None)?
1425 .is_some()
1426 {
1427 return Err(Error::FetchIntoCheckedOutBranch {
1428 branch: short.to_owned(),
1429 });
1430 }
1431 }
1432 }
1433 }
1434 let _ = explicit_refspec_fetch;
1435 Ok(())
1436}
1437
1438pub(crate) fn ensure_local_remote_repository(remote_git_dir: &Path) -> Result<()> {
1442 if crate::submodule_gitdir::is_valid_git_dir(remote_git_dir) {
1443 return Ok(());
1444 }
1445 Err(Error::remote_unreadable(
1446 remote_git_dir.display().to_string(),
1447 RemoteUnreadableReason::RepositoryNotFound,
1448 ))
1449}
1450
1451pub fn fetch_local_with_progress(
1456 local_git_dir: &Path,
1457 remote_git_dir: &Path,
1458 opts: &FetchOptions,
1459 progress: &mut dyn Progress,
1460 cancel: &dyn crate::progress::Cancel,
1461) -> Result<FetchOutcome> {
1462 crate::progress::poll_cancel(progress, cancel)?;
1463 ensure_local_remote_repository(remote_git_dir)?;
1464 let local = crate::fetch::open_fetch_target(local_git_dir, opts)?;
1465 fetch_local_into(&local, remote_git_dir, opts, progress, cancel)
1466}
1467
1468pub(crate) fn fetch_local_into(
1470 local: &crate::repo::Repository,
1471 remote_git_dir: &Path,
1472 opts: &FetchOptions,
1473 progress: &mut dyn Progress,
1474 cancel: &dyn crate::progress::Cancel,
1475) -> Result<FetchOutcome> {
1476 crate::progress::poll_cancel(progress, cancel)?;
1477 ensure_local_remote_repository(remote_git_dir)?;
1478 let local_git_dir = local.git_dir();
1479 validate_fetch_depth_local(local_git_dir, opts)?;
1480
1481 let remote_odb = open_odb(remote_git_dir);
1482 let remote_algo = remote_odb.hash_algo();
1483 let reopened = sync_empty_destination_object_format(local_git_dir, remote_algo, opts)?
1484 .then(|| reopen_after_format_change(local))
1485 .transpose()?;
1486 let local = reopened.as_ref().unwrap_or(local);
1487 let local_git_dir = local.git_dir();
1488 let local_odb = local.odb();
1489
1490 let remote_entries = crate::remote::list_refs_from_git_dir_for_fetch(
1492 remote_git_dir,
1493 Some(&remote_odb),
1494 &opts.refspecs,
1495 )
1496 .map_err(crate::error::Error::from)?;
1497
1498 let mut default_branch = None;
1499 let mut remote_head_oid = None;
1500 let mut remote_refs: Vec<(String, ObjectId)> = Vec::new();
1502 for entry in &remote_entries {
1503 if entry.name == "HEAD" {
1504 remote_head_oid = Some(entry.oid);
1505 default_branch = entry
1506 .symref_target
1507 .as_ref()
1508 .map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned());
1509 continue;
1510 }
1511 if entry.name.ends_with("^{}") {
1512 continue;
1513 }
1514 if !crate::refs::is_valid_fetch_advertised_ref(&entry.name) {
1515 continue;
1516 }
1517 remote_refs.push((entry.name.clone(), entry.oid));
1518 }
1519
1520 let FetchMatchSet {
1521 mut matched,
1522 positive,
1523 negatives,
1524 explicit_refspec_fetch,
1525 } = build_fetch_matched_set(opts, &remote_refs, remote_head_oid)?;
1526 let mut matched_oids: HashSet<ObjectId> = matched.iter().map(|m| m.oid).collect();
1527 let mut seen_remote_ref: HashSet<String> =
1528 matched.iter().map(|m| m.remote_ref.clone()).collect();
1529
1530 let local_shallow = crate::shallow::load_shallow_boundaries(local_git_dir);
1531 let remote_shallow = crate::shallow::load_shallow_boundaries(remote_git_dir);
1532
1533 if opts.tags == TagMode::All {
1536 apply_tag_mode(
1537 opts.tags,
1538 &remote_refs,
1539 &remote_odb,
1540 &remote_shallow,
1541 &negatives,
1542 &HashSet::new(),
1543 &mut matched,
1544 &mut matched_oids,
1545 &mut seen_remote_ref,
1546 )?;
1547 }
1548 let shallow_response = if opts.has_deepen_request() {
1549 let shallow_wants: HashSet<ObjectId> = matched
1550 .iter()
1551 .map(|m| shallow_want_commit_tip(&remote_odb, m))
1552 .collect::<Result<_>>()?;
1553 let remote_repo =
1554 crate::repo::Repository::open(remote_git_dir, &RepositoryOptions::empty())?;
1555 let (wire_depth, deepen_relative) = opts
1556 .wire_deepen()
1557 .map(|(d, r)| (Some(d), r))
1558 .unwrap_or((None, false));
1559 let req = crate::serve::ShallowRequest {
1560 client_shallow: local_shallow.iter().copied().collect(),
1561 depth: wire_depth,
1562 deepen_since: opts
1563 .deepen_since
1564 .as_deref()
1565 .and_then(|s| s.parse::<i64>().ok()),
1566 deepen_not: Vec::new(),
1567 deepen_relative,
1568 };
1569 Some(crate::serve::compute_shallow_response(
1570 &remote_repo,
1571 &shallow_wants.into_iter().collect::<Vec<_>>(),
1572 &req,
1573 )?)
1574 } else {
1575 None
1576 };
1577 let copy_source_shallow = shallow_response
1578 .as_ref()
1579 .map(|resp| resp.pack_shallow_grafts.clone())
1580 .unwrap_or_else(|| remote_shallow.clone());
1581 let mut tag_shallow = copy_source_shallow.clone();
1582 if shallow_response.is_none() {
1583 tag_shallow.extend(local_shallow.iter().copied());
1584 }
1585
1586 let following_only = if opts.tags == TagMode::Following {
1587 let tags_present = crate::fetch::tags_already_local(local.refs(), &remote_refs);
1588 apply_tag_mode(
1589 opts.tags,
1590 &remote_refs,
1591 &remote_odb,
1592 &tag_shallow,
1593 &negatives,
1594 &tags_present,
1595 &mut matched,
1596 &mut matched_oids,
1597 &mut seen_remote_ref,
1598 )?
1599 } else {
1600 HashSet::new()
1601 };
1602
1603 let mut following_tag_wants = if shallow_response.is_some() {
1604 local_odb
1607 .missing_oids(
1608 matched
1609 .iter()
1610 .filter(|m| {
1611 following_only.contains(&m.oid)
1612 && m.advertised_peel.is_some_and(|peel| peel != m.oid)
1613 })
1614 .map(|m| m.oid),
1615 )?
1616 .into_iter()
1617 .collect()
1618 } else {
1619 crate::fetch::following_tag_wants_with_local_peel(&matched, &following_only, local_odb)?
1620 };
1621 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
1622 if shallow_request && shallow_response.is_none() {
1623 let fetch_commit_oids = matched
1624 .iter()
1625 .filter(|m| !m.is_tag)
1626 .map(|m| m.oid)
1627 .collect();
1628 let peel_set = crate::fetch::following_peel_commit_set_for_shallow(
1629 &remote_odb,
1630 &fetch_commit_oids,
1631 opts.absolute_depth_for_tags(),
1632 &tag_shallow,
1633 )?;
1634 following_tag_wants.extend(
1635 crate::fetch::following_tag_wants_peel_among_fetched_commits(
1636 &matched,
1637 &following_only,
1638 &peel_set,
1639 ),
1640 );
1641 }
1642
1643 let mut wants: Vec<ObjectId> = if shallow_request {
1645 matched_oids
1646 .iter()
1647 .copied()
1648 .filter(|oid| !following_only.contains(oid) || following_tag_wants.contains(oid))
1649 .collect()
1650 } else {
1651 local_odb.missing_oids(matched_oids.iter().copied())?
1652 };
1653 let (shallow_new, shallow_unshallow) = if let Some(resp) = shallow_response {
1654 for oid in resp.extra_wants.iter().chain(resp.shallow.iter()).copied() {
1655 if !wants.contains(&oid) {
1656 wants.push(oid);
1657 }
1658 }
1659 (resp.shallow, resp.unshallow)
1660 } else {
1661 (Vec::new(), Vec::new())
1662 };
1663 let mut pack_oids: HashSet<ObjectId> = HashSet::new();
1664 let mut transfer = TransferStats::default();
1665 if !wants.is_empty() && !opts.dry_run {
1666 let mut haves: Vec<ObjectId> = Vec::new();
1668 let mut have_seen: HashSet<ObjectId> = HashSet::new();
1669 for m in &matched {
1670 if let Some(local_ref) = &m.local_ref {
1671 if let Ok(old) = local.resolve_ref_oid(local_ref) {
1672 if have_seen.insert(old) {
1673 haves.push(old);
1674 }
1675 }
1676 }
1677 }
1678
1679 let remote_cfg = config_for_git_dir(remote_git_dir);
1680 let pack_opts = PackBuildOptions::for_local_copy(remote_cfg.as_ref());
1681 let (oids, fetch_transfer) = copy_objects_for_local_fetch(
1682 local_git_dir,
1683 &remote_odb,
1684 local_odb,
1685 &wants,
1686 &haves,
1687 ©_source_shallow,
1688 &local_shallow,
1689 !opts.has_deepen_request(),
1690 &pack_opts,
1691 opts,
1692 progress,
1693 cancel,
1694 )?;
1695 pack_oids = oids;
1696 transfer = fetch_transfer;
1697 }
1698
1699 if opts.initial_remote_fetch
1700 && !remote_shallow.is_empty()
1701 && !opts.has_deepen_request()
1702 && !opts.dry_run
1703 {
1704 let remote_shallow_vec: Vec<ObjectId> = remote_shallow.iter().copied().collect();
1705 crate::shallow::apply_shallow_updates(local_git_dir, &remote_shallow_vec, &[])?;
1706 }
1707 if (!shallow_new.is_empty() || !shallow_unshallow.is_empty()) && !opts.dry_run {
1708 crate::shallow::apply_shallow_updates(local_git_dir, &shallow_new, &shallow_unshallow)?;
1709 }
1710
1711 if opts.tags == TagMode::Following {
1712 let boundaries = crate::shallow::load_shallow_boundaries(local_git_dir);
1713 crate::fetch::retain_following_tags(local_odb, &mut matched, &pack_oids, &boundaries)?;
1714 }
1715
1716 let dry_run_remote = if opts.dry_run {
1719 crate::repo::Repository::open(remote_git_dir, &RepositoryOptions::empty()).ok()
1720 } else {
1721 None
1722 };
1723 let classify_repo = if opts.dry_run {
1724 dry_run_remote.as_ref()
1725 } else {
1726 Some(local)
1727 };
1728
1729 let mut updates: Vec<RefUpdate> = Vec::new();
1730
1731 let mut prune_batch: Vec<crate::refs::store::RefUpdate> = Vec::new();
1736 if opts.prune {
1737 prune_tracking_refs(
1738 local_git_dir,
1739 &positive,
1740 &remote_refs,
1741 opts.dry_run,
1742 &mut updates,
1743 if opts.dry_run {
1744 None
1745 } else {
1746 Some(&mut prune_batch)
1747 },
1748 )?;
1749 }
1750 if !opts.dry_run && !prune_batch.is_empty() {
1751 crate::refs::commit_ref_store_batch(local_git_dir, &prune_batch)?;
1752 }
1753
1754 let mut store_batch: Vec<crate::refs::store::RefUpdate> = Vec::new();
1755 for m in &matched {
1756 let Some(local_ref) = &m.local_ref else {
1757 updates.push(RefUpdate {
1759 remote_ref: m.remote_ref.clone(),
1760 local_ref: None,
1761 old_oid: None,
1762 new_oid: Some(m.oid),
1763 mode: UpdateMode::NoChangeNeeded,
1764 note: Some("not stored (empty destination)".to_owned()),
1765 });
1766 continue;
1767 };
1768
1769 let old = local.resolve_ref_oid(local_ref).ok();
1770 let mode = classify_update(
1771 old.as_ref(),
1772 &m.oid,
1773 m.force,
1774 m.dest_is_tag,
1775 classify_repo,
1776 None,
1777 );
1778
1779 let write = matches!(
1780 mode,
1781 UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
1782 );
1783 enforce_fetch_ref_update(
1784 local_ref,
1785 old,
1786 m.oid,
1787 mode,
1788 opts,
1789 explicit_refspec_fetch,
1790 classify_repo,
1791 )?;
1792 if write && !opts.dry_run {
1793 if !crate::refs::is_valid_storable_ref_name(local_ref) {
1794 updates.push(RefUpdate {
1795 remote_ref: m.remote_ref.clone(),
1796 local_ref: Some(local_ref.clone()),
1797 old_oid: old,
1798 new_oid: Some(m.oid),
1799 mode,
1800 note: Some("skipped (invalid ref name)".to_owned()),
1801 });
1802 continue;
1803 }
1804 if m.dest_is_tag && !ref_target_exists(local_odb, &remote_odb, m.oid)? {
1805 updates.push(RefUpdate {
1806 remote_ref: m.remote_ref.clone(),
1807 local_ref: Some(local_ref.clone()),
1808 old_oid: old,
1809 new_oid: Some(m.oid),
1810 mode: UpdateMode::NoChangeNeeded,
1811 note: Some("skipped (tag target not present locally)".to_owned()),
1812 });
1813 continue;
1814 }
1815 store_batch.push(crate::refs::store::RefUpdate {
1816 name: local_ref.clone(),
1817 new_value: Some(crate::refs::store::RawRef::Direct(m.oid)),
1818 expected: crate::refs::store::Expected::Any,
1819 reflog: None,
1820 flags: Default::default(),
1821 });
1822 }
1823
1824 updates.push(RefUpdate {
1825 remote_ref: m.remote_ref.clone(),
1826 local_ref: Some(local_ref.clone()),
1827 old_oid: old,
1828 new_oid: Some(m.oid),
1829 mode,
1830 note: None,
1831 });
1832 }
1833
1834 if let Some(remote) = opts.remote_name.as_deref() {
1835 if !opts.dry_run {
1836 if let Some(branch) = default_branch.as_deref() {
1837 let head_ref = format!("refs/remotes/{remote}/HEAD");
1838 if crate::refs::resolve_ref(local_git_dir, &head_ref).is_err() {
1839 let target = format!("refs/remotes/{remote}/{branch}");
1840 let target_will_exist = store_batch.iter().any(|u| u.name == target)
1841 || crate::refs::resolve_ref(local_git_dir, &target).is_ok();
1842 if target_will_exist {
1843 store_batch.push(crate::refs::store::RefUpdate {
1844 name: head_ref,
1845 new_value: Some(crate::refs::store::RawRef::Symbolic(target)),
1846 expected: crate::refs::store::Expected::Any,
1847 reflog: None,
1848 flags: Default::default(),
1849 });
1850 }
1851 }
1852 }
1853 }
1854 }
1855 if !opts.dry_run && !store_batch.is_empty() {
1856 apply_fetch_ref_updates(
1857 local_git_dir,
1858 &store_batch,
1859 &mut updates,
1860 FetchRefRejectHint {
1861 prune: opts.prune,
1862 positive: &positive,
1863 remote_refs: &remote_refs,
1864 remote_name: opts.remote_name.as_deref(),
1865 },
1866 )?;
1867 }
1868
1869 crate::fetch::finish_initial_remote_fetch_layout(
1870 local_git_dir,
1871 opts,
1872 default_branch.as_deref(),
1873 )?;
1874 if let Some(remote) = opts.remote_name.as_deref() {
1875 crate::fetch::ensure_remote_tracking_head_symref(
1876 local_git_dir,
1877 remote,
1878 default_branch.as_deref(),
1879 opts.dry_run,
1880 )?;
1881 }
1882 finish_anonymous_url_fetch(
1883 local_git_dir,
1884 opts,
1885 explicit_refspec_fetch,
1886 default_branch.as_deref(),
1887 &remote_refs,
1888 &matched,
1889 )?;
1890 let new_shallow = crate::shallow::dedupe_shallow_list(if opts.has_deepen_request() {
1891 shallow_new
1892 } else if opts.initial_remote_fetch {
1893 remote_shallow.iter().copied().collect()
1894 } else {
1895 Vec::new()
1896 });
1897 Ok(FetchOutcome {
1898 updates,
1899 default_branch,
1900 new_shallow,
1901 new_unshallow: shallow_unshallow,
1902 transfer,
1903 })
1904}
1905
1906pub fn push_local(
1947 local_git_dir: &Path,
1948 remote_git_dir: &Path,
1949 refs: &[PushRefSpec],
1950 opts: &PushOptions,
1951 cancel: &dyn crate::progress::Cancel,
1952) -> Result<PushOutcome> {
1953 let local = crate::repo::Repository::open(local_git_dir, &RepositoryOptions::empty())?;
1954 push_local_from(&local, remote_git_dir, refs, opts, cancel)
1955}
1956
1957pub(crate) fn push_local_from(
1959 local: &crate::repo::Repository,
1960 remote_git_dir: &Path,
1961 refs: &[PushRefSpec],
1962 opts: &PushOptions,
1963 cancel: &dyn crate::progress::Cancel,
1964) -> Result<PushOutcome> {
1965 let local_git_dir = local.git_dir();
1966 let mut progress_sink = crate::progress::NullProgress;
1967 crate::progress::poll_cancel(&mut progress_sink, cancel)?;
1968 if !opts.push_options.is_empty() {
1969 return Err(Error::PushOptionsUnsupported);
1970 }
1971 ensure_local_remote_repository(remote_git_dir)?;
1972
1973 let local_odb = local.odb();
1974 let remote_odb = open_odb(remote_git_dir);
1975
1976 let remote_have_tips: Vec<ObjectId> = match &opts.remote_ref_tips {
1979 Some(tips) => tips.clone(),
1980 None => crate::refs::list_refs(remote_git_dir, "refs/")?
1981 .into_iter()
1982 .map(|(_, oid)| oid)
1983 .collect(),
1984 };
1985
1986 let mut decisions: Vec<PushDecision> = Vec::with_capacity(refs.len());
1988 for spec in refs {
1989 decisions.push(decide_push(spec, local_odb, remote_git_dir, Some(local))?);
1990 }
1991
1992 let any_rejected = decisions.iter().any(|d| d.result.status.is_error());
1995 if opts.atomic && any_rejected {
1996 for d in &mut decisions {
1997 if matches!(d.result.status, PushRefStatus::Ok) {
1998 d.result.status = PushRefStatus::AtomicPushFailed;
1999 d.apply = false;
2000 }
2001 }
2002 return Ok(PushOutcome {
2003 results: decisions.into_iter().map(|d| d.result).collect(),
2004 remote_messages: Vec::new(),
2005 });
2006 }
2007
2008 let push_wants: Vec<ObjectId> = decisions
2009 .iter()
2010 .filter(|d| d.apply && !opts.dry_run)
2011 .filter_map(|d| match &d.action {
2012 PushAction::Update(src) => Some(*src),
2013 _ => None,
2014 })
2015 .collect();
2016 if !push_wants.is_empty() && !opts.dry_run {
2017 crate::progress::poll_cancel(&mut progress_sink, cancel)?;
2018 let local_cfg = config_for_git_dir(local_git_dir);
2019 let pack_opts = if remote_have_tips.is_empty() {
2020 PackBuildOptions::for_local_copy(local_cfg.as_ref())
2021 } else {
2022 PackBuildOptions::for_local_push(local_cfg.as_ref())
2023 };
2024 let empty_shallow = HashSet::new();
2025 let remote_shallow = crate::shallow::load_shallow_boundaries(remote_git_dir);
2026 copy_objects_for_local_push(
2027 local_odb,
2028 &remote_odb,
2029 &push_wants,
2030 &remote_have_tips,
2031 &empty_shallow,
2032 &remote_shallow,
2033 &pack_opts,
2034 )?;
2035 }
2036
2037 let mut ref_batch: Vec<crate::refs::store::RefUpdate> = Vec::new();
2039 for d in &mut decisions {
2040 if !d.apply || opts.dry_run {
2041 continue;
2042 }
2043 match &d.action {
2044 PushAction::Update(src) => {
2045 let update = push_store_ref_update(
2046 d.result.remote_ref.as_str(),
2047 Some(*src),
2048 d.result.old_oid,
2049 );
2050 if opts.atomic {
2051 ref_batch.push(update);
2052 } else if let Err(err) = crate::refs::commit_store_update(remote_git_dir, update) {
2053 apply_ref_store_error_to_push_decision(d, &err, false);
2054 }
2055 }
2056 PushAction::Delete => {
2057 let update =
2058 push_store_ref_update(d.result.remote_ref.as_str(), None, d.result.old_oid);
2059 if opts.atomic {
2060 ref_batch.push(update);
2061 } else if let Err(err) = crate::refs::commit_store_update(remote_git_dir, update) {
2062 apply_ref_store_error_to_push_decision(d, &err, false);
2063 }
2064 }
2065 PushAction::None => {}
2066 }
2067 }
2068 if opts.atomic && !opts.dry_run && !ref_batch.is_empty() {
2069 if let Err(err) = crate::refs::commit_ref_store_batch(remote_git_dir, &ref_batch) {
2070 apply_atomic_batch_ref_store_error(&mut decisions, &ref_batch, err);
2071 }
2072 }
2073
2074 let results: Vec<_> = decisions.into_iter().map(|d| d.result).collect();
2075 if !opts.dry_run {
2076 if let Some(remote) = opts.tracking_remote.as_deref() {
2077 crate::branch_tracking::apply_push_remote_tracking_updates(
2078 local_git_dir,
2079 remote,
2080 &results,
2081 )?;
2082 }
2083 }
2084
2085 Ok(PushOutcome {
2086 results,
2087 remote_messages: Vec::new(),
2088 })
2089}
2090
2091fn push_store_ref_update(
2092 refname: &str,
2093 new_oid: Option<ObjectId>,
2094 expected_old: Option<ObjectId>,
2095) -> crate::refs::store::RefUpdate {
2096 crate::refs::store::RefUpdate {
2097 name: refname.to_owned(),
2098 new_value: new_oid.map(crate::refs::store::RawRef::Direct),
2099 expected: match expected_old {
2100 Some(oid) => crate::refs::store::Expected::Oid(oid),
2101 None => crate::refs::store::Expected::Any,
2102 },
2103 reflog: None,
2104 flags: Default::default(),
2105 }
2106}
2107
2108fn push_status_for_ref_store_error(err: &Error) -> PushRefStatus {
2109 match err {
2110 Error::RefStore(crate::refs::store::RefStoreError::ExpectedMismatch { .. }) => {
2111 PushRefStatus::RejectStale
2112 }
2113 _ => PushRefStatus::RemoteRejected,
2114 }
2115}
2116
2117fn ref_store_error_refname(err: &Error) -> Option<String> {
2118 let Error::RefStore(store_err) = err else {
2119 return None;
2120 };
2121 Some(match store_err {
2122 crate::refs::store::RefStoreError::ExpectedMismatch { name, .. }
2123 | crate::refs::store::RefStoreError::DuplicateUpdate { name }
2124 | crate::refs::store::RefStoreError::LockHeld { name }
2125 | crate::refs::store::RefStoreError::InvalidRefName { name }
2126 | crate::refs::store::RefStoreError::NullOid { name }
2127 | crate::refs::store::RefStoreError::NotFound { name } => name.clone(),
2128 crate::refs::store::RefStoreError::NameUnavailable { reason } => match reason {
2129 crate::refs::RefnameUnavailable::SameBatch { refname, .. }
2130 | crate::refs::RefnameUnavailable::AncestorExists {
2131 new_ref: refname, ..
2132 }
2133 | crate::refs::RefnameUnavailable::DescendantExists {
2134 new_ref: refname, ..
2135 } => refname.clone(),
2136 },
2137 crate::refs::store::RefStoreError::SymrefLoop
2138 | crate::refs::store::RefStoreError::Corrupt(_)
2139 | crate::refs::store::RefStoreError::GitDirPathIo { .. }
2140 | crate::refs::store::RefStoreError::InvalidReflog(_)
2141 | crate::refs::store::RefStoreError::Backend(_) => return None,
2142 })
2143}
2144
2145fn apply_ref_store_error_to_push_decision(d: &mut PushDecision, err: &Error, _atomic: bool) {
2146 d.result.status = push_status_for_ref_store_error(err);
2147 d.result.message = Some(err.to_string());
2148 d.apply = false;
2149}
2150
2151fn apply_atomic_batch_ref_store_error(
2152 decisions: &mut [PushDecision],
2153 batch: &[crate::refs::store::RefUpdate],
2154 err: Error,
2155) {
2156 let fail_ref = ref_store_error_refname(&err);
2157 let message = err.to_string();
2158 let batch_names: HashSet<&str> = batch.iter().map(|u| u.name.as_str()).collect();
2159 let in_batch = decisions
2160 .iter_mut()
2161 .filter(|d| d.apply && batch_names.contains(d.result.remote_ref.as_str()));
2162 if let Some(fail_name) = fail_ref {
2163 for d in in_batch {
2164 if d.result.remote_ref == fail_name {
2165 d.result.status = push_status_for_ref_store_error(&err);
2166 d.result.message = Some(message.clone());
2167 } else {
2168 d.result.status = PushRefStatus::AtomicPushFailed;
2169 d.result.message = Some("atomic push failed".to_owned());
2170 }
2171 d.apply = false;
2172 }
2173 } else {
2174 let mut first = true;
2175 for d in in_batch {
2176 if first {
2177 d.result.status = push_status_for_ref_store_error(&err);
2178 d.result.message = Some(message.clone());
2179 first = false;
2180 } else {
2181 d.result.status = PushRefStatus::AtomicPushFailed;
2182 d.result.message = Some("atomic push failed".to_owned());
2183 }
2184 d.apply = false;
2185 }
2186 }
2187}
2188
2189enum PushAction {
2191 Update(ObjectId),
2193 Delete,
2195 None,
2197}
2198
2199struct PushDecision {
2201 result: PushRefResult,
2202 action: PushAction,
2203 apply: bool,
2205}
2206
2207fn decide_push(
2209 spec: &PushRefSpec,
2210 local_odb: &Odb,
2211 remote_git_dir: &Path,
2212 local_repo: Option<&crate::repo::Repository>,
2213) -> Result<PushDecision> {
2214 let remote_current = crate::refs::resolve_ref(remote_git_dir, &spec.dst).ok();
2215
2216 if !spec.delete {
2222 if let Some(src) = spec.src {
2223 if remote_current == Some(src) {
2224 return Ok(PushDecision {
2225 result: PushRefResult {
2226 local_ref: push_spec_local_ref(spec),
2227 remote_ref: spec.dst.clone(),
2228 old_oid: remote_current,
2229 new_oid: Some(src),
2230 forced: false,
2231 deletion: false,
2232 status: PushRefStatus::UpToDate,
2233 message: None,
2234 },
2235 action: PushAction::None,
2236 apply: false,
2237 });
2238 }
2239 }
2240 }
2241
2242 if spec.expect_absent && remote_current.is_some() {
2246 return Ok(PushDecision {
2247 result: PushRefResult {
2248 local_ref: push_spec_local_ref(spec),
2249 remote_ref: spec.dst.clone(),
2250 old_oid: remote_current,
2251 new_oid: spec.src,
2252 forced: false,
2253 deletion: spec.delete,
2254 status: PushRefStatus::RejectStale,
2255 message: Some("stale info".to_owned()),
2256 },
2257 action: PushAction::None,
2258 apply: false,
2259 });
2260 }
2261
2262 if let Some(expected) = spec.expected_old {
2266 if remote_current != Some(expected) {
2267 return Ok(PushDecision {
2268 result: PushRefResult {
2269 local_ref: None,
2270 remote_ref: spec.dst.clone(),
2271 old_oid: remote_current,
2272 new_oid: spec.src,
2273 forced: false,
2274 deletion: spec.delete,
2275 status: PushRefStatus::RejectStale,
2276 message: Some("stale info".to_owned()),
2277 },
2278 action: PushAction::None,
2279 apply: false,
2280 });
2281 }
2282 }
2283
2284 if spec.delete {
2285 let (status, action, apply, message) = match remote_current {
2286 Some(_) => (PushRefStatus::Ok, PushAction::Delete, true, None),
2287 None => (
2288 PushRefStatus::RejectRemoteRefMissing,
2289 PushAction::None,
2290 false,
2291 Some("remote ref does not exist".to_owned()),
2292 ),
2293 };
2294 return Ok(PushDecision {
2295 result: PushRefResult {
2296 local_ref: push_spec_local_ref(spec),
2297 remote_ref: spec.dst.clone(),
2298 old_oid: remote_current,
2299 new_oid: None,
2300 forced: false,
2301 deletion: true,
2302 status,
2303 message,
2304 },
2305 action,
2306 apply,
2307 });
2308 }
2309
2310 let Some(src) = spec.src else {
2312 return Err(Error::Message(format!(
2313 "push to '{}' has no source object and is not a deletion",
2314 spec.dst
2315 )));
2316 };
2317 if !local_odb.contains(&src)? {
2318 return Err(Error::Message(format!(
2319 "source object {src} for '{}' is missing from the local object store",
2320 spec.dst
2321 )));
2322 }
2323
2324 if remote_current == Some(src) {
2326 return Ok(PushDecision {
2327 result: PushRefResult {
2328 local_ref: push_spec_local_ref(spec),
2329 remote_ref: spec.dst.clone(),
2330 old_oid: remote_current,
2331 new_oid: Some(src),
2332 forced: false,
2333 deletion: false,
2334 status: PushRefStatus::UpToDate,
2335 message: None,
2336 },
2337 action: PushAction::None,
2338 apply: false,
2339 });
2340 }
2341
2342 let Some(old) = remote_current else {
2344 return Ok(PushDecision {
2345 result: PushRefResult {
2346 local_ref: push_spec_local_ref(spec),
2347 remote_ref: spec.dst.clone(),
2348 old_oid: None,
2349 new_oid: Some(src),
2350 forced: false,
2351 deletion: false,
2352 status: PushRefStatus::Ok,
2353 message: None,
2354 },
2355 action: PushAction::Update(src),
2356 apply: true,
2357 });
2358 };
2359
2360 let is_ff = local_repo
2364 .map(|r| crate::merge_base::is_ancestor(r, old, src).unwrap_or(false))
2365 .unwrap_or(false);
2366
2367 if is_ff {
2368 Ok(PushDecision {
2369 result: PushRefResult {
2370 local_ref: push_spec_local_ref(spec),
2371 remote_ref: spec.dst.clone(),
2372 old_oid: Some(old),
2373 new_oid: Some(src),
2374 forced: false,
2375 deletion: false,
2376 status: PushRefStatus::Ok,
2377 message: None,
2378 },
2379 action: PushAction::Update(src),
2380 apply: true,
2381 })
2382 } else if spec.force {
2383 Ok(PushDecision {
2384 result: PushRefResult {
2385 local_ref: push_spec_local_ref(spec),
2386 remote_ref: spec.dst.clone(),
2387 old_oid: Some(old),
2388 new_oid: Some(src),
2389 forced: true,
2390 deletion: false,
2391 status: PushRefStatus::Ok,
2392 message: None,
2393 },
2394 action: PushAction::Update(src),
2395 apply: true,
2396 })
2397 } else {
2398 Ok(PushDecision {
2399 result: PushRefResult {
2400 local_ref: push_spec_local_ref(spec),
2401 remote_ref: spec.dst.clone(),
2402 old_oid: Some(old),
2403 new_oid: Some(src),
2404 forced: false,
2405 deletion: false,
2406 status: PushRefStatus::RejectNonFastForward,
2407 message: Some("non-fast-forward".to_owned()),
2408 },
2409 action: PushAction::None,
2410 apply: false,
2411 })
2412 }
2413}
2414
2415pub(crate) struct MatchedRef {
2417 pub(crate) remote_ref: String,
2418 pub(crate) local_ref: Option<String>,
2420 pub(crate) oid: ObjectId,
2421 pub(crate) force: bool,
2422 pub(crate) is_tag: bool,
2423 pub(crate) dest_is_tag: bool,
2425 pub(crate) advertised_peel: Option<ObjectId>,
2427 pub(crate) fetch_head: crate::fetch_request::FetchHeadPolicy,
2429}
2430
2431fn objects_dir(git_dir: &Path) -> PathBuf {
2436 crate::repo::common_git_dir_for_config(git_dir).join("objects")
2437}
2438
2439pub(crate) fn open_odb(git_dir: &Path) -> Odb {
2440 Odb::new(&objects_dir(git_dir)).with_config_git_dir(git_dir.to_path_buf())
2441}
2442
2443pub(crate) fn remote_hash_algo_from_advertisement(
2445 capability_blocks: &[&[String]],
2446 remote_refs: &[(String, ObjectId)],
2447) -> HashAlgo {
2448 for caps in capability_blocks {
2449 for c in *caps {
2450 if let Some(name) = c.strip_prefix("object-format=") {
2451 if let Some(algo) = HashAlgo::from_name(name) {
2452 return algo;
2453 }
2454 }
2455 }
2456 }
2457 remote_refs
2458 .iter()
2459 .find_map(|(_, oid)| HashAlgo::from_len(oid.as_bytes().len()))
2460 .unwrap_or(HashAlgo::Sha1)
2461}
2462
2463pub(crate) fn sync_empty_destination_object_format(
2467 local_git_dir: &Path,
2468 remote_algo: HashAlgo,
2469 opts: &FetchOptions,
2470) -> Result<bool> {
2471 if opts.dry_run || !opts.initial_remote_fetch {
2472 return Ok(false);
2473 }
2474 let local_odb = open_odb(local_git_dir);
2475 if local_odb.hash_algo() == remote_algo {
2476 return Ok(false);
2477 }
2478 if local_odb.approximate_object_count()? > 0 {
2479 return Err(Error::Message(format!(
2480 "unsupported object format: destination uses {} but remote uses {}; re-clone into an empty directory with a matching format",
2481 local_odb.hash_algo().name(),
2482 remote_algo.name()
2483 )));
2484 }
2485 write_empty_destination_object_format(local_git_dir, remote_algo)?;
2486 Ok(true)
2487}
2488
2489pub(crate) fn reopen_after_format_change(
2492 local: &crate::repo::Repository,
2493) -> Result<crate::repo::Repository> {
2494 crate::repo::Repository::open(
2495 local.git_dir(),
2496 &RepositoryOptions::with_environment(local.environment().clone()),
2497 )
2498}
2499
2500fn write_empty_destination_object_format(git_dir: &Path, algo: HashAlgo) -> Result<()> {
2501 use crate::config::{ConfigFile, ConfigScope};
2502 let config_path = crate::repo::local_config_write_path(git_dir);
2503 let content = fs::read_to_string(&config_path).unwrap_or_default();
2504 let mut file = ConfigFile::parse(&config_path, &content, ConfigScope::Local)?;
2505 if algo == HashAlgo::Sha1 {
2506 file.unset("extensions.objectformat")?;
2507 file.set("core.repositoryformatversion", "0")?;
2508 } else {
2509 file.set("core.repositoryformatversion", "1")?;
2510 file.set("extensions.objectformat", algo.name())?;
2511 }
2512 file.write()?;
2513 Ok(())
2514}
2515
2516fn config_for_git_dir(git_dir: &Path) -> Option<crate::config::ConfigSet> {
2517 crate::config::ConfigSet::load(
2518 &crate::environment::Environment::empty(),
2519 Some(git_dir),
2520 true,
2521 )
2522 .ok()
2523}
2524
2525pub(crate) fn finish_anonymous_url_fetch(
2527 git_dir: &Path,
2528 opts: &FetchOptions,
2529 explicit_refspec_fetch: bool,
2530 default_branch: Option<&str>,
2531 remote_refs: &[(String, ObjectId)],
2532 matched: &[MatchedRef],
2533) -> Result<()> {
2534 if opts.dry_run {
2535 return Ok(());
2536 }
2537 let write_fetch_head = opts.remote_name.is_none() || explicit_refspec_fetch;
2538 if !write_fetch_head {
2539 return Ok(());
2540 }
2541 let source = opts.fetch_source.clone().or_else(|| {
2542 let name = opts.remote_name.as_deref()?;
2543 let config = config_for_git_dir(git_dir)?;
2544 config
2545 .get(&format!("remote.{name}.url"))
2546 .filter(|url| !url.trim().is_empty())
2547 });
2548 let Some(source) = source.as_deref() else {
2549 return Ok(());
2550 };
2551 let lines = fetch_head_lines_for_ad_hoc_fetch(
2552 matched,
2553 remote_refs,
2554 &opts.refspecs,
2555 default_branch,
2556 source,
2557 );
2558 if lines.is_empty() {
2559 return Ok(());
2560 }
2561 crate::fetch_head::write_fetch_head(git_dir, &lines)
2562}
2563
2564fn fetch_head_lines_for_ad_hoc_fetch(
2565 matched: &[MatchedRef],
2566 remote_refs: &[(String, ObjectId)],
2567 refspecs: &[String],
2568 default_branch: Option<&str>,
2569 source: &str,
2570) -> Vec<crate::fetch_head::FetchHeadLine> {
2571 use crate::fetch_head::FetchHeadLine;
2572 use crate::remote::Remote;
2573
2574 let anonymous_only = refspecs.len() == 1
2575 && refspecs[0].trim_start_matches('+') == Remote::ANONYMOUS_FETCH_REFSPEC;
2576
2577 if anonymous_only {
2578 if let Some(branch) = default_branch {
2579 let head_ref = format!("refs/heads/{branch}");
2580 if let Some(m) = matched
2581 .iter()
2582 .find(|m| m.remote_ref == head_ref && !m.is_tag)
2583 {
2584 return vec![FetchHeadLine {
2585 oid: m.oid,
2586 for_merge: true,
2587 description: source.to_owned(),
2588 }];
2589 }
2590 }
2591 if let Some((_, oid)) = remote_refs.iter().find(|(name, _)| name == "HEAD") {
2592 return vec![FetchHeadLine {
2593 oid: *oid,
2594 for_merge: true,
2595 description: source.to_owned(),
2596 }];
2597 }
2598 return Vec::new();
2599 }
2600
2601 matched
2602 .iter()
2603 .filter(|m| m.fetch_head == crate::fetch_request::FetchHeadPolicy::ForMerge)
2604 .map(|m| FetchHeadLine {
2605 oid: m.oid,
2606 for_merge: true,
2607 description: crate::fetch_request::fetch_head_description(
2608 &m.remote_ref,
2609 m.is_tag,
2610 source,
2611 ),
2612 })
2613 .collect()
2614}
2615
2616#[must_use]
2618pub(crate) fn remote_head_oid_from_advertised(
2619 advertised: &[(String, ObjectId)],
2620) -> Option<ObjectId> {
2621 advertised
2622 .iter()
2623 .find(|(name, _)| name == "HEAD")
2624 .map(|(_, oid)| *oid)
2625}
2626
2627pub(crate) fn match_positive(refname: &str, positive: &[RefspecItem]) -> Option<Option<String>> {
2632 for item in positive {
2633 let Some(src) = item.src.as_deref() else {
2634 continue;
2635 };
2636 if let Some(dst) = apply_refspec(src, item.dst.as_deref(), refname) {
2637 if dst.is_empty() {
2639 return Some(None);
2640 }
2641 return Some(Some(dst));
2642 }
2643 }
2644 None
2645}
2646
2647pub(crate) fn refspecs_force(refname: &str, positive: &[RefspecItem]) -> bool {
2649 positive.iter().any(|item| {
2650 item.force
2651 && item
2652 .src
2653 .as_deref()
2654 .is_some_and(|src| apply_refspec(src, item.dst.as_deref(), refname).is_some())
2655 })
2656}
2657
2658pub(crate) fn ref_excluded(refname: &str, negatives: &[RefspecItem]) -> bool {
2660 negatives.iter().any(|item| {
2661 item.src
2662 .as_deref()
2663 .is_some_and(|src| glob_matches(src, refname))
2664 })
2665}
2666
2667pub(crate) fn apply_refspec(src: &str, dst: Option<&str>, refname: &str) -> Option<String> {
2673 match src.find('*') {
2674 Some(star) => {
2675 let prefix = &src[..star];
2676 let suffix = &src[star + 1..];
2677 if !refname.starts_with(prefix)
2678 || !refname.ends_with(suffix)
2679 || refname.len() < prefix.len() + suffix.len()
2680 {
2681 return None;
2682 }
2683 let middle = &refname[prefix.len()..refname.len() - suffix.len()];
2684 match dst {
2685 None => Some(refname.to_owned()),
2686 Some("") => Some(String::new()),
2687 Some(d) => Some(d.replacen('*', middle, 1)),
2688 }
2689 }
2690 None => {
2691 if src != refname {
2692 return None;
2693 }
2694 match dst {
2695 None => Some(refname.to_owned()),
2696 Some("") => Some(String::new()),
2697 Some(d) => Some(d.to_owned()),
2698 }
2699 }
2700 }
2701}
2702
2703fn glob_matches(pattern: &str, refname: &str) -> bool {
2705 match pattern.find('*') {
2706 Some(star) => {
2707 let prefix = &pattern[..star];
2708 let suffix = &pattern[star + 1..];
2709 refname.starts_with(prefix)
2710 && refname.ends_with(suffix)
2711 && refname.len() >= prefix.len() + suffix.len()
2712 }
2713 None => pattern == refname,
2714 }
2715}
2716
2717#[allow(clippy::too_many_arguments)]
2723pub(crate) fn apply_tag_mode(
2724 mode: TagMode,
2725 remote_refs: &[(String, ObjectId)],
2726 remote_odb: &Odb,
2727 shallow_boundaries: &HashSet<ObjectId>,
2728 negatives: &[RefspecItem],
2729 already_local: &HashSet<String>,
2730 matched: &mut Vec<MatchedRef>,
2731 matched_oids: &mut HashSet<ObjectId>,
2732 seen_remote_ref: &mut HashSet<String>,
2733) -> Result<HashSet<ObjectId>> {
2734 let mut following_only = HashSet::new();
2735 if mode == TagMode::None {
2736 return Ok(following_only);
2737 }
2738
2739 let candidates: Vec<(&String, ObjectId, Option<ObjectId>)> = remote_refs
2742 .iter()
2743 .filter(|(name, _)| {
2744 name.starts_with("refs/tags/")
2745 && !seen_remote_ref.contains(name)
2746 && !ref_excluded(name, negatives)
2747 && !(mode == TagMode::Following && already_local.contains(name))
2748 })
2749 .map(|(name, oid)| {
2750 let peeled = match mode {
2751 TagMode::Following => remote_odb.peel_tags(*oid).ok().map(|(p, _)| p),
2752 _ => None,
2753 };
2754 (name, *oid, peeled)
2755 })
2756 .collect();
2757
2758 let following_closure: HashSet<ObjectId> = if mode == TagMode::Following {
2763 let roots: Vec<ObjectId> = matched
2764 .iter()
2765 .filter(|m| !m.is_tag)
2766 .map(|m| m.oid)
2767 .collect();
2768 let targets: HashSet<ObjectId> = candidates
2769 .iter()
2770 .map(|(_, oid, peeled)| peeled.unwrap_or(*oid))
2771 .collect();
2772 crate::fetch::reachable_commits(remote_odb, &roots, &targets, shallow_boundaries)?
2773 } else {
2774 HashSet::new()
2775 };
2776
2777 for (name, oid, peeled) in candidates {
2778 let keep = match mode {
2779 TagMode::All => true,
2780 TagMode::Following => {
2782 following_closure.contains(&oid)
2783 || peeled.is_some_and(|p| following_closure.contains(&p))
2784 }
2785 TagMode::None => false,
2786 };
2787 if keep && seen_remote_ref.insert(name.clone()) {
2788 if matched_oids.insert(oid) && mode == TagMode::Following {
2789 following_only.insert(oid);
2790 }
2791 matched.push(MatchedRef {
2792 remote_ref: name.clone(),
2793 local_ref: Some(name.clone()),
2794 oid,
2795 force: false,
2796 is_tag: true,
2797 dest_is_tag: true,
2798 advertised_peel: peeled,
2799 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
2800 });
2801 }
2802 }
2803 Ok(following_only)
2804}
2805
2806fn shallow_want_commit_tip(remote_odb: &Odb, matched: &MatchedRef) -> Result<ObjectId> {
2808 if !matched.is_tag {
2809 return Ok(matched.oid);
2810 }
2811 if let Some(peel) = matched.advertised_peel.filter(|p| !p.is_zero()) {
2812 return Ok(peel);
2813 }
2814 if remote_odb.contains(&matched.oid)? {
2815 let (peeled, _) = remote_odb.peel_tags(matched.oid)?;
2816 return Ok(peeled);
2817 }
2818 Ok(matched.oid)
2819}
2820
2821fn ref_target_exists(local_odb: &Odb, remote_odb: &Odb, tag_oid: ObjectId) -> Result<bool> {
2824 let peel_odb = if local_odb.contains(&tag_oid)? {
2825 local_odb
2826 } else {
2827 remote_odb
2828 };
2829 match peel_odb.peel_tags(tag_oid) {
2830 Ok((peeled, _)) => local_odb.contains(&peeled),
2831 Err(_) => Ok(false),
2832 }
2833}
2834
2835pub(crate) fn classify_update(
2841 old: Option<&ObjectId>,
2842 new: &ObjectId,
2843 force: bool,
2844 is_tag: bool,
2845 repo: Option<&crate::repo::Repository>,
2846 classify_odb: Option<&crate::odb::Odb>,
2847) -> UpdateMode {
2848 let Some(old) = old else {
2849 return UpdateMode::New;
2850 };
2851 if old == new {
2852 return UpdateMode::UpToDate;
2853 }
2854 let ff = if let Some(r) = repo {
2856 crate::merge_base::is_ancestor(r, *old, *new).unwrap_or(false)
2857 } else if let Some(odb) = classify_odb {
2858 crate::merge_base::is_ancestor_odb_walk(odb, *old, *new).unwrap_or(false)
2859 } else {
2860 false
2861 };
2862 if ff && !is_tag {
2863 return UpdateMode::FastForward;
2864 }
2865 if force {
2866 return UpdateMode::Forced;
2867 }
2868 if is_tag {
2869 return UpdateMode::TagUpdateRejected;
2870 }
2871 UpdateMode::NonFastForwardRejected
2872}
2873
2874fn is_default_head_under_tracking_prefix(refname: &str, tracking_prefix: &str) -> bool {
2878 refname == format!("{tracking_prefix}HEAD")
2879}
2880
2881pub fn tracking_refs_gone_on_remote(
2890 local_git_dir: &Path,
2891 positive: &[RefspecItem],
2892 remote_refs: &[(String, ObjectId)],
2893) -> Result<Vec<(String, ObjectId)>> {
2894 let mut live: HashSet<String> = HashSet::new();
2895 for (name, _) in remote_refs {
2896 if let Some(Some(dst)) = match_positive(name, positive) {
2897 live.insert(dst);
2898 }
2899 }
2900
2901 let mut pruned: HashMap<String, ObjectId> = HashMap::new();
2902 for item in positive {
2903 let Some(dst) = item.dst.as_deref() else {
2904 continue;
2905 };
2906 if let Some(star) = dst.find('*') {
2907 let prefix = &dst[..star];
2908 for (name, oid) in crate::refs::list_refs(local_git_dir, prefix)? {
2909 if !name.starts_with(prefix) {
2910 continue;
2911 }
2912 if is_default_head_under_tracking_prefix(&name, prefix) {
2913 continue;
2914 }
2915 if !live.contains(&name) {
2916 pruned.entry(name).or_insert(oid);
2917 }
2918 }
2919 } else if !live.contains(dst) {
2920 let exact_prefix = dst.strip_suffix("HEAD").filter(|p| p.ends_with('/'));
2921 if exact_prefix.is_some_and(|p| is_default_head_under_tracking_prefix(dst, p)) {
2922 continue;
2923 }
2924 if let Ok(oid) = crate::refs::resolve_ref(local_git_dir, dst) {
2925 pruned.entry(dst.to_owned()).or_insert(oid);
2926 }
2927 }
2928 }
2929 Ok(pruned.into_iter().collect())
2930}
2931
2932pub(crate) struct FetchRefRejectHint<'a> {
2934 pub prune: bool,
2936 pub positive: &'a [RefspecItem],
2938 pub remote_refs: &'a [(String, ObjectId)],
2940 pub remote_name: Option<&'a str>,
2942}
2943
2944fn fetch_ref_store_error_rejectable(err: &Error) -> bool {
2945 matches!(
2946 err,
2947 Error::RefStore(crate::refs::store::RefStoreError::NameUnavailable { .. })
2948 | Error::RefStore(crate::refs::store::RefStoreError::LockHeld { .. })
2949 )
2950}
2951
2952fn blocking_ref_from_unavailable(reason: &crate::refs::RefnameUnavailable) -> Option<&str> {
2953 match reason {
2954 crate::refs::RefnameUnavailable::AncestorExists { blocking, .. }
2955 | crate::refs::RefnameUnavailable::DescendantExists { blocking, .. } => Some(blocking),
2956 crate::refs::RefnameUnavailable::SameBatch { .. } => None,
2957 }
2958}
2959
2960fn append_fetch_prune_hint(
2961 note: &mut String,
2962 stale_tracking: &HashSet<String>,
2963 blocking: &str,
2964 remote_name: Option<&str>,
2965) {
2966 if stale_tracking.contains(blocking) {
2967 let remote = remote_name.unwrap_or("origin");
2968 note.push_str(&format!(
2969 " — stale remote-tracking ref; try `grit fetch --prune` or `grit remote prune {remote}`"
2970 ));
2971 }
2972}
2973
2974fn mark_fetch_ref_update_rejected(
2975 updates: &mut [RefUpdate],
2976 local_ref: &str,
2977 err: &Error,
2978 stale_tracking: &HashSet<String>,
2979 hint: &FetchRefRejectHint<'_>,
2980) {
2981 let Some(update) = updates
2982 .iter_mut()
2983 .find(|u| u.local_ref.as_deref() == Some(local_ref))
2984 else {
2985 return;
2986 };
2987 update.mode = UpdateMode::RefLockRejected;
2988 let mut note = match err {
2989 Error::RefStore(crate::refs::store::RefStoreError::NameUnavailable { reason }) => {
2990 let mut text = reason.lock_message_suffix();
2991 if let Some(blocking) = blocking_ref_from_unavailable(reason) {
2992 append_fetch_prune_hint(&mut text, stale_tracking, blocking, hint.remote_name);
2993 }
2994 text
2995 }
2996 Error::RefStore(crate::refs::store::RefStoreError::LockHeld { name }) => {
2997 format!("ref '{name}' is locked")
2998 }
2999 _ => err.to_string(),
3000 };
3001 if note.is_empty() {
3002 note = "unable to update local ref".to_owned();
3003 }
3004 update.note = Some(note);
3005}
3006
3007pub(crate) fn apply_fetch_ref_updates(
3017 local_git_dir: &Path,
3018 store_batch: &[crate::refs::store::RefUpdate],
3019 updates: &mut [RefUpdate],
3020 hint: FetchRefRejectHint<'_>,
3021) -> Result<()> {
3022 if store_batch.is_empty() {
3023 return Ok(());
3024 }
3025 if crate::refs::commit_ref_store_batch(local_git_dir, store_batch).is_ok() {
3026 return Ok(());
3027 }
3028
3029 let stale_tracking: HashSet<String> = if hint.prune {
3030 HashSet::new()
3031 } else {
3032 tracking_refs_gone_on_remote(local_git_dir, hint.positive, hint.remote_refs)?
3033 .into_iter()
3034 .map(|(name, _)| name)
3035 .collect()
3036 };
3037
3038 for store_update in store_batch {
3039 match crate::refs::commit_store_update(local_git_dir, store_update.clone()) {
3040 Ok(()) => {}
3041 Err(err) if fetch_ref_store_error_rejectable(&err) => {
3042 mark_fetch_ref_update_rejected(
3043 updates,
3044 &store_update.name,
3045 &err,
3046 &stale_tracking,
3047 &hint,
3048 );
3049 }
3050 Err(err) => return Err(err),
3051 }
3052 }
3053 Ok(())
3054}
3055
3056pub(crate) fn prune_tracking_refs(
3062 local_git_dir: &Path,
3063 positive: &[RefspecItem],
3064 remote_refs: &[(String, ObjectId)],
3065 dry_run: bool,
3066 updates: &mut Vec<RefUpdate>,
3067 mut store_batch: Option<&mut Vec<crate::refs::store::RefUpdate>>,
3068) -> Result<()> {
3069 let pruned: Vec<(String, ObjectId)> =
3070 tracking_refs_gone_on_remote(local_git_dir, positive, remote_refs)?;
3071
3072 let prune_delete_flags = crate::refs::store::RefUpdateFlags {
3073 no_deref: true,
3074 ..Default::default()
3075 };
3076 for (name, oid) in pruned.into_iter() {
3077 if !dry_run {
3078 if let Some(batch) = store_batch.as_mut() {
3079 batch.push(crate::refs::store::RefUpdate {
3080 name: name.clone(),
3081 new_value: None,
3082 expected: crate::refs::store::Expected::Any,
3083 reflog: None,
3084 flags: prune_delete_flags,
3085 });
3086 } else {
3087 crate::refs::delete_ref(local_git_dir, &name)?;
3088 }
3089 }
3090 updates.push(RefUpdate {
3091 remote_ref: String::new(),
3092 local_ref: Some(name),
3093 old_oid: Some(oid),
3094 new_oid: None,
3095 mode: UpdateMode::DeletedMissing,
3096 note: Some("pruned (gone on remote)".to_owned()),
3097 });
3098 }
3099 Ok(())
3100}
3101
3102#[cfg(test)]
3103mod tests {
3104 use super::*;
3105 use crate::progress::ProgressPhase;
3106 use crate::ref_storage::RefStorageFormat;
3107 use crate::repo::init_repository;
3108 use std::process::Command;
3109
3110 #[derive(Debug, Default)]
3111 struct PhaseRecorder {
3112 events: Vec<&'static str>,
3113 started: Vec<(ProgressPhase, Option<u64>)>,
3114 finished: Vec<ProgressPhase>,
3115 counts: Vec<u64>,
3116 active: Option<ProgressPhase>,
3117 current: u64,
3118 }
3119
3120 impl ProgressSink for PhaseRecorder {
3121 fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
3122 self.events.push("start");
3123 self.started.push((phase, total));
3124 self.active = Some(phase);
3125 self.current = 0;
3126 }
3127 fn inc(&mut self, units: u64) {
3128 self.events.push("inc");
3129 self.current += units;
3130 self.counts.push(self.current);
3131 }
3132 fn set(&mut self, current: u64) {
3133 self.events.push("set");
3134 self.current = current;
3135 self.counts.push(self.current);
3136 }
3137 fn finish(&mut self) {
3138 self.events.push("finish");
3139 if let Some(phase) = self.active.take() {
3140 self.finished.push(phase);
3141 }
3142 }
3143 }
3144
3145 impl Progress for PhaseRecorder {}
3146
3147 fn git_ok(dir: &Path, args: &[&str]) -> bool {
3148 Command::new("git") .env("GIT_AUTHOR_NAME", "Test")
3150 .env("GIT_AUTHOR_EMAIL", "test@example.com")
3151 .env("GIT_COMMITTER_NAME", "Test")
3152 .env("GIT_COMMITTER_EMAIL", "test@example.com")
3153 .current_dir(dir)
3154 .args(args)
3155 .env("GIT_CONFIG_GLOBAL", "/dev/null")
3156 .env("GIT_CONFIG_SYSTEM", "/dev/null")
3157 .status()
3158 .map(|s| s.success())
3159 .unwrap_or(false)
3160 }
3161
3162 fn assert_monotonic(counts: &[u64]) {
3163 for pair in counts.windows(2) {
3164 assert!(pair[1] >= pair[0], "progress counts must be monotonic");
3165 }
3166 }
3167
3168 #[test]
3169 fn local_fetch_classifies_through_repository_ref_store() {
3170 use crate::refs::store::{MemoryRefStore, RawRef};
3171 use std::sync::Arc;
3172
3173 let tmp = tempfile::tempdir().unwrap();
3174 assert!(git_ok(tmp.path(), &["init", "-q", "-b", "main"]));
3175 assert!(git_ok(
3176 tmp.path(),
3177 &["commit", "--allow-empty", "-qm", "base"]
3178 ));
3179 let repo =
3180 crate::repo::Repository::open(&tmp.path().join(".git"), &RepositoryOptions::empty())
3181 .unwrap();
3182 let tip = repo.resolve_ref_oid("HEAD").unwrap();
3183 let store = Arc::new(MemoryRefStore::new());
3184 store
3185 .set_ref("refs/remotes/origin/main", RawRef::Direct(tip))
3186 .unwrap();
3187 let repo = repo.with_ref_store(store);
3188 let opts = FetchOptions {
3189 refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".into()],
3190 tags: TagMode::None,
3191 dry_run: true,
3192 ..Default::default()
3193 };
3194 let outcome = fetch_local_into(
3195 &repo,
3196 &tmp.path().join(".git"),
3197 &opts,
3198 &mut PhaseRecorder::default(),
3199 &crate::progress::NeverCancel,
3200 )
3201 .unwrap();
3202 assert_eq!(outcome.transfer.objects, 0);
3203 assert_eq!(outcome.updates.len(), 1);
3204 assert_eq!(outcome.updates[0].old_oid, Some(tip));
3205 assert_eq!(outcome.updates[0].mode, UpdateMode::UpToDate);
3206 }
3207
3208 #[test]
3209 fn fetch_local_reports_copy_progress() {
3210 let remote = tempfile::tempdir().expect("remote");
3211 assert!(git_ok(
3212 remote.path(),
3213 &["init", "-q", "-b", "main", "--bare"]
3214 ));
3215 let src = tempfile::tempdir().expect("src");
3216 assert!(git_ok(src.path(), &["init", "-q", "-b", "main"]));
3217 std::fs::write(src.path().join("f.txt"), "payload\n").unwrap();
3218 assert!(git_ok(src.path(), &["add", "f.txt"]));
3219 assert!(git_ok(src.path(), &["commit", "-qm", "c1"]));
3220 assert!(git_ok(
3221 src.path(),
3222 &["push", "-q", remote.path().to_str().unwrap(), "main:main",],
3223 ));
3224
3225 let local = tempfile::tempdir().expect("local");
3226 init_repository(local.path(), false, "main", None, RefStorageFormat::Files).expect("init");
3227
3228 let opts = FetchOptions {
3229 refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
3230 initial_remote_fetch: true,
3231 tags: TagMode::None,
3232 ..Default::default()
3233 };
3234 let mut rec = PhaseRecorder::default();
3235 fetch_local_with_progress(
3236 &local.path().join(".git"),
3237 remote.path(),
3238 &opts,
3239 &mut rec,
3240 &crate::progress::NeverCancel,
3241 )
3242 .expect("fetch_local");
3243
3244 assert!(
3245 rec.started
3246 .iter()
3247 .any(|(p, _)| *p == ProgressPhase::CopyingObjects),
3248 "expected CopyingObjects phase"
3249 );
3250 assert!(
3251 rec.finished.contains(&ProgressPhase::CopyingObjects),
3252 "expected CopyingObjects finish"
3253 );
3254 assert!(!rec.counts.is_empty(), "expected copy progress counts");
3255 assert_monotonic(&rec.counts);
3256 let first_progress = rec.events.iter().find(|e| **e != "start").copied();
3257 assert_eq!(
3258 rec.events.first().copied(),
3259 Some("start"),
3260 "CopyingObjects must start before link/copy work reports progress"
3261 );
3262 assert!(
3263 first_progress.is_some(),
3264 "expected set/inc after start during copy"
3265 );
3266 }
3267
3268 #[test]
3269 fn transfer_stats_add_up_and_keep_kinds_only_when_known() {
3270 use crate::index_pack::ObjectKindCounts;
3271 let kinds = |commits, blobs| ObjectKindCounts {
3272 commits,
3273 trees: 0,
3274 blobs,
3275 tags: 0,
3276 };
3277 let mut total = TransferStats::default();
3278 total += TransferStats {
3279 objects: 3,
3280 bytes: 100,
3281 kinds: Some(kinds(1, 2)),
3282 };
3283 total += TransferStats::default();
3285 total += TransferStats {
3286 objects: 2,
3287 bytes: 50,
3288 kinds: Some(kinds(1, 1)),
3289 };
3290 assert_eq!(
3291 total,
3292 TransferStats {
3293 objects: 5,
3294 bytes: 150,
3295 kinds: Some(kinds(2, 3)),
3296 }
3297 );
3298
3299 total += TransferStats {
3301 objects: 4,
3302 bytes: 10,
3303 kinds: None,
3304 };
3305 assert_eq!(total.objects, 9);
3306 assert_eq!(total.bytes, 160);
3307 assert_eq!(total.kinds, None);
3308 }
3309
3310 #[test]
3311 fn transfer_stats_for_local_operation_counts_new_pack_index() {
3312 let tmp = tempfile::tempdir().expect("tempdir");
3313 let repo = tmp.path().join("repo");
3314 std::fs::create_dir_all(&repo).unwrap();
3315 assert!(git_ok(&repo, &["init", "-q", "-b", "main", "."]));
3316 std::fs::write(repo.join("blob.txt"), b"payload\n").unwrap();
3317 assert!(git_ok(&repo, &["add", "blob.txt"]));
3318 assert!(git_ok(&repo, &["commit", "-qm", "c"]));
3319 assert!(git_ok(&repo, &["repack", "-a", "-d", "-f"]));
3320
3321 let git_dir = repo.join(".git");
3322 let pack_dir = git_dir.join("objects/pack");
3323 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.clone());
3324 let stats = transfer_stats_for_local_operation(&odb, &HashSet::new(), &HashSet::new())
3325 .expect("stats");
3326 assert!(stats.bytes > 0);
3327 assert!(stats.objects > 0);
3328
3329 let pack_path = std::fs::read_dir(&pack_dir)
3330 .expect("read pack dir")
3331 .filter_map(|e| e.ok())
3332 .map(|e| e.path())
3333 .find(|p| p.extension().is_some_and(|e| e == "pack"))
3334 .expect("pack file");
3335 let idx = pack_path.with_extension("idx");
3336 let index = crate::pack::read_pack_index_no_verify(&idx).expect("idx");
3337 assert_eq!(
3338 stats.objects,
3339 u64::try_from(index.len()).expect("count"),
3340 "object count must match the on-disk index entry count"
3341 );
3342 }
3343
3344 #[test]
3345 fn update_mode_modified_repo_excludes_rejected_tag_updates() {
3346 assert!(UpdateMode::New.modified_repo());
3347 assert!(UpdateMode::DeletedMissing.modified_repo());
3348 assert!(!UpdateMode::TagUpdateRejected.modified_repo());
3349 assert!(!UpdateMode::UpToDate.modified_repo());
3350 }
3351
3352 #[test]
3353 fn update_mode_report_classification_covers_every_mode() {
3354 use UpdateMode::*;
3355 for (mode, reported, rejected) in [
3356 (New, true, false),
3357 (FastForward, true, false),
3358 (Forced, true, false),
3359 (DeletedMissing, true, false),
3360 (NonFastForwardRejected, true, true),
3361 (TagUpdateRejected, true, true),
3362 (SourceObjectNotFound, true, true),
3363 (UpToDate, false, false),
3364 (NoChangeNeeded, false, false),
3365 (Unborn, false, false),
3366 ] {
3367 assert_eq!(mode.is_reported(), reported, "{mode:?}");
3368 assert_eq!(mode.is_rejected(), rejected, "{mode:?}");
3369 assert!(!(mode.modified_repo() && mode.is_rejected()), "{mode:?}");
3370 }
3371 }
3372
3373 #[test]
3374 fn fetch_outcome_summarizes_reported_changed_and_rejected_refs() {
3375 let update = |name: &str, mode| RefUpdate {
3376 remote_ref: format!("refs/heads/{name}"),
3377 local_ref: Some(format!("refs/remotes/origin/{name}")),
3378 old_oid: None,
3379 new_oid: None,
3380 mode,
3381 note: None,
3382 };
3383 let outcome = FetchOutcome {
3384 updates: vec![
3385 update("same", UpdateMode::UpToDate),
3386 update("new", UpdateMode::New),
3387 update("ff", UpdateMode::FastForward),
3388 update("noop", UpdateMode::NoChangeNeeded),
3389 ],
3390 ..Default::default()
3391 };
3392 let reported: Vec<&str> = outcome
3393 .reported_updates()
3394 .map(|u| u.remote_ref.as_str())
3395 .collect();
3396 assert_eq!(reported, ["refs/heads/new", "refs/heads/ff"]);
3397 assert_eq!(outcome.changed_ref_count(), 2);
3398 assert!(!outcome.has_rejections());
3399
3400 let rejected = FetchOutcome {
3401 updates: vec![update("diverged", UpdateMode::NonFastForwardRejected)],
3402 ..Default::default()
3403 };
3404 assert_eq!(rejected.changed_ref_count(), 0);
3405 assert!(rejected.has_rejections());
3406 assert_eq!(rejected.reported_updates().count(), 1);
3407 assert!(FetchOutcome::default().reported_updates().next().is_none());
3408 }
3409}