1use crate::git_path::CanonicalizeExt;
30use std::collections::BTreeSet;
31use std::fs;
32use std::fs::OpenOptions;
33use std::io::Write;
34use std::path::{Component, Path, PathBuf};
35use std::sync::{Arc, Mutex};
36
37use crate::change_id::ChangeIdGenerator;
38use crate::check_ref_format::validate_branch_short_name;
39use crate::command_runner::CommandRunner;
40use crate::config::{
41 config_file_stamps, stamp_for_path, ConfigCacheKey, ConfigFile, ConfigScope, ConfigSet,
42 LoadConfigOptions,
43};
44use crate::diagnostics::{self, DiagnosticsHandle};
45use crate::environment::{Environment, RepositoryOptions};
46use crate::error::{Error, NotARepositoryReason, Result};
47use crate::git_dir::io_error::{map_git_dir_io, map_index_write_error, GitDirPathOperation};
48use crate::git_path::path_for_error_message;
49use crate::hooks::run_hook;
50use crate::index::Index;
51use crate::init_filesystem::{apply_init_filesystem_config, InitFilesystemConfigOptions};
52use crate::objects::{HashAlgo, ObjectId};
53use crate::odb::Odb;
54use crate::repo_caches::RepoCaches;
55use crate::rev_parse::is_inside_work_tree;
56use crate::sparse_checkout::effective_cone_mode_for_sparse_file;
57use crate::split_index::{write_index_file_split, WriteSplitIndexRequest};
58use crate::worktree::io_error::{map_worktree_io, WorktreePathOperation};
59use crate::worktree_cwd::cwd_relative_under_work_tree;
60
61fn compute_git_prefix(env: &Environment, work_tree: &Path) -> Option<String> {
63 if let Some(prefix) = env.git_prefix.as_deref() {
64 let p = prefix.trim();
65 if !p.is_empty() {
66 return Some(p.to_owned());
67 }
68 }
69 let rel = cwd_relative_under_work_tree(work_tree, &env.cwd)?;
70 if rel.is_empty() {
71 None
72 } else {
73 Some(rel)
74 }
75}
76
77fn read_sparse_checkout_patterns(git_dir: &Path) -> Vec<String> {
78 let path = git_dir.join("info").join("sparse-checkout");
79 let Ok(content) = fs::read_to_string(&path) else {
80 return Vec::new();
81 };
82 content
83 .lines()
84 .map(|l| l.trim())
85 .filter(|l| !l.is_empty() && !l.starts_with('#'))
86 .map(String::from)
87 .collect()
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct DiscoveryInfo {
93 pub explicit_git_dir: bool,
95 pub discovery_root: Option<PathBuf>,
97 pub work_tree_from_env: bool,
99 pub discovery_via_gitfile: bool,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct InitOutcome {
106 pub reinitialized: bool,
108 pub initial_branch: String,
110}
111
112#[derive(Debug, Clone)]
114#[non_exhaustive]
115pub struct InitOptions {
116 pub bare: bool,
118 pub initial_branch: Option<String>,
124 pub template_dir: Option<PathBuf>,
126 pub ref_storage: crate::ref_storage::RefStorageFormat,
128 pub separate_git_dir: Option<PathBuf>,
130 pub bare_with_worktree_at_path: bool,
132 pub object_format: Option<HashAlgo>,
136 pub environment: Option<Environment>,
141}
142
143impl Default for InitOptions {
144 fn default() -> Self {
145 Self {
146 bare: false,
147 initial_branch: None,
148 template_dir: None,
149 ref_storage: crate::ref_storage::RefStorageFormat::Files,
150 separate_git_dir: None,
151 bare_with_worktree_at_path: false,
152 object_format: None,
153 environment: None,
154 }
155 }
156}
157
158impl InitOptions {
159 #[must_use]
161 pub fn new() -> Self {
162 Self::default()
163 }
164
165 #[must_use]
167 pub fn with_bare(mut self, bare: bool) -> Self {
168 self.bare = bare;
169 self
170 }
171
172 #[must_use]
174 pub fn with_initial_branch(mut self, initial_branch: impl Into<String>) -> Self {
175 self.initial_branch = Some(initial_branch.into());
176 self
177 }
178
179 #[must_use]
181 pub fn with_object_format(mut self, object_format: HashAlgo) -> Self {
182 self.object_format = Some(object_format);
183 self
184 }
185
186 #[must_use]
188 pub fn with_environment(mut self, environment: Environment) -> Self {
189 self.environment = Some(environment);
190 self
191 }
192
193 #[must_use]
195 pub fn with_ref_storage(mut self, ref_storage: crate::ref_storage::RefStorageFormat) -> Self {
196 self.ref_storage = ref_storage;
197 self
198 }
199
200 #[must_use]
202 pub fn with_template_dir(mut self, template_dir: Option<PathBuf>) -> Self {
203 self.template_dir = template_dir;
204 self
205 }
206
207 #[must_use]
209 pub fn with_separate_git_dir(mut self, separate_git_dir: Option<PathBuf>) -> Self {
210 self.separate_git_dir = separate_git_dir;
211 self
212 }
213
214 #[must_use]
216 pub fn with_bare_with_worktree_at_path(mut self, bare_with_worktree_at_path: bool) -> Self {
217 self.bare_with_worktree_at_path = bare_with_worktree_at_path;
218 self
219 }
220}
221
222#[must_use]
271pub struct RepositoryBuilder {
272 options: RepositoryOptions,
273 odb_builder: Option<crate::odb::OdbBuilder>,
274 ref_store: Option<Arc<dyn crate::refs::store::RefStore>>,
275 initial_branch: Option<String>,
276}
277
278impl RepositoryBuilder {
279 pub fn new(options: RepositoryOptions) -> Self {
281 Self {
282 options,
283 odb_builder: None,
284 ref_store: None,
285 initial_branch: None,
286 }
287 }
288
289 pub fn odb_builder(mut self, builder: crate::odb::OdbBuilder) -> Self {
298 self.odb_builder = Some(builder);
299 self
300 }
301
302 pub fn ref_store(mut self, store: Arc<dyn crate::refs::store::RefStore>) -> Self {
313 self.ref_store = Some(store);
314 self
315 }
316
317 pub fn initial_branch(mut self, branch: impl Into<String>) -> Self {
322 self.initial_branch = Some(branch.into());
323 self
324 }
325
326 pub fn open(self, git_dir: &Path, work_tree: Option<&Path>) -> Result<Repository> {
347 let mut options = self.options;
348 if let Some(wt) = work_tree {
349 options.work_tree = Some(wt.to_path_buf());
350 }
351 let repo = if let Some(builder) = self.odb_builder {
352 Repository::open_with_odb_internal(&options, git_dir, builder, self.ref_store)?
353 } else {
354 Repository::open_internal_with_refs(&options, git_dir, self.ref_store)?
355 };
356 if let Some(branch) = self.initial_branch {
357 seed_unborn_head(repo.refs(), &branch)?;
358 }
359 Ok(repo)
360 }
361}
362
363fn seed_unborn_head(store: &dyn crate::refs::store::RefStore, branch: &str) -> Result<()> {
365 use crate::refs::store::{Expected, RawRef, RefStoreError, RefTransaction, RefUpdate};
366
367 validate_branch_short_name(branch).map_err(|e| Error::InvalidRef(e.to_string()))?;
368 if store.read_raw("HEAD")?.is_some() {
369 return Ok(());
370 }
371 let txn = RefTransaction::new().update(RefUpdate {
372 name: "HEAD".to_owned(),
373 new_value: Some(RawRef::Symbolic(format!("refs/heads/{branch}"))),
374 expected: Expected::Missing,
375 reflog: None,
376 flags: crate::refs::store::RefUpdateFlags {
377 no_deref: true,
378 ..Default::default()
379 },
380 })?;
381 match store.prepare(txn).and_then(|prepared| prepared.commit()) {
382 Ok(()) | Err(RefStoreError::ExpectedMismatch { .. }) => Ok(()),
383 Err(err) => Err(err.into()),
384 }
385}
386
387pub struct Repository {
425 git_dir: PathBuf,
426 common_git_dir: PathBuf,
428 work_tree: Option<PathBuf>,
429 odb: Odb,
430 explicit_git_dir: bool,
431 discovery_root: Option<PathBuf>,
432 work_tree_from_env: bool,
433 discovery_via_gitfile: bool,
434 config_session: Mutex<Option<ConfigSession>>,
436 caches: Arc<RepoCaches>,
438 ref_store: crate::refs::store::ObjectCheckedRefStore,
440 environment: Arc<Environment>,
442 git_prefix: Option<String>,
444 command_runner: Arc<dyn CommandRunner>,
446 change_id_generator: Arc<dyn ChangeIdGenerator>,
448 diagnostics: DiagnosticsHandle,
449 network_trace: bool,
450 reference_unix_time: Option<i64>,
452 test_assume_different_owner: bool,
453 thread_policy: crate::parallel::ThreadPolicy,
454 hook_stdio: crate::hooks::HookStdio,
455}
456
457const _: () = {
458 fn assert_send_sync<T: Send + Sync>() {}
459 let _ = assert_send_sync::<Repository>;
460};
461
462impl std::fmt::Debug for Repository {
463 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464 f.debug_struct("Repository")
465 .field("git_dir", &self.git_dir())
466 .field("work_tree", &self.work_tree())
467 .field("explicit_git_dir", &self.discovery_info().explicit_git_dir)
468 .field("network_trace", &self.network_trace)
469 .finish_non_exhaustive()
470 }
471}
472
473pub(crate) type RepositoryConfigSnapshot = Arc<RepoCaches>;
475
476#[derive(Debug, Clone)]
478struct RepoCachedSettings {
479 use_replace_refs: bool,
481 replace_ref_base: String,
483}
484
485#[derive(Clone)]
487struct ConfigSessionValidation {
488 key: ConfigCacheKey,
489 env_fingerprint: Vec<(String, Option<String>)>,
490 base_stamps: Vec<(PathBuf, Option<(std::time::SystemTime, u64)>)>,
491 extra_stamps: Vec<(PathBuf, Option<(std::time::SystemTime, u64)>)>,
492 environment: Arc<Environment>,
496}
497
498struct ConfigSession {
500 config: Arc<ConfigSet>,
501 validation: ConfigSessionValidation,
502 cached_settings: RepoCachedSettings,
503 replace_lookup_inactive: bool,
505}
506
507impl ConfigSessionValidation {
508 fn still_valid(
514 &mut self,
515 env: &Arc<Environment>,
516 git_dir: &Path,
517 opts: impl FnOnce() -> LoadConfigOptions,
518 ) -> bool {
519 if Arc::ptr_eq(env, &self.environment) && self.key.git_dir.as_deref() == Some(git_dir) {
520 return stamps_unchanged(&self.base_stamps) && stamps_unchanged(&self.extra_stamps);
521 }
522 let opts = &opts();
523 let Some(env_fp) = env.config_fingerprint() else {
524 return false;
525 };
526 if self.env_fingerprint.as_slice() != env_fp.as_slice() {
527 return false;
528 }
529 let key = ConfigCacheKey::new(Some(git_dir), opts);
530 if key != self.key {
531 return false;
532 }
533 let base_stamps = config_file_stamps(env, Some(git_dir), opts);
534 if self.base_stamps.as_slice() != base_stamps.as_slice()
535 || !stamps_unchanged(&self.extra_stamps)
536 {
537 return false;
538 }
539 self.environment = Arc::clone(env);
540 true
541 }
542}
543
544fn stamps_unchanged(stamps: &[(PathBuf, Option<(std::time::SystemTime, u64)>)]) -> bool {
545 stamps
546 .iter()
547 .all(|(path, stamp)| stamp_for_path(path) == *stamp)
548}
549
550#[allow(clippy::too_many_arguments)]
551fn checked_ref_store_with_hooks(
552 inner: Arc<dyn crate::refs::store::RefStore>,
553 odb: crate::odb::Odb,
554 git_dir: &Path,
555 work_tree: Option<&Path>,
556 environment: &Arc<Environment>,
557 command_runner: &Arc<dyn CommandRunner>,
558 hook_stdio: crate::hooks::HookStdio,
559 diagnostics: &crate::diagnostics::DiagnosticsHandle,
560) -> crate::refs::store::ObjectCheckedRefStore {
561 let hooks = Arc::new(crate::hooks::RefHookContext::new(
562 git_dir.to_path_buf(),
563 work_tree.map(Path::to_path_buf),
564 Arc::clone(environment),
565 Arc::clone(command_runner),
566 hook_stdio,
567 Arc::clone(diagnostics),
568 odb.hash_algo(),
569 ));
570 crate::refs::store::ObjectCheckedRefStore::new(inner, odb).with_ref_hooks(hooks)
571}
572
573impl Repository {
574 fn config_load_options(&self, include_system: bool) -> LoadConfigOptions {
575 LoadConfigOptions {
576 include_system,
577 diagnostics: Some(self.diagnostics()),
578 ..LoadConfigOptions::for_repository(
579 &self.environment,
580 Some(&self.git_dir),
581 Some(Arc::clone(&self.caches)),
582 )
583 }
584 }
585
586 fn from_canonical_git_dir(
587 options: &RepositoryOptions,
588 git_dir: PathBuf,
589 work_tree: Option<&Path>,
590 injected_refs: Option<Arc<dyn crate::refs::store::RefStore>>,
591 ) -> Result<Self> {
592 let environment = Arc::new(options.environment.clone());
593 let command_runner = Arc::clone(&options.command_runner);
594 let change_id_generator = Arc::clone(&options.change_id_generator);
595 if injected_refs.is_none() && !head_file_present(&git_dir) {
598 return Err(not_a_repository_at(&git_dir));
599 }
600
601 let common_git_dir = resolve_common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
602 let objects_dir = resolve_objects_dir(&environment, &git_dir, &common_git_dir)
603 .filter(|dir| dir.exists())
604 .ok_or_else(|| not_a_repository_at(&git_dir))?;
605
606 let work_tree = match work_tree {
607 Some(p) => {
608 let cwd = environment.discovery_cwd();
609 let mut resolved = if p.is_absolute() {
610 p.to_path_buf()
611 } else {
612 cwd.join(p)
613 };
614 if resolved.exists() {
615 resolved = resolved
616 .canonicalize_simplified()
617 .map_err(|_| Error::PathError(p.display().to_string()))?;
618 }
619 Some(resolved)
620 }
621 None => None,
622 };
623
624 let caches = RepoCaches::with_diagnostics(
625 Arc::clone(&command_runner),
626 Arc::clone(&options.diagnostics),
627 );
628 caches.set_promisor_fetcher(options.promisor_fetcher.clone());
629 let odb = if let Some(ref wt) = work_tree {
630 Odb::with_work_tree(&objects_dir, wt)
631 .with_config_git_dir(git_dir.clone())
632 .with_shared_config_state(caches.clone(), environment.clone())
633 } else {
634 Odb::new(&objects_dir)
635 .with_config_git_dir(git_dir.clone())
636 .with_shared_config_state(caches.clone(), environment.clone())
637 };
638
639 let git_prefix = work_tree
640 .as_ref()
641 .and_then(|wt| compute_git_prefix(environment.as_ref(), wt));
642
643 let ref_store = checked_ref_store_with_hooks(
644 open_or_install_ref_store(&caches, &git_dir, &environment, injected_refs)?,
645 odb.clone(),
646 &git_dir,
647 work_tree.as_deref(),
648 &environment,
649 &command_runner,
650 options.hook_stdio,
651 &options.diagnostics,
652 );
653 caches.configure_filter_subprocess(&environment, &git_dir, work_tree.as_deref());
654
655 Ok(Self {
656 git_dir,
657 common_git_dir,
658 work_tree,
659 odb,
660 explicit_git_dir: false,
661 discovery_root: None,
662 work_tree_from_env: false,
663 discovery_via_gitfile: false,
664 config_session: Mutex::new(None),
665 caches,
666 ref_store,
667 environment,
668 git_prefix,
669 command_runner,
670 change_id_generator,
671 diagnostics: Arc::clone(&options.diagnostics),
672 network_trace: options.network_trace,
673 reference_unix_time: options.reference_unix_time,
674 test_assume_different_owner: options.test_assume_different_owner,
675 thread_policy: options.thread_policy,
676 hook_stdio: options.hook_stdio,
677 })
678 }
679
680 #[must_use]
682 pub fn thread_policy(&self) -> crate::parallel::ThreadPolicy {
683 self.thread_policy
684 }
685
686 #[must_use]
688 pub fn with_thread_policy(mut self, policy: crate::parallel::ThreadPolicy) -> Self {
689 self.thread_policy = policy;
690 self
691 }
692
693 #[must_use]
695 pub(crate) fn caches(&self) -> &Arc<RepoCaches> {
696 &self.caches
697 }
698
699 #[must_use]
701 pub(crate) fn promisor_hydrate_hook(&self) -> Option<crate::blame::PromisorHydrateHook> {
702 self.caches.promisor_hydrate_hook()
703 }
704
705 #[must_use]
707 pub fn environment(&self) -> &Environment {
708 &self.environment
709 }
710
711 #[must_use]
713 pub fn reference_unix_time(&self) -> i64 {
714 self.wall_clock_epoch()
715 }
716
717 #[must_use]
719 pub fn wall_clock_epoch(&self) -> i64 {
720 if let Some(ts) = self.reference_unix_time {
721 return ts;
722 }
723 if let Some(ts) = self.environment.git_now_date_override() {
724 return ts;
725 }
726 crate::git_date::tm::process_wall_clock_sec()
727 }
728
729 #[must_use]
733 pub fn identity_now(&self) -> time::OffsetDateTime {
734 crate::environment::offset_from_epoch_and_tz(
735 self.wall_clock_epoch(),
736 self.environment.tz.as_deref(),
737 )
738 }
739
740 pub fn identity(
774 &self,
775 role: crate::config::ident_resolve::IdentRole,
776 now: time::OffsetDateTime,
777 ) -> Result<crate::ident::Signature> {
778 let config = self.config()?;
779 Ok(crate::config::ident_resolve::resolve_signature_with(
780 self.environment(),
781 &config,
782 role,
783 now,
784 )?)
785 }
786
787 #[must_use]
789 pub fn command_runner(&self) -> Arc<dyn CommandRunner> {
790 Arc::clone(&self.command_runner)
791 }
792
793 #[must_use]
796 pub fn hook_stdio(&self) -> crate::hooks::HookStdio {
797 self.hook_stdio
798 }
799
800 #[must_use]
802 pub fn change_id_generator(&self) -> Arc<dyn ChangeIdGenerator> {
803 Arc::clone(&self.change_id_generator)
804 }
805
806 #[must_use]
812 pub fn linked_worktree_discover_options(&self) -> RepositoryOptions {
813 let mut environment = (*self.environment).clone();
814 environment.git_dir = None;
815 environment.git_work_tree = None;
816 environment.git_index_file = None;
817 environment.git_prefix = None;
818 environment.git_object_directory = None;
819 RepositoryOptions {
820 environment,
821 command_runner: Arc::clone(&self.command_runner),
822 change_id_generator: Arc::clone(&self.change_id_generator),
823 diagnostics: Arc::clone(&self.diagnostics),
824 network_trace: self.network_trace,
825 test_assume_different_owner: self.test_assume_different_owner,
826 reference_unix_time: self.reference_unix_time,
827 thread_policy: self.thread_policy,
828 work_tree: None,
829 validate_repository_format: true,
830 open_for_serving: false,
831 promisor_fetcher: self.caches().promisor_fetcher(),
832 hook_stdio: self.hook_stdio,
833 }
834 }
835
836 #[must_use]
845 pub(crate) fn peer_repository_options(&self) -> RepositoryOptions {
846 let mut options = self.linked_worktree_discover_options();
847 let env = &mut options.environment;
848 env.git_alternate_object_directories = None;
849 env.git_namespace = None;
850 env.git_replace_ref_base = None;
851 env.git_no_replace_objects = false;
852 env.git_config = None;
853 env.git_config_parameters = None;
854 env.git_config_count = None;
855 env.git_config_pairs.clear();
856 options
857 }
858
859 pub fn open_linked_worktree(&self, admin_dir: &Path, work_tree: &Path) -> Result<Self> {
875 let options = self.linked_worktree_discover_options();
876 if self.refs_on_disk() && self.odb.uses_files_primary() {
877 return Self::discover(Some(work_tree), &options);
878 }
879 let git_dir = admin_dir
880 .canonicalize_simplified()
881 .map_err(|_| not_a_repository_at(admin_dir))?;
882 let work_tree = work_tree
883 .canonicalize_simplified()
884 .map_err(|_| Error::PathError(work_tree.display().to_string()))?;
885 let environment = Arc::new(options.environment);
886 let caches = RepoCaches::with_diagnostics(
887 Arc::clone(&self.command_runner),
888 Arc::clone(&self.diagnostics),
889 );
890 let ref_store = if self.refs_on_disk() {
891 caches.open_ref_store(&git_dir, &environment)?
892 } else {
893 let view: Arc<dyn crate::refs::store::RefStore> =
894 Arc::new(crate::refs::store::LinkedWorktreeRefStore::new(
895 git_dir.clone(),
896 Arc::clone(self.ref_store.inner()),
897 ));
898 caches.install_ref_store(&git_dir, Arc::clone(&view));
899 view
900 };
901 let ref_store = checked_ref_store_with_hooks(
902 ref_store,
903 self.odb.clone(),
904 &git_dir,
905 Some(work_tree.as_path()),
906 &environment,
907 &self.command_runner,
908 self.hook_stdio,
909 &self.diagnostics,
910 );
911 let repo = Self {
912 git_prefix: compute_git_prefix(&environment, &work_tree),
913 git_dir,
914 common_git_dir: self.common_git_dir.clone(),
915 work_tree: Some(work_tree),
916 odb: self.odb.clone(),
917 explicit_git_dir: false,
918 discovery_root: None,
919 work_tree_from_env: false,
920 discovery_via_gitfile: true,
921 config_session: Mutex::new(None),
922 caches,
923 ref_store,
924 environment,
925 command_runner: Arc::clone(&self.command_runner),
926 change_id_generator: Arc::clone(&self.change_id_generator),
927 diagnostics: Arc::clone(&self.diagnostics),
928 network_trace: self.network_trace,
929 reference_unix_time: self.reference_unix_time,
930 test_assume_different_owner: self.test_assume_different_owner,
931 thread_policy: self.thread_policy,
932 hook_stdio: self.hook_stdio,
933 };
934 repo.caches().configure_filter_subprocess(
935 &repo.environment,
936 repo.git_dir(),
937 repo.work_tree(),
938 );
939 repo.refresh_config_session()?;
940 Ok(repo)
941 }
942
943 fn repo_cached_settings_from_config(
944 cfg: &ConfigSet,
945 environment: &Environment,
946 ) -> RepoCachedSettings {
947 let use_replace_refs = cfg
948 .get_bool("core.useReplaceRefs")
949 .and_then(|r| r.ok())
950 .unwrap_or(true);
951 let replace_ref_base = environment
952 .git_replace_ref_base
953 .clone()
954 .filter(|s| !s.is_empty())
955 .unwrap_or_else(|| "refs/replace/".to_owned());
956 let replace_ref_base = if replace_ref_base.ends_with('/') {
957 replace_ref_base
958 } else {
959 format!("{replace_ref_base}/")
960 };
961 RepoCachedSettings {
962 use_replace_refs,
963 replace_ref_base,
964 }
965 }
966
967 fn ensure_config_arc(&self) -> Result<Arc<ConfigSet>> {
968 self.config_arc_with_validation(true)
969 }
970
971 pub(crate) fn ensure_config_arc_fast(&self) -> Result<Arc<ConfigSet>> {
973 self.config_arc_with_validation(false)
974 }
975
976 fn config_arc_with_validation(&self, validate_stamps: bool) -> Result<Arc<ConfigSet>> {
977 if validate_stamps {
978 if let Ok(mut guard) = self.config_session.lock() {
979 if let Some(session) = guard.as_mut() {
980 if session
981 .validation
982 .still_valid(&self.environment, &self.git_dir, || {
983 self.config_load_options(true)
984 })
985 {
986 return Ok(Arc::clone(&session.config));
987 }
988 }
989 }
990 } else if let Ok(guard) = self.config_session.lock() {
991 if let Some(session) = guard.as_ref() {
992 return Ok(Arc::clone(&session.config));
993 }
994 }
995 self.refresh_config_session()?;
996 self.config_session
997 .lock()
998 .map_err(|e| Error::Message(format!("config session lock poisoned: {e}")))?
999 .as_ref()
1000 .map(|s| Arc::clone(&s.config))
1001 .ok_or_else(|| Error::Message("config session missing after refresh".to_owned()))
1002 }
1003
1004 fn refresh_config_session(&self) -> Result<()> {
1005 crate::trace::trace_span!("grit.config.load");
1006 let opts = self.config_load_options(true);
1007 if let Ok(mut guard) = self.config_session.lock() {
1008 if let Some(session) = guard.as_mut() {
1009 if session
1010 .validation
1011 .still_valid(&self.environment, &self.git_dir, || opts.clone())
1012 {
1013 return Ok(());
1014 }
1015 let env = self.environment.as_ref();
1016 let base_stamps = config_file_stamps(env, Some(&self.git_dir), &opts);
1017 if session.validation.base_stamps.as_slice() != base_stamps.as_slice() {
1018 self.caches.invalidate_config_file_derived();
1019 }
1020 }
1021 }
1022 self.caches.clear_session_config();
1023 let env = self.environment.as_ref();
1024 let cfg = self
1025 .caches
1026 .load_config_with_options(env, Some(&self.git_dir), &opts)?;
1027 let key = ConfigCacheKey::new(Some(&self.git_dir), &opts);
1028 let (env_fingerprint, base_stamps, extra_stamps) =
1029 self.caches.config_entry_stamps(&key).unwrap_or_else(|| {
1030 (
1031 env.config_fingerprint().unwrap_or_default(),
1032 config_file_stamps(env, Some(&self.git_dir), &opts),
1033 Vec::new(),
1034 )
1035 });
1036 let cached_settings =
1037 Self::repo_cached_settings_from_config(cfg.as_ref(), self.environment.as_ref());
1038 let replace_lookup_inactive = self.replace_lookup_inactive(cfg.as_ref());
1039 self.caches
1040 .set_session_config(key.clone(), Arc::clone(&cfg));
1041 let session = ConfigSession {
1042 config: Arc::clone(&cfg),
1043 validation: ConfigSessionValidation {
1044 key,
1045 env_fingerprint,
1046 base_stamps,
1047 extra_stamps,
1048 environment: Arc::clone(&self.environment),
1049 },
1050 cached_settings,
1051 replace_lookup_inactive,
1052 };
1053 *self
1054 .config_session
1055 .lock()
1056 .map_err(|e| Error::Message(format!("config session lock poisoned: {e}")))? =
1057 Some(session);
1058 Ok(())
1059 }
1060
1061 fn replace_lookup_inactive(&self, cfg: &ConfigSet) -> bool {
1062 if self.environment.git_no_replace_objects {
1063 return true;
1064 }
1065 if !cfg
1066 .get_bool("core.useReplaceRefs")
1067 .and_then(|r| r.ok())
1068 .unwrap_or(true)
1069 {
1070 return true;
1071 }
1072 let replace_dir = self.common_git_dir.join("refs/replace");
1073 !replace_dir_is_non_empty(&replace_dir)
1074 }
1075
1076 #[must_use]
1078 pub fn diagnostics(&self) -> DiagnosticsHandle {
1079 Arc::clone(&self.diagnostics)
1080 }
1081
1082 pub fn set_diagnostics(&mut self, sink: DiagnosticsHandle) {
1084 self.caches.set_diagnostics(Arc::clone(&sink));
1085 self.diagnostics = sink;
1086 }
1087
1088 #[must_use]
1090 pub fn network_trace_enabled(&self) -> bool {
1091 self.network_trace
1092 }
1093
1094 pub fn set_network_trace(&mut self, enabled: bool) {
1096 self.network_trace = enabled;
1097 }
1098
1099 pub(crate) fn warn(&self, warning: diagnostics::Warning) {
1100 self.diagnostics.warn(warning);
1101 }
1102
1103 pub fn config(&self) -> Result<Arc<ConfigSet>> {
1114 self.ensure_config_arc()
1115 }
1116
1117 pub fn reload_config(&self) -> Result<()> {
1126 self.caches.invalidate_config_cache();
1127 if let Ok(mut guard) = self.config_session.lock() {
1128 *guard = None;
1129 }
1130 self.refresh_config_session()?;
1131 Ok(())
1132 }
1133
1134 pub(crate) fn install_config_snapshot(&self) {
1136 let _ = self
1137 .caches
1138 .open_ref_store(&self.git_dir, self.environment.as_ref());
1139 }
1140
1141 #[must_use]
1148 pub fn common_git_dir(&self) -> &Path {
1149 &self.common_git_dir
1150 }
1151
1152 fn validate_repository_format_cached(&self) -> Result<()> {
1153 let Some(config_path) = repository_config_path(&self.git_dir) else {
1154 return Ok(());
1155 };
1156 let parsed = self.caches.repository_format(&self.git_dir, &config_path)?;
1157 validate_repository_format_parsed(&parsed)
1158 }
1159
1160 #[must_use]
1169 pub fn refs(&self) -> &dyn crate::refs::store::RefStore {
1170 &self.ref_store
1171 }
1172
1173 #[must_use]
1177 pub(crate) fn refs_on_disk(&self) -> bool {
1178 use crate::refs::store::RefStorageFormat;
1179 matches!(
1180 self.refs().format(),
1181 RefStorageFormat::Files | RefStorageFormat::Reftable
1182 )
1183 }
1184
1185 #[inline]
1187 #[must_use]
1188 pub fn objects(&self) -> crate::objects::Objects<'_> {
1189 crate::objects::Objects::new(self)
1190 }
1191
1192 #[inline]
1194 #[must_use]
1195 pub fn references(&self) -> crate::refs::References<'_> {
1196 crate::refs::References::new(self)
1197 }
1198
1199 #[inline]
1201 #[must_use]
1202 pub fn index_handle(&self) -> crate::index::IndexHandle<'_> {
1203 crate::index::IndexHandle::new(self)
1204 }
1205
1206 #[inline]
1208 #[must_use]
1209 pub fn config_handle(&self) -> crate::config::ConfigHandle<'_> {
1210 crate::config::ConfigHandle::new(self)
1211 }
1212
1213 #[inline]
1215 #[must_use]
1216 pub fn rev_walk(&self) -> crate::revwalk::RevWalk<'_> {
1217 crate::revwalk::RevWalk::new(self)
1218 }
1219
1220 #[inline]
1222 #[must_use]
1223 pub fn diff(&self) -> crate::diff::Diff<'_> {
1224 crate::diff::Diff::new(self)
1225 }
1226
1227 #[must_use]
1229 pub fn remotes(&self) -> crate::repo_handles::RepositoryRemotes<'_> {
1230 crate::repo_handles::RepositoryRemotes::new(self)
1231 }
1232
1233 #[must_use]
1235 pub fn worktrees(&self) -> crate::repo_handles::RepositoryWorktrees<'_> {
1236 crate::repo_handles::RepositoryWorktrees::new(self)
1237 }
1238
1239 #[must_use]
1241 pub fn stash(&self) -> crate::repo_handles::RepositoryStash<'_> {
1242 crate::repo_handles::RepositoryStash::new(self)
1243 }
1244
1245 #[must_use]
1247 pub fn workspace_snapshot(&self) -> crate::repo_handles::RepositoryWorkspaceSnapshot<'_> {
1248 crate::repo_handles::RepositoryWorkspaceSnapshot::new(self)
1249 }
1250
1251 #[must_use]
1253 pub fn notes(&self) -> crate::repo_handles::RepositoryNotes<'_> {
1254 crate::repo_handles::RepositoryNotes::new(self)
1255 }
1256
1257 #[must_use]
1259 pub fn hooks(&self) -> crate::repo_handles::RepositoryHooks<'_> {
1260 crate::repo_handles::RepositoryHooks::new(self)
1261 }
1262
1263 pub fn fsck(
1272 &self,
1273 options: &crate::fsck::FsckOptions,
1274 progress: &mut dyn crate::progress::ProgressSink,
1275 cancel: &dyn crate::progress::Cancel,
1276 ) -> crate::error::Result<crate::fsck::FsckReport> {
1277 crate::fsck::fsck(self, options, progress, cancel)
1278 }
1279
1280 pub fn tree_sha256(
1289 &self,
1290 oid: crate::objects::ObjectId,
1291 options: &crate::tree_sha256::TreeSha256Options,
1292 ) -> crate::error::Result<crate::tree_sha256::TreeSha256Report> {
1293 crate::tree_sha256::tree_sha256(self, oid, options)
1294 }
1295
1296 pub fn verify_tag(
1305 &self,
1306 oid: crate::objects::ObjectId,
1307 options: &crate::tree_sha256::TagVerifyOptions,
1308 ) -> crate::error::Result<crate::tree_sha256::TagVerification> {
1309 crate::tree_sha256::verify_tag(self, oid, options)
1310 }
1311
1312 pub fn patch_id(
1326 &self,
1327 commit: &crate::objects::ObjectId,
1328 mode: crate::patch_ids::PatchIdMode,
1329 ) -> crate::error::Result<Option<crate::objects::ObjectId>> {
1330 crate::patch_ids::compute_patch_id(self.odb().hash_algo(), self.odb(), commit, mode)
1331 }
1332
1333 pub fn patch_ids_for_revs(
1350 &self,
1351 specs: &[&str],
1352 mode: crate::patch_ids::PatchIdMode,
1353 ) -> crate::error::Result<Vec<(crate::objects::ObjectId, crate::objects::ObjectId)>> {
1354 let walk = self.rev_walk();
1355 let walk_options = crate::revwalk::RevWalkListOptions::default();
1356 let mut out = Vec::new();
1357 for spec in specs {
1358 let commits = if crate::rev_parse::is_top_level_range(spec) {
1359 walk.commits(&[spec], &walk_options)?
1360 } else {
1361 vec![crate::rev_parse::resolve_revision_without_index_dwim(
1362 self, spec,
1363 )?]
1364 };
1365 let patch_ids =
1366 crate::rev_list::map_commits_in_parallel(&commits, |c| self.patch_id(c, mode));
1367 for (patch_id, commit) in patch_ids.into_iter().zip(commits) {
1368 if let Some(patch_id) = patch_id? {
1369 out.push((patch_id, commit));
1370 }
1371 }
1372 }
1373 Ok(out)
1374 }
1375
1376 #[must_use]
1378 pub fn reflogs(&self) -> crate::repo_handles::RepositoryReflogs<'_> {
1379 crate::repo_handles::RepositoryReflogs::new(self)
1380 }
1381
1382 #[must_use]
1384 pub fn bundles(&self) -> crate::repo_handles::RepositoryBundles<'_> {
1385 crate::repo_handles::RepositoryBundles::new(self)
1386 }
1387
1388 pub fn repack(
1394 &self,
1395 opts: &crate::maintenance::RepackOptions,
1396 ) -> crate::error::Result<crate::maintenance::RepackReport> {
1397 crate::maintenance::repack(self, opts)
1398 }
1399
1400 pub(crate) fn resolve_ref_oid(&self, refname: &str) -> Result<crate::objects::ObjectId> {
1406 self.refs().resolve(refname)
1407 }
1408
1409 #[must_use]
1414 pub fn with_ref_store(mut self, store: Arc<dyn crate::refs::store::RefStore>) -> Self {
1415 let store = linked_worktree_view(&self.git_dir, store);
1416 self.caches
1417 .install_ref_store(&self.git_dir, Arc::clone(&store));
1418 self.ref_store = checked_ref_store_with_hooks(
1419 store,
1420 self.odb.clone(),
1421 self.git_dir(),
1422 self.work_tree().map(Path::as_ref),
1423 &self.environment,
1424 &self.command_runner,
1425 self.hook_stdio,
1426 &self.diagnostics,
1427 );
1428 self
1429 }
1430
1431 #[must_use]
1433 pub(crate) fn pathspec_precompose_enabled(&self) -> bool {
1434 self.caches
1435 .pathspec_precompose_enabled(self.environment.as_ref(), &self.git_dir)
1436 }
1437
1438 #[cfg(test)]
1441 pub(crate) fn force_pathspec_precompose_for_test(&self) {
1442 self.caches.force_pathspec_precompose_for_test();
1443 }
1444
1445 pub fn resolve_worktree_pathspec(
1459 &self,
1460 pathspec: &str,
1461 prefix: Option<&str>,
1462 ) -> std::result::Result<String, crate::pathspec::PathspecResolveError> {
1463 let work_tree = self
1464 .work_tree()
1465 .ok_or(crate::pathspec::PathspecResolveError::NeedsWorkTree)?;
1466 Ok(
1467 crate::pathspec::resolve_pathspec_in_worktree_with_argv_precompose(
1468 pathspec,
1469 pathspec,
1470 work_tree,
1471 prefix,
1472 self.pathspec_precompose_enabled(),
1473 )?,
1474 )
1475 }
1476
1477 #[must_use]
1478 pub(crate) fn pathspec_match_context(&self) -> crate::pathspec::PathspecMatchContext {
1479 crate::pathspec::pathspec_match_context_with_precompose(self.pathspec_precompose_enabled())
1480 }
1481
1482 pub fn paths_match_pathspecs(&self, path: &str, specs: &[String]) -> Result<bool> {
1492 Ok(self.pathspec_matcher(specs)?.matches(path))
1493 }
1494
1495 pub fn pathspec_matcher<'s>(
1511 &self,
1512 specs: &'s [String],
1513 ) -> Result<crate::pathspec::PathspecMatcher<'s>> {
1514 let precompose = self.pathspec_precompose_enabled();
1515 if !specs
1516 .iter()
1517 .any(|spec| crate::pathspec::pathspec_has_attr_magic(spec))
1518 {
1519 return Ok(crate::pathspec::PathspecMatcher::new(
1520 specs, precompose, None, false,
1521 ));
1522 }
1523 crate::pathspec::validate_attr_pathspecs(specs)?;
1524 let attrs = match self.work_tree() {
1525 Some(work_tree) => self.caches().gitattributes_stack_shared(self, work_tree)?,
1526 None => self.caches().gitattributes_bare_shared(self)?,
1527 };
1528 let icase = crate::crlf::config_ignorecase(self.config()?.as_ref());
1529 Ok(crate::pathspec::PathspecMatcher::new(
1530 specs,
1531 precompose,
1532 Some(attrs),
1533 icase,
1534 ))
1535 }
1536
1537 #[must_use]
1542 pub fn pathspec_cwd_prefix(&self) -> Option<&str> {
1543 self.git_prefix.as_deref().filter(|p| !p.is_empty())
1544 }
1545
1546 pub fn pathspec_argv_prefix<'a>(&'a self, pathspec: &str) -> Option<&'a str> {
1548 if crate::pathspec::pathspec_has_top(pathspec) {
1549 None
1550 } else {
1551 self.pathspec_cwd_prefix()
1552 }
1553 }
1554
1555 pub fn resolve_rev_walk_pathspecs(&self, paths: &[String]) -> Result<Vec<String>> {
1565 if paths.is_empty() {
1566 return Ok(Vec::new());
1567 }
1568 let prefix = self.pathspec_cwd_prefix();
1569 let cwd_for_implicit = prefix.map(str::to_owned);
1570 let mut resolved = Vec::with_capacity(paths.len());
1571 if self.work_tree().is_some() {
1572 for pathspec in paths {
1573 let prefix = Self::pathspec_argv_prefix(self, pathspec);
1574 let spec = self
1575 .resolve_worktree_pathspec(pathspec, prefix)
1576 .map_err(|e| Error::PathError(e.to_string()))?;
1577 resolved.push(spec);
1578 }
1579 } else {
1580 let root = self.git_dir();
1581 for pathspec in paths {
1582 let prefix = Self::pathspec_argv_prefix(self, pathspec);
1583 resolved.push(crate::pathspec::resolve_pathspec(pathspec, root, prefix));
1584 }
1585 }
1586 Ok(crate::pathspec::extend_pathspec_list_implicit_cwd(
1587 &resolved,
1588 cwd_for_implicit.as_deref(),
1589 ))
1590 }
1591
1592 #[must_use]
1599 pub fn git_dir(&self) -> &Path {
1600 &self.git_dir
1601 }
1602
1603 #[must_use]
1610 pub fn work_tree(&self) -> Option<&Path> {
1611 self.work_tree.as_deref()
1612 }
1613
1614 #[must_use]
1616 pub fn odb(&self) -> &Odb {
1617 &self.odb
1618 }
1619
1620 pub fn prefetch_promised(&self, oids: &[ObjectId]) -> Result<()> {
1626 let Some(fetcher) = self.caches().promisor_fetcher() else {
1627 return Err(Error::Message(
1628 "promisor prefetch requires a PromisorFetcher on RepositoryOptions".into(),
1629 ));
1630 };
1631 fetcher.fetch_missing(self, oids)
1632 }
1633
1634 #[must_use]
1636 pub fn discovery_info(&self) -> DiscoveryInfo {
1637 DiscoveryInfo {
1638 explicit_git_dir: self.explicit_git_dir,
1639 discovery_root: self.discovery_root.clone(),
1640 work_tree_from_env: self.work_tree_from_env,
1641 discovery_via_gitfile: self.discovery_via_gitfile,
1642 }
1643 }
1644
1645 fn cached_settings(&self) -> RepoCachedSettings {
1646 if let Ok(guard) = self.config_session.lock() {
1647 if let Some(session) = guard.as_ref() {
1648 return session.cached_settings.clone();
1649 }
1650 }
1651 self.ensure_config_arc_fast()
1652 .map(|cfg| {
1653 Self::repo_cached_settings_from_config(cfg.as_ref(), self.environment.as_ref())
1654 })
1655 .unwrap_or_else(|_| {
1656 Self::repo_cached_settings_from_config(&ConfigSet::new(), self.environment.as_ref())
1657 })
1658 }
1659
1660 pub fn init(path: &Path, options: &InitOptions) -> Result<Self> {
1666 Self::init_with_outcome(path, options).map(|(repo, _)| repo)
1667 }
1668
1669 pub fn init_with_outcome(path: &Path, options: &InitOptions) -> Result<(Self, InitOutcome)> {
1676 if options.bare && options.separate_git_dir.is_some() {
1677 return Err(Error::InitBareWithSeparateGitDir);
1678 }
1679 let env = options
1680 .environment
1681 .clone()
1682 .unwrap_or_else(Environment::empty);
1683 let git_dir = resolve_git_dir_for_init(path, options)?;
1684 let reinitialized = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
1685 let existing_format = hash_algo_from_git_dir(&git_dir)?;
1686 let object_format = match (reinitialized, options.object_format) {
1687 (true, None) => existing_format,
1688 (true, Some(requested)) if requested != existing_format => {
1689 return Err(Error::InitObjectFormatChange);
1690 }
1691 (true, Some(requested)) => requested,
1692 (false, None) => HashAlgo::Sha1,
1693 (false, Some(requested)) => requested,
1694 };
1695 let initial_branch = if reinitialized {
1696 head_branch_name_for_outcome(&git_dir)?
1697 } else {
1698 resolve_initial_branch(&env, options.initial_branch.as_deref())?
1699 };
1700
1701 let separate_layout = options.separate_git_dir.is_some()
1702 || (!options.bare
1703 && !options.bare_with_worktree_at_path
1704 && path.join(".git").is_file());
1705
1706 let repo = if options.bare_with_worktree_at_path {
1707 init_bare_with_env_worktree(
1708 path,
1709 path,
1710 &initial_branch,
1711 options.template_dir.as_deref(),
1712 options.ref_storage,
1713 object_format,
1714 &env,
1715 )?
1716 } else if separate_layout {
1717 init_repository_separate_git_dir(
1718 path,
1719 &git_dir,
1720 &initial_branch,
1721 options.template_dir.as_deref(),
1722 options.ref_storage,
1723 object_format,
1724 &env,
1725 )?
1726 } else {
1727 init_repository_with_object_format(
1728 path,
1729 options.bare,
1730 &initial_branch,
1731 options.template_dir.as_deref(),
1732 options.ref_storage,
1733 object_format,
1734 &env,
1735 )?
1736 };
1737 Ok((
1738 repo,
1739 InitOutcome {
1740 reinitialized,
1741 initial_branch,
1742 },
1743 ))
1744 }
1745
1746 pub fn open(path: &Path, options: &RepositoryOptions) -> Result<Self> {
1756 if options.open_for_serving {
1757 return Self::open_for_serving_internal(path, options);
1758 }
1759 Self::open_internal(options, path)
1760 }
1761
1762 fn open_with_work_tree(
1763 options: &RepositoryOptions,
1764 git_dir: &Path,
1765 work_tree: Option<&Path>,
1766 ) -> Result<Self> {
1767 let mut opts = options.clone();
1768 opts.work_tree = work_tree.map(|p| p.to_path_buf());
1769 Self::open_internal(&opts, git_dir)
1770 }
1771
1772 fn open_internal(options: &RepositoryOptions, git_dir: &Path) -> Result<Self> {
1773 Self::open_internal_with_refs(options, git_dir, None)
1774 }
1775
1776 fn open_internal_with_refs(
1777 options: &RepositoryOptions,
1778 git_dir: &Path,
1779 injected_refs: Option<Arc<dyn crate::refs::store::RefStore>>,
1780 ) -> Result<Self> {
1781 crate::trace::trace_span!("grit.repository.open");
1782 let work_tree = options.work_tree.as_deref();
1783 let git_dir = git_dir
1784 .canonicalize_simplified()
1785 .map_err(|_| not_a_repository_at(git_dir))?;
1786
1787 let repo = Self::from_canonical_git_dir(options, git_dir, work_tree, injected_refs)?;
1788 repo.refresh_config_session()?;
1789 if options.validate_repository_format {
1790 repo.validate_repository_format_cached()?;
1791 }
1792 repo.install_config_snapshot();
1793 warn_core_bare_worktree_conflict(options, &repo);
1794 let _ = trace_repo_setup_if_requested(&repo);
1795 Ok(repo)
1796 }
1797
1798 fn open_for_serving_internal(path: &Path, options: &RepositoryOptions) -> Result<Self> {
1799 let mut with_git_suffix = path.as_os_str().to_owned();
1800 with_git_suffix.push(".git");
1801 let candidates = [
1802 (path.join(".git"), Some(path.to_path_buf())),
1803 (path.to_path_buf(), None),
1804 (PathBuf::from(with_git_suffix), None),
1805 ];
1806 for (git_dir, work_tree) in candidates {
1807 if !git_dir.join("HEAD").is_file() {
1808 continue;
1809 }
1810 let mut opts = options.clone();
1811 opts.work_tree = work_tree;
1812 opts.open_for_serving = false;
1813 return Self::open_internal(&opts, &git_dir);
1814 }
1815 Err(not_a_repository_at(path))
1816 }
1817
1818 fn open_with_odb_internal(
1819 options: &RepositoryOptions,
1820 git_dir: &Path,
1821 mut builder: crate::odb::OdbBuilder,
1822 injected_refs: Option<Arc<dyn crate::refs::store::RefStore>>,
1823 ) -> Result<Self> {
1824 let work_tree = options.work_tree.as_deref();
1825 let environment = Arc::new(options.environment.clone());
1826 let cwd = environment.discovery_cwd();
1827 let command_runner = Arc::clone(&options.command_runner);
1828 let change_id_generator = Arc::clone(&options.change_id_generator);
1829
1830 let fully_injected = injected_refs.is_some() && builder.has_custom_primary();
1833 let requested_git_dir = cwd.join(git_dir);
1834 let git_dir = match requested_git_dir.canonicalize_simplified() {
1835 Ok(dir) => dir,
1836 Err(_) if fully_injected => canonicalize_existing_prefix(&requested_git_dir)?,
1837 Err(_) => return Err(not_a_repository_at(git_dir)),
1838 };
1839
1840 if injected_refs.is_none() && !head_file_present(&git_dir) {
1841 return Err(not_a_repository_at(&git_dir));
1842 }
1843
1844 let common_git_dir = resolve_common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
1845 let objects_dir = resolve_objects_dir(&environment, &git_dir, &common_git_dir)
1846 .ok_or_else(|| not_a_repository_at(&git_dir))?;
1847
1848 if !builder.has_custom_primary() && !objects_dir.exists() {
1850 return Err(not_a_repository_at(&git_dir));
1851 }
1852 let objects_dir = canonicalize_existing_prefix(&objects_dir)?;
1853 let builder_objects = canonicalize_existing_prefix(&cwd.join(builder.objects_dir()))?;
1854 if builder_objects != objects_dir {
1855 return Err(Error::OdbObjectsDirMismatch {
1856 builder: builder_objects,
1857 repository: objects_dir,
1858 });
1859 }
1860 builder.set_objects_dir(objects_dir);
1861 let injected_algos: Vec<HashAlgo> = builder.injected_hash_algos().collect();
1862
1863 let work_tree = match work_tree {
1864 Some(p) => {
1865 let mut resolved = cwd.join(p);
1866 if resolved.exists() {
1867 resolved = resolved
1868 .canonicalize_simplified()
1869 .map_err(|_| Error::PathError(p.display().to_string()))?;
1870 }
1871 Some(resolved)
1872 }
1873 None => None,
1874 };
1875
1876 let caches = RepoCaches::with_diagnostics(
1877 Arc::clone(&command_runner),
1878 Arc::clone(&options.diagnostics),
1879 );
1880 let odb = builder
1881 .build()
1882 .with_config_git_dir(git_dir.clone())
1883 .with_shared_config_state(caches.clone(), environment.clone())
1884 .with_resolved_work_tree(work_tree.clone());
1885
1886 let git_prefix = work_tree
1887 .as_ref()
1888 .and_then(|wt| compute_git_prefix(environment.as_ref(), wt));
1889
1890 let ref_store = checked_ref_store_with_hooks(
1891 open_or_install_ref_store(&caches, &git_dir, &environment, injected_refs)?,
1892 odb.clone(),
1893 &git_dir,
1894 work_tree.as_deref(),
1895 &environment,
1896 &command_runner,
1897 options.hook_stdio,
1898 &options.diagnostics,
1899 );
1900 caches.configure_filter_subprocess(&environment, &git_dir, work_tree.as_deref());
1901
1902 let repo = Self {
1903 git_dir,
1904 common_git_dir,
1905 work_tree,
1906 odb,
1907 explicit_git_dir: false,
1908 discovery_root: None,
1909 work_tree_from_env: false,
1910 discovery_via_gitfile: false,
1911 config_session: Mutex::new(None),
1912 caches,
1913 ref_store,
1914 environment,
1915 git_prefix,
1916 command_runner,
1917 change_id_generator,
1918 diagnostics: Arc::clone(&options.diagnostics),
1919 network_trace: options.network_trace,
1920 reference_unix_time: options.reference_unix_time,
1921 test_assume_different_owner: options.test_assume_different_owner,
1922 thread_policy: options.thread_policy,
1923 hook_stdio: options.hook_stdio,
1924 };
1925 repo.refresh_config_session()?;
1926 if options.validate_repository_format {
1927 repo.validate_repository_format_cached()?;
1928 }
1929 repo.check_injected_object_format(&injected_algos)?;
1930 warn_core_bare_worktree_conflict(options, &repo);
1931 let _ = trace_repo_setup_if_requested(&repo);
1932 Ok(repo)
1933 }
1934
1935 fn check_injected_object_format(&self, injected: &[HashAlgo]) -> Result<()> {
1942 if injected.is_empty() {
1943 return Ok(());
1944 }
1945 let repository = self
1946 .ensure_config_arc_fast()?
1947 .get("extensions.objectformat")
1948 .and_then(|name| HashAlgo::from_name(&name))
1949 .unwrap_or(HashAlgo::Sha1);
1950 match injected.iter().find(|algo| **algo != repository) {
1951 Some(&store) => Err(Error::ObjectFormatMismatch { repository, store }),
1952 None => Ok(()),
1953 }
1954 }
1955
1956 pub fn discover(start: Option<&Path>, options: &RepositoryOptions) -> Result<Self> {
1958 let env = &options.environment;
1959 let cwd = env.discovery_cwd();
1960
1961 if let Some(dir) = env.git_dir.as_deref() {
1963 let mut git_dir = PathBuf::from(dir);
1964 if git_dir.is_relative() {
1965 git_dir = cwd.join(git_dir);
1966 }
1967 git_dir = resolve_git_dir_env_path(&git_dir)?;
1969 let work_tree = env.git_work_tree.as_deref().map(|wt| {
1970 let p = PathBuf::from(wt);
1971 if p.is_absolute() {
1972 p
1973 } else {
1974 cwd.join(p)
1975 }
1976 });
1977 if let Some(ref wt_path) = work_tree {
1978 if env
1979 .git_work_tree
1980 .as_deref()
1981 .is_some_and(|raw| Path::new(raw).is_absolute())
1982 {
1983 validate_git_work_tree_path(wt_path)?;
1984 }
1985 }
1986 if work_tree.is_some() {
1987 let mut repo = Self::open_with_work_tree(options, &git_dir, work_tree.as_deref())?;
1988 repo.explicit_git_dir = true;
1989 repo.discovery_root = None;
1990 repo.work_tree_from_env = false;
1991 repo.discovery_via_gitfile = false;
1992 return Ok(repo);
1993 }
1994 let (is_bare, core_wt) = read_core_bare_and_worktree(&git_dir)?;
1996 let bare_worktree_conflict = is_bare && core_wt.is_some();
1997 let resolved_wt = if is_bare {
1998 None
1999 } else if let Some(raw) = core_wt {
2000 Some(resolve_core_worktree_path(&git_dir, &raw)?)
2001 } else {
2002 Some(
2008 cwd.canonicalize_simplified()
2009 .unwrap_or_else(|_| cwd.clone()),
2010 )
2011 };
2012 let mut repo = Self::open_with_work_tree(options, &git_dir, resolved_wt.as_deref())?;
2013 repo.explicit_git_dir = true;
2014 repo.discovery_root = None;
2015 repo.work_tree_from_env = false;
2016 repo.discovery_via_gitfile = false;
2017 let _ = bare_worktree_conflict;
2018 return Ok(repo);
2019 }
2020
2021 let env_work_tree = env.git_work_tree.as_deref().map(|wt| {
2024 let p = PathBuf::from(wt);
2025 if p.is_absolute() {
2026 p
2027 } else {
2028 cwd.join(p)
2029 }
2030 });
2031 if let Some(ref p) = env_work_tree {
2032 if env
2033 .git_work_tree
2034 .as_deref()
2035 .is_some_and(|raw| Path::new(raw).is_absolute())
2036 {
2037 validate_git_work_tree_path(p)?;
2038 }
2039 }
2040 let start = start.unwrap_or(&cwd);
2041 let start = if start.is_absolute() {
2042 start.to_path_buf()
2043 } else {
2044 cwd.join(start)
2045 };
2046
2047 let (ceiling_paths, no_resolve_ceilings) = parse_ceiling_directories(env);
2051 let ceiling_dirs: Vec<String> = ceiling_paths
2052 .into_iter()
2053 .map(|p| path_for_ceiling_compare(&p))
2054 .collect();
2055
2056 let start_canon = start
2057 .canonicalize_simplified()
2058 .unwrap_or_else(|_| start.clone());
2059 let ceil_cmp_buf = if no_resolve_ceilings {
2061 path_for_ceiling_compare(&start)
2062 } else {
2063 path_for_ceiling_compare(&start_canon)
2064 };
2065 let mut dir_buf = path_for_ceiling_compare(&start_canon);
2066 let min_offset = offset_1st_component(&dir_buf);
2067 let mut ceil_offset: isize = longest_ancestor_length(&ceil_cmp_buf, &ceiling_dirs)
2068 .map(|n| n as isize)
2069 .unwrap_or(-1);
2070 if ceiling_dirs.contains(&dir_buf) {
2071 ceil_offset = dir_buf.len() as isize;
2072 } else if ceil_offset < 0 {
2073 ceil_offset = min_offset as isize - 2;
2074 }
2075
2076 loop {
2077 let current = Path::new(&dir_buf);
2078 if let Some(DiscoveredAt { mut repo, gitfile }) = try_open_at(options, current)? {
2079 repo.environment = Arc::new(env.clone());
2085 repo.refresh_config_session()?;
2086 repo.validate_repository_format_cached()?;
2087 repo.install_config_snapshot();
2088 let cfg = repo.config()?;
2089 repo.set_diagnostics(Arc::clone(&options.diagnostics));
2090 if let Some(ref wt) = env_work_tree {
2091 repo.work_tree =
2092 Some(wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone()));
2093 repo.work_tree_from_env = true;
2094 } else {
2095 repo.work_tree_from_env = false;
2096 let linked_gitfile =
2101 repo.discovery_via_gitfile && resolve_common_dir(&repo.git_dir).is_some();
2102 if !linked_gitfile {
2103 let (is_bare, core_wt) =
2104 read_core_bare_and_worktree_from_config(cfg.as_ref());
2105 if is_bare {
2106 repo.work_tree = None;
2107 } else if let Some(raw) = core_wt {
2108 repo.work_tree = Some(resolve_core_worktree_path(&repo.git_dir, &raw)?);
2109 }
2110 }
2111 }
2112 let assume_different =
2113 options.test_assume_different_owner || env.test_assume_different_owner();
2114 if assume_different {
2115 repo.enforce_safe_directory()?;
2116 } else {
2117 ensure_valid_ownership(
2118 env,
2119 gitfile.as_deref(),
2120 repo.work_tree.as_deref(),
2121 &repo.git_dir,
2122 )?;
2123 }
2124 if let Some(wt) = repo.work_tree.as_ref() {
2125 repo.git_prefix = compute_git_prefix(env, wt);
2126 }
2127 let _ = trace_repo_setup_if_requested(&repo);
2128 return Ok(repo);
2129 }
2130
2131 let mut offset: isize = dir_buf.len() as isize;
2132 if offset <= min_offset as isize {
2133 break;
2134 }
2135 loop {
2136 offset -= 1;
2137 if offset <= ceil_offset {
2138 break;
2139 }
2140 if dir_buf
2141 .as_bytes()
2142 .get(offset as usize)
2143 .is_some_and(|b| *b == b'/')
2144 {
2145 break;
2146 }
2147 }
2148 if offset <= ceil_offset {
2149 break;
2150 }
2151 let off_u = offset as usize;
2152 let new_len = if off_u > min_offset {
2153 off_u
2154 } else {
2155 min_offset
2156 };
2157 dir_buf.truncate(new_len);
2158 }
2159
2160 Err(not_a_repository_at(&start))
2161 }
2162
2163 #[must_use]
2165 pub(crate) fn index_path(&self) -> PathBuf {
2166 self.git_dir.join("index")
2167 }
2168
2169 pub(crate) fn index_path_for_env(&self) -> Result<PathBuf> {
2173 if let Some(raw) = self.environment.git_index_file.as_deref() {
2174 if !raw.is_empty() {
2175 let p = PathBuf::from(raw);
2176 return Ok(if p.is_absolute() {
2177 p
2178 } else {
2179 self.environment.discovery_cwd().join(p)
2180 });
2181 }
2182 }
2183 Ok(self.index_path())
2184 }
2185
2186 pub(crate) fn load_index(&self) -> Result<Index> {
2190 let path = self.index_path_for_env()?;
2191 self.load_index_at(&path)
2192 }
2193
2194 pub(crate) fn load_index_at(&self, path: &std::path::Path) -> Result<Index> {
2197 self.load_index_at_with_context(
2198 path,
2199 crate::index::IndexParseContext::from_policy(self.thread_policy()),
2200 )
2201 }
2202
2203 pub(crate) fn load_index_at_with_context(
2204 &self,
2205 path: &std::path::Path,
2206 parse_ctx: crate::index::IndexParseContext,
2207 ) -> Result<Index> {
2208 let mut idx = self.load_index_at_preserving_sparse(path, parse_ctx)?;
2209 let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
2210 idx.expand_sparse_directory_placeholders(
2211 &self.odb,
2212 crate::path_validate::ProtectOptions::from_config(cfg.as_ref()),
2213 )?;
2214 self.clear_present_skip_worktree(&mut idx, cfg.as_ref());
2215 Ok(idx)
2216 }
2217
2218 pub(crate) fn load_index_preserving_sparse(&self) -> Result<Index> {
2220 self.load_index_at_preserving_sparse(
2221 &self.index_path_for_env()?,
2222 crate::index::IndexParseContext::from_policy(self.thread_policy()),
2223 )
2224 }
2225
2226 pub(crate) fn load_index_at_preserving_sparse(
2229 &self,
2230 path: &std::path::Path,
2231 parse_ctx: crate::index::IndexParseContext,
2232 ) -> Result<Index> {
2233 crate::trace::trace_span!("grit.index.read");
2234 let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
2235 if let Some(res) = cfg.as_ref().get_bool("index.sparse") {
2236 res.map_err(|s| Error::Config(s.into()))?;
2237 }
2238 let mut idx = Index::load_preserving_sparse_optional_with_context(
2239 path,
2240 &self.odb,
2241 Some(cfg.as_ref()),
2242 parse_ctx,
2243 )?;
2244 crate::split_index::resolve_split_index_if_needed(&mut idx, &self.git_dir, path)?;
2245 if idx.source_mtime.is_none() {
2246 idx.source_mtime = crate::index::index_file_mtime(path);
2247 }
2248 Ok(idx)
2249 }
2250
2251 pub(crate) fn clear_present_skip_worktree(&self, index: &mut Index, cfg: &ConfigSet) {
2252 if let Some(ref wt) = self.work_tree {
2253 crate::sparse_checkout::clear_skip_worktree_from_present_files(
2254 &self.git_dir,
2255 wt,
2256 index,
2257 Some(cfg),
2258 );
2259 }
2260 }
2261
2262 pub(crate) fn write_index(&self, index: &mut Index) -> Result<()> {
2265 self.write_index_with_post_index_change(index, false, false)
2266 }
2267
2268 pub(crate) fn begin_index_update(&self) -> Result<crate::index::IndexLock> {
2277 test_inject_index_write_fail()?;
2278 let _ = crate::porcelain::commit::reap_stale_next_index_files(self, false)?;
2279 let path = self.index_path_for_env()?;
2280 let cfg = self.config()?;
2281 crate::index::IndexLock::acquire(&path, Some(cfg.as_ref()))
2282 }
2283
2284 pub(crate) fn commit_index_update(
2290 &self,
2291 lock: &mut crate::index::IndexLock,
2292 index: &mut Index,
2293 updated_workdir: bool,
2294 ) -> Result<()> {
2295 self.flush_index_update(lock, index)?;
2296 self.finish_index_update(lock, updated_workdir)
2297 }
2298
2299 pub(crate) fn flush_index_update(
2301 &self,
2302 lock: &mut crate::index::IndexLock,
2303 index: &mut Index,
2304 ) -> Result<()> {
2305 let path = lock.index_path.clone();
2306 let (skip_hash, cfg) = self.prepare_index_for_disk_write(&path, index)?;
2307 write_index_file_split(
2308 &path,
2309 &self.git_dir,
2310 index,
2311 cfg.as_ref(),
2312 WriteSplitIndexRequest::default(),
2313 skip_hash,
2314 self.thread_policy(),
2315 Some(lock),
2316 false,
2317 )
2318 }
2319
2320 pub(crate) fn finish_index_update(
2322 &self,
2323 lock: &mut crate::index::IndexLock,
2324 updated_workdir: bool,
2325 ) -> Result<()> {
2326 lock.commit_replace_index()?;
2327 let updated_workdir_arg = if updated_workdir { "1" } else { "0" };
2328 let _ = run_hook(self, "post-index-change", &[updated_workdir_arg, "0"], None);
2329 Ok(())
2330 }
2331
2332 pub fn write_largest_pack_bitmap(
2339 &self,
2340 options: &crate::pack_bitmap::PackBitmapWriteOptions,
2341 now: std::time::SystemTime,
2342 ) -> std::result::Result<std::path::PathBuf, crate::pack_bitmap::PackBitmapWriteError> {
2343 let path = self
2344 .odb
2345 .largest_pack_index_path()
2346 .map_err(|e| crate::pack_bitmap::PackBitmapWriteError::Object(e.to_string()))?
2347 .ok_or(crate::pack_bitmap::PackBitmapWriteError::InvalidPackPath(
2348 "no pack index in object store".to_owned(),
2349 ))?;
2350 crate::pack_bitmap::PackBitmapWriter::write(self, &path, options, now)
2351 }
2352
2353 pub(crate) fn try_write_index(&self, index: &mut Index) -> Result<bool> {
2366 self.try_write_index_at(&self.index_path_for_env()?, index)
2367 }
2368
2369 pub(crate) fn try_write_index_at(&self, path: &Path, index: &mut Index) -> Result<bool> {
2372 if !self.environment().git_optional_locks {
2373 return Ok(false);
2374 }
2375 let cfg = self.config().ok();
2376 let mut lock = match crate::index::IndexLock::acquire(path, cfg.as_deref()) {
2377 Ok(lock) => lock,
2378 Err(e) if Self::opportunistic_index_write_skippable(&e) => return Ok(false),
2379 Err(e) => return Err(e),
2380 };
2381 let on_disk = crate::index::read_index_on_disk_identity(path, index.hash_algo)?;
2382 if !crate::index::opportunistic_index_write_still_current(index, &on_disk) {
2383 return Ok(false);
2384 }
2385 match self.write_index_with_lock(&mut lock, index, false) {
2386 Ok(()) => Ok(true),
2387 Err(e) if Self::opportunistic_index_write_skippable(&e) => Ok(false),
2388 Err(e) => Err(e),
2389 }
2390 }
2391
2392 fn opportunistic_index_write_skippable(err: &crate::error::Error) -> bool {
2393 use crate::error::Error;
2394 match err {
2395 Error::LockHeld { .. } => true,
2396 Error::Io(e) => matches!(
2397 e.kind(),
2398 std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::ReadOnlyFilesystem
2399 ),
2400 _ => false,
2401 }
2402 }
2403
2404 pub(crate) fn write_index_with_post_index_change(
2415 &self,
2416 index: &mut Index,
2417 updated_workdir: bool,
2418 updated_skipworktree: bool,
2419 ) -> Result<()> {
2420 let path = self.index_path_for_env()?;
2421 self.write_index_at_with_post_index_change(
2422 &path,
2423 index,
2424 updated_workdir,
2425 updated_skipworktree,
2426 )
2427 }
2428
2429 pub(crate) fn write_index_at(&self, path: &std::path::Path, index: &mut Index) -> Result<()> {
2431 self.write_index_at_with_post_index_change(path, index, false, false)
2432 }
2433
2434 pub(crate) fn write_index_at_with_split(
2436 &self,
2437 path: &std::path::Path,
2438 index: &mut Index,
2439 split: WriteSplitIndexRequest,
2440 ) -> Result<()> {
2441 self.write_index_at_split(path, index, split)
2442 }
2443
2444 pub(crate) fn write_index_at_with_post_index_change(
2459 &self,
2460 path: &std::path::Path,
2461 index: &mut Index,
2462 updated_workdir: bool,
2463 updated_skipworktree: bool,
2464 ) -> Result<()> {
2465 self.write_index_at_split_with_post_index_change(
2466 path,
2467 index,
2468 WriteSplitIndexRequest::default(),
2469 updated_workdir,
2470 updated_skipworktree,
2471 )
2472 }
2473
2474 pub(crate) fn write_index_at_split(
2476 &self,
2477 path: &std::path::Path,
2478 index: &mut Index,
2479 split: WriteSplitIndexRequest,
2480 ) -> Result<()> {
2481 self.write_index_at_split_with_post_index_change(path, index, split, false, false)
2482 }
2483
2484 pub(crate) fn write_index_at_split_with_post_index_change(
2497 &self,
2498 path: &std::path::Path,
2499 index: &mut Index,
2500 split: WriteSplitIndexRequest,
2501 updated_workdir: bool,
2502 updated_skipworktree: bool,
2503 ) -> Result<()> {
2504 self.write_index_file(
2505 path,
2506 index,
2507 split,
2508 updated_workdir,
2509 updated_skipworktree,
2510 None,
2511 )
2512 }
2513
2514 pub(crate) fn write_index_with_lock(
2526 &self,
2527 lock: &mut crate::index::IndexLock,
2528 index: &mut Index,
2529 updated_workdir: bool,
2530 ) -> Result<()> {
2531 let path = lock.index_path.clone();
2532 self.write_index_file(
2533 &path,
2534 index,
2535 WriteSplitIndexRequest::default(),
2536 updated_workdir,
2537 false,
2538 Some(lock),
2539 )
2540 }
2541
2542 pub(crate) fn lock_index(&self) -> Result<crate::index::IndexLock> {
2554 let path = self.index_path_for_env()?;
2555 let cfg = self.config()?;
2556 crate::index::IndexLock::acquire(&path, Some(cfg.as_ref()))
2557 }
2558
2559 pub(crate) fn lock_and_load_index(&self) -> Result<(crate::index::IndexLock, Index)> {
2574 let lock = self.lock_index()?;
2575 let index = self.load_index_at(&lock.index_path)?;
2576 Ok((lock, index))
2577 }
2578
2579 pub(crate) fn lock_and_load_index_preserving_sparse(
2581 &self,
2582 ) -> Result<(crate::index::IndexLock, Index)> {
2583 let lock = self.lock_index()?;
2584 let index = self.load_index_at_preserving_sparse(
2585 &lock.index_path,
2586 crate::index::IndexParseContext::from_policy(self.thread_policy()),
2587 )?;
2588 Ok((lock, index))
2589 }
2590
2591 fn prepare_index_for_disk_write(
2593 &self,
2594 path: &std::path::Path,
2595 index: &mut Index,
2596 ) -> Result<(bool, Arc<ConfigSet>)> {
2597 test_inject_index_write_fail()?;
2598 index.hash_algo = self.odb.hash_algo();
2603 self.finalize_sparse_index_if_needed(index)?;
2604 let prev_index_mtime = crate::index::index_file_mtime(path);
2605 let cfg = self.config()?;
2606 if let Some(work_tree) = self.work_tree.as_deref() {
2607 if !index.suppress_pre_write_index_housekeeping {
2608 crate::untracked_cache::ensure_untracked_cache_shell(
2609 index,
2610 work_tree,
2611 cfg.as_ref(),
2612 );
2613 if !index.skip_racy_smudge_on_write {
2614 crate::diff::smudge_racily_clean_entries(
2615 &self.odb,
2616 &self.git_dir,
2617 index,
2618 work_tree,
2619 prev_index_mtime,
2620 Some(cfg.as_ref()),
2621 Some(self.caches().filters()),
2622 );
2623 }
2624 }
2625 }
2626 index.clear_content_verified();
2627 let skip_hash = crate::index::index_skip_hash_for_write(Some(cfg.as_ref()));
2628 Ok((skip_hash, cfg))
2629 }
2630
2631 fn write_index_file(
2632 &self,
2633 path: &std::path::Path,
2634 index: &mut Index,
2635 split: WriteSplitIndexRequest,
2636 updated_workdir: bool,
2637 updated_skipworktree: bool,
2638 existing_lock: Option<&mut crate::index::IndexLock>,
2639 ) -> Result<()> {
2640 crate::trace::trace_span!("grit.index.write");
2641 let (skip_hash, cfg) = self.prepare_index_for_disk_write(path, index)?;
2642 write_index_file_split(
2643 path,
2644 &self.git_dir,
2645 index,
2646 cfg.as_ref(),
2647 split,
2648 skip_hash,
2649 self.thread_policy(),
2650 existing_lock,
2651 true,
2652 )
2653 .map_err(|err| map_index_write_error(&self.git_dir, path, err))?;
2654 let updated_workdir_arg = if updated_workdir { "1" } else { "0" };
2656 let updated_skipworktree_arg = if updated_skipworktree { "1" } else { "0" };
2657 let _ = run_hook(
2658 self,
2659 "post-index-change",
2660 &[updated_workdir_arg, updated_skipworktree_arg],
2661 None,
2662 );
2663 Ok(())
2664 }
2665
2666 fn finalize_sparse_index_if_needed(&self, index: &mut Index) -> Result<()> {
2667 let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
2668 let sparse_enabled = cfg
2669 .get("core.sparseCheckout")
2670 .map(|v| v == "true")
2671 .unwrap_or(false);
2672 if !sparse_enabled {
2673 index.sparse_directories = false;
2674 return Ok(());
2675 }
2676 let cone_cfg = cfg
2677 .get("core.sparseCheckoutCone")
2678 .and_then(|v| v.parse::<bool>().ok())
2679 .unwrap_or(true);
2680 let sparse_ix = cfg
2681 .get("index.sparse")
2682 .map(|v| v == "true")
2683 .unwrap_or(false);
2684 let patterns = read_sparse_checkout_patterns(&self.git_dir);
2685 let cone = effective_cone_mode_for_sparse_file(cone_cfg, &patterns);
2686 let tree_oid = match index.cache_tree_root {
2689 Some(tree) => Some(tree),
2690 None => crate::state::resolve_head_for_repository(self)?.tree(&self.odb)?,
2691 };
2692 if let Some(t) = tree_oid {
2693 let protect = crate::path_validate::ProtectOptions::from_config(cfg.as_ref());
2694 index.try_collapse_sparse_directories(
2695 &self.odb, &t, &patterns, cone, sparse_ix, protect,
2696 )?;
2697 } else {
2698 index.sparse_directories = false;
2699 }
2700 Ok(())
2701 }
2702
2703 #[must_use]
2708 pub(crate) fn bloom_pathspec_cwd(&self) -> Option<String> {
2709 let wt = self.work_tree.as_ref()?;
2710 let cwd = self.environment.discovery_cwd();
2711 let wt = wt.canonicalize_simplified().ok()?;
2712 let cwd = cwd.canonicalize_simplified().ok()?;
2713 let rel = cwd.strip_prefix(&wt).ok()?;
2714 let s = rel.to_string_lossy().replace('\\', "/");
2715 let s = s.trim_start_matches('/').to_string();
2716 Some(s)
2717 }
2718
2719 #[must_use]
2721 pub fn is_bare(&self) -> bool {
2722 if let Ok(cfg) = self.config() {
2723 if let Some(Ok(bare)) = cfg.get_bool("core.bare") {
2724 return bare;
2725 }
2726 }
2727 self.work_tree.is_none()
2728 }
2729
2730 pub(crate) fn replace_ref_base_if_enabled(&self) -> Option<String> {
2734 if self.environment.git_no_replace_objects {
2735 return None;
2736 }
2737 let settings = self.cached_settings();
2738 settings
2739 .use_replace_refs
2740 .then_some(settings.replace_ref_base)
2741 }
2742
2743 pub(crate) fn object_id_after_replace(
2745 &self,
2746 oid: &crate::objects::ObjectId,
2747 ) -> crate::objects::ObjectId {
2748 self.resolve_replace_redirect(oid).unwrap_or(*oid)
2749 }
2750
2751 pub(crate) fn read_info_replaced(
2753 &self,
2754 oid: &crate::objects::ObjectId,
2755 ) -> Result<crate::objects::ObjectInfo> {
2756 let effective = self.object_id_after_replace(oid);
2757 self.odb.read_info(&effective)
2758 }
2759
2760 pub(crate) fn read_replaced(
2767 &self,
2768 oid: &crate::objects::ObjectId,
2769 ) -> Result<crate::objects::Object> {
2770 let effective = self.object_id_after_replace(oid);
2771 self.odb.read(&effective)
2772 }
2773
2774 fn resolve_replace_redirect(
2775 &self,
2776 oid: &crate::objects::ObjectId,
2777 ) -> Option<crate::objects::ObjectId> {
2778 if self.environment.git_no_replace_objects {
2779 return None;
2780 }
2781 if let Ok(guard) = self.config_session.lock() {
2782 if guard.as_ref().is_some_and(|s| s.replace_lookup_inactive) {
2783 return None;
2784 }
2785 }
2786 let settings = self.cached_settings();
2787 if !settings.use_replace_refs {
2788 return None;
2789 }
2790 let replace_ref =
2791 self.common_git_dir
2792 .join(format!("{}{}", settings.replace_ref_base, oid.to_hex()));
2793 if !replace_ref.is_file() {
2794 return None;
2795 }
2796 let content = std::fs::read_to_string(&replace_ref).ok()?;
2797 let hex = content.trim();
2798 let replacement_oid = hex.parse::<crate::objects::ObjectId>().ok()?;
2799 if self.odb.read_info(&replacement_oid).is_ok() {
2800 Some(replacement_oid)
2801 } else {
2802 None
2803 }
2804 }
2805}
2806
2807pub(crate) fn trace_repo_setup_if_requested(repo: &Repository) -> std::io::Result<()> {
2812 let Some(path) = repo.environment.git_trace_setup.as_deref() else {
2813 return Ok(());
2814 };
2815 if path.is_empty() || path == "0" {
2816 return Ok(());
2817 }
2818 let trace_path = Path::new(path);
2819 if !trace_path.is_absolute() {
2820 return Ok(());
2821 }
2822
2823 let actual_cwd = repo.environment.discovery_cwd();
2824 let actual_cwd = actual_cwd
2825 .canonicalize_simplified()
2826 .unwrap_or_else(|_| actual_cwd.clone());
2827
2828 let (trace_cwd, prefix) = if let Some(ref wt) = repo.work_tree {
2831 let wt_canon = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
2832 if actual_cwd.starts_with(&wt_canon) {
2833 let rel = actual_cwd
2834 .strip_prefix(&wt_canon)
2835 .map(|p| p.to_path_buf())
2836 .unwrap_or_default();
2837 let prefix = if rel.as_os_str().is_empty() {
2838 "(null)".to_owned()
2839 } else {
2840 let mut s = rel.to_string_lossy().replace('\\', "/");
2841 if !s.ends_with('/') {
2842 s.push('/');
2843 }
2844 s
2845 };
2846 (wt_canon, prefix)
2847 } else {
2848 (actual_cwd.clone(), "(null)".to_owned())
2849 }
2850 } else {
2851 (actual_cwd.clone(), "(null)".to_owned())
2852 };
2853
2854 let git_dir_display =
2855 display_git_dir_for_setup_trace(repo, &trace_cwd, &actual_cwd, prefix.as_str());
2856 let common_display = display_common_dir_for_setup_trace(
2857 repo,
2858 &trace_cwd,
2859 &actual_cwd,
2860 prefix.as_str(),
2861 &git_dir_display,
2862 );
2863 let worktree_display = repo
2864 .work_tree
2865 .as_ref()
2866 .map(|p| {
2867 p.canonicalize_simplified()
2868 .unwrap_or_else(|_| lexical_normalize_path(p))
2869 .display()
2870 .to_string()
2871 })
2872 .unwrap_or_else(|| "(null)".to_owned());
2873
2874 let mut f = OpenOptions::new()
2875 .create(true)
2876 .append(true)
2877 .open(trace_path)?;
2878 writeln!(f, "setup: git_dir: {git_dir_display}")?;
2879 writeln!(f, "setup: git_common_dir: {common_display}")?;
2880 writeln!(f, "setup: worktree: {worktree_display}")?;
2881 writeln!(f, "setup: cwd: {}", trace_cwd.display())?;
2882 writeln!(f, "setup: prefix: {prefix}")?;
2883 Ok(())
2884}
2885
2886fn lexical_normalize_path(path: &Path) -> PathBuf {
2888 let mut out = PathBuf::new();
2889 let mut absolute = false;
2890 for c in path.components() {
2891 match c {
2892 Component::Prefix(p) => {
2893 out.push(p.as_os_str());
2894 }
2895 Component::RootDir => {
2896 absolute = true;
2897 out.push(c.as_os_str());
2898 }
2899 Component::CurDir => {}
2900 Component::ParentDir => {
2901 if absolute {
2902 let _ = out.pop();
2903 } else if !out.pop() {
2904 out.push("..");
2905 }
2906 }
2907 Component::Normal(s) => out.push(s),
2908 }
2909 }
2910 if out.as_os_str().is_empty() {
2911 PathBuf::from(".")
2912 } else {
2913 out
2914 }
2915}
2916
2917fn path_relative_to(target: &Path, base: &Path) -> Option<PathBuf> {
2919 let t = target.canonicalize_simplified().ok()?;
2920 let b = base.canonicalize_simplified().ok()?;
2921 let tc: Vec<_> = t.components().collect();
2922 let bc: Vec<_> = b.components().collect();
2923 let mut i = 0usize;
2924 while i < tc.len() && i < bc.len() && tc[i] == bc[i] {
2925 i += 1;
2926 }
2927 let up = bc.len().saturating_sub(i);
2928 let mut out = PathBuf::new();
2929 for _ in 0..up {
2930 out.push("..");
2931 }
2932 for comp in &tc[i..] {
2933 out.push(comp.as_os_str());
2934 }
2935 Some(out)
2936}
2937
2938fn rel_path_for_setup_trace(target: &Path, trace_cwd: &Path) -> String {
2939 let t = target
2940 .canonicalize_simplified()
2941 .unwrap_or_else(|_| target.to_path_buf());
2942 let tc = trace_cwd
2943 .canonicalize_simplified()
2944 .unwrap_or_else(|_| trace_cwd.to_path_buf());
2945 if let Some(rel) = path_relative_to(&t, &tc) {
2946 let s = rel.to_string_lossy().replace('\\', "/");
2947 return if s.is_empty() || s == "." {
2948 ".".to_owned()
2949 } else {
2950 s
2951 };
2952 }
2953 t.display().to_string()
2954}
2955
2956fn trace_cwd_strictly_inside_git_parent(trace_cwd: &Path, git_dir: &Path) -> bool {
2957 let tc = trace_cwd
2958 .canonicalize_simplified()
2959 .unwrap_or_else(|_| trace_cwd.to_path_buf());
2960 let gd = git_dir
2961 .canonicalize_simplified()
2962 .unwrap_or_else(|_| git_dir.to_path_buf());
2963 let Some(parent) = gd.parent() else {
2964 return false;
2965 };
2966 let parent = parent.to_path_buf();
2967 if tc == parent {
2968 return false;
2969 }
2970 tc.starts_with(&parent) && tc != parent
2971}
2972
2973fn display_git_dir_for_setup_trace(
2974 repo: &Repository,
2975 trace_cwd: &Path,
2976 actual_cwd: &Path,
2977 setup_prefix: &str,
2978) -> String {
2979 let gd = repo
2980 .git_dir
2981 .canonicalize_simplified()
2982 .unwrap_or_else(|_| repo.git_dir.clone());
2983 let tc = trace_cwd
2984 .canonicalize_simplified()
2985 .unwrap_or_else(|_| trace_cwd.to_path_buf());
2986 let ac = actual_cwd
2987 .canonicalize_simplified()
2988 .unwrap_or_else(|_| actual_cwd.to_path_buf());
2989
2990 if repo.work_tree.is_none() && !repo.explicit_git_dir {
2993 if ac == gd {
2994 return ".".to_owned();
2995 }
2996 if ac.starts_with(&gd) && ac != gd {
2997 return gd.display().to_string();
2998 }
2999 }
3000
3001 if !repo.explicit_git_dir {
3003 if let Some(wt) = &repo.work_tree {
3004 let wt = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3005 if ac.starts_with(&gd) && ac != wt {
3006 return gd.display().to_string();
3007 }
3008 }
3009 }
3010
3011 if repo.explicit_git_dir {
3015 if repo.work_tree.is_none() {
3016 if let Some(raw) = repo.environment.git_dir.as_deref() {
3017 let p = Path::new(raw.trim());
3018 if p.is_absolute() {
3019 return gd.display().to_string();
3020 }
3021 let joined = ac.join(p);
3022 if joined.is_file() {
3023 return gd.display().to_string();
3024 }
3025 if let Some(rel) = path_relative_to(&gd, &tc) {
3026 let s = rel.to_string_lossy().replace('\\', "/");
3027 return if s.is_empty() || s == "." {
3028 ".".to_owned()
3029 } else {
3030 s
3031 };
3032 }
3033 }
3034 return gd.display().to_string();
3035 }
3036 if let Some(wt) = &repo.work_tree {
3037 let wt = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3038 let strictly_inside_wt = ac.starts_with(&wt) && ac != wt;
3039 if strictly_inside_wt {
3040 return gd.display().to_string();
3041 }
3042 if let Some(raw) = repo.environment.git_dir.as_deref() {
3043 let p = Path::new(raw.trim());
3044 if p.is_relative() {
3045 let joined = ac.join(p);
3046 if joined.is_file() {
3047 return gd.display().to_string();
3049 }
3050 if let Some(rel) = path_relative_to(&gd, &tc) {
3051 let s = rel.to_string_lossy().replace('\\', "/");
3052 return if s.is_empty() || s == "." {
3053 ".".to_owned()
3054 } else {
3055 s
3056 };
3057 }
3058 }
3059 return gd.display().to_string();
3060 }
3061 }
3062 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
3063 return rel_path_for_setup_trace(&gd, trace_cwd);
3064 }
3065 return gd.display().to_string();
3066 }
3067
3068 let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
3069 (Some(root), Some(wt)) if !repo.work_tree_from_env => {
3070 let r = root
3071 .canonicalize_simplified()
3072 .unwrap_or_else(|_| root.clone());
3073 let w = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3074 r != w
3075 }
3076 _ => false,
3077 };
3078
3079 if repo.work_tree_from_env {
3080 if !repo.discovery_via_gitfile {
3081 if setup_prefix == "(null)" {
3082 if let (Some(root), Some(wt)) = (&repo.discovery_root, &repo.work_tree) {
3083 let r = root
3084 .canonicalize_simplified()
3085 .unwrap_or_else(|_| root.clone());
3086 let w = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3087 if r == w {
3088 let dot_git = r.join(".git");
3089 let dot_git = dot_git.canonicalize_simplified().unwrap_or(dot_git);
3090 if gd == dot_git {
3091 return ".git".to_owned();
3092 }
3093 }
3094 }
3095 }
3096 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
3097 return rel_path_for_setup_trace(&gd, trace_cwd);
3098 }
3099 }
3100 return gd.display().to_string();
3101 }
3102
3103 if work_relocated {
3104 if let Some(wt) = &repo.work_tree {
3105 let wt = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3106 if ac == wt {
3107 return gd.display().to_string();
3108 }
3109 let inside_wt = ac.starts_with(&wt) && ac != wt;
3110 if inside_wt {
3111 if let Some(rel) = path_relative_to(&gd, &ac) {
3112 let s = rel.to_string_lossy().replace('\\', "/");
3113 return if s.is_empty() || s == "." {
3114 ".".to_owned()
3115 } else {
3116 s
3117 };
3118 }
3119 }
3120 }
3121 }
3122 if repo.work_tree.is_some() {
3123 if let Some(root) = &repo.discovery_root {
3124 let r = root
3125 .canonicalize_simplified()
3126 .unwrap_or_else(|_| root.clone());
3127 let dot_git = r.join(".git");
3128 let dot_git = dot_git.canonicalize_simplified().unwrap_or(dot_git);
3129 if gd == dot_git {
3130 return ".git".to_owned();
3131 }
3132 } else if let Some(wt) = &repo.work_tree {
3133 let wt = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3134 let dot_git = wt.join(".git");
3135 let dot_git = dot_git.canonicalize_simplified().unwrap_or(dot_git);
3136 if gd == dot_git {
3137 return ".git".to_owned();
3138 }
3139 }
3140 }
3141
3142 if repo.discovery_via_gitfile && !repo.explicit_git_dir {
3143 return gd.display().to_string();
3144 }
3145
3146 if repo.work_tree.is_none() && !repo.explicit_git_dir {
3150 if let Some(gp) = gd.parent() {
3151 let gp = gp
3152 .canonicalize_simplified()
3153 .unwrap_or_else(|_| gp.to_path_buf());
3154 let gdc = gd.canonicalize_simplified().unwrap_or_else(|_| gd.clone());
3155 if tc.starts_with(&gp) && tc != gp && !tc.starts_with(&gdc) {
3156 return gdc.display().to_string();
3157 }
3158 if tc == gp {
3159 return rel_path_for_setup_trace(&gd, trace_cwd);
3160 }
3161 }
3162 }
3163
3164 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
3165 rel_path_for_setup_trace(&gd, trace_cwd)
3166 } else {
3167 gd.display().to_string()
3168 }
3169}
3170
3171fn display_common_dir_for_setup_trace(
3172 repo: &Repository,
3173 trace_cwd: &Path,
3174 actual_cwd: &Path,
3175 _setup_prefix: &str,
3176 git_dir_display: &str,
3177) -> String {
3178 let gd = repo
3179 .git_dir
3180 .canonicalize_simplified()
3181 .unwrap_or_else(|_| repo.git_dir.clone());
3182 let Some(common) = resolve_common_dir(&gd) else {
3183 return git_dir_display.to_owned();
3184 };
3185 let common = common.canonicalize_simplified().unwrap_or(common);
3186 if common == gd {
3187 return git_dir_display.to_owned();
3188 }
3189
3190 let ac = actual_cwd
3191 .canonicalize_simplified()
3192 .unwrap_or_else(|_| actual_cwd.to_path_buf());
3193 if repo.work_tree.is_none() && !repo.explicit_git_dir {
3194 if ac == common {
3195 return ".".to_owned();
3196 }
3197 if ac.starts_with(&common) && ac != common {
3198 return common.display().to_string();
3199 }
3200 }
3201
3202 let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
3203 (Some(root), Some(wt)) if !repo.work_tree_from_env => {
3204 let r = root
3205 .canonicalize_simplified()
3206 .unwrap_or_else(|_| root.clone());
3207 let w = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3208 r != w
3209 }
3210 _ => false,
3211 };
3212 if work_relocated {
3213 if let Some(wt) = &repo.work_tree {
3214 let wt = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3215 if ac == wt {
3216 return common.display().to_string();
3217 }
3218 let inside_wt = ac.starts_with(&wt) && ac != wt;
3219 if inside_wt {
3220 if let Some(rel) = path_relative_to(&common, &ac) {
3221 let s = rel.to_string_lossy().replace('\\', "/");
3222 return if s.is_empty() || s == "." {
3223 ".".to_owned()
3224 } else {
3225 s
3226 };
3227 }
3228 }
3229 }
3230 }
3231
3232 if repo.discovery_via_gitfile && !repo.explicit_git_dir {
3233 return common.display().to_string();
3234 }
3235
3236 if repo.work_tree.is_none() && !repo.explicit_git_dir {
3237 let tc = trace_cwd
3238 .canonicalize_simplified()
3239 .unwrap_or_else(|_| trace_cwd.to_path_buf());
3240 if let Some(cp) = common.parent() {
3241 let cp = cp
3242 .canonicalize_simplified()
3243 .unwrap_or_else(|_| cp.to_path_buf());
3244 let comc = common
3245 .canonicalize_simplified()
3246 .unwrap_or_else(|_| common.clone());
3247 if tc.starts_with(&cp) && tc != cp && !tc.starts_with(&comc) {
3248 return comc.display().to_string();
3249 }
3250 if tc == cp {
3251 return rel_path_for_setup_trace(&common, trace_cwd);
3252 }
3253 }
3254 }
3255
3256 if trace_cwd_strictly_inside_git_parent(trace_cwd, &common) {
3257 rel_path_for_setup_trace(&common, trace_cwd)
3258 } else {
3259 common.display().to_string()
3260 }
3261}
3262
3263fn replace_dir_is_non_empty(replace_dir: &Path) -> bool {
3265 let Ok(entries) = fs::read_dir(replace_dir) else {
3266 return false;
3267 };
3268 entries.filter_map(|e| e.ok()).any(|e| {
3269 let Ok(ft) = e.file_type() else {
3270 return false;
3271 };
3272 ft.is_file()
3273 })
3274}
3275
3276fn resolve_common_dir(git_dir: &Path) -> Option<PathBuf> {
3278 let commondir_file = git_dir.join("commondir");
3279 if !commondir_file.is_file() {
3280 return None;
3281 }
3282 let common_raw = fs::read_to_string(commondir_file).ok()?;
3283 let common_rel = common_raw.trim();
3284 if common_rel.is_empty() {
3285 return None;
3286 }
3287 let common_dir = if Path::new(common_rel).is_absolute() {
3288 PathBuf::from(common_rel)
3289 } else {
3290 git_dir.join(common_rel)
3291 };
3292 Some(common_dir.canonicalize_simplified().unwrap_or(common_dir))
3293}
3294
3295#[must_use]
3297pub fn common_git_dir_for_config(git_dir: &Path) -> PathBuf {
3298 resolve_common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf())
3299}
3300
3301pub fn worktree_config_enabled(common_dir: &Path) -> bool {
3303 let path = common_dir.join("config");
3304 let Ok(content) = crate::repo_caches::read_local_config_file(&path) else {
3305 return false;
3306 };
3307 worktree_config_enabled_for_text(Some(content.as_str()))
3308}
3309
3310pub fn worktree_config_enabled_for_text(content: Option<&str>) -> bool {
3312 let Some(content) = content else {
3313 return false;
3314 };
3315 let mut in_extensions = false;
3316 for raw_line in content.lines() {
3317 let mut line = raw_line.trim();
3318 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
3319 continue;
3320 }
3321 if line.starts_with('[') {
3322 let Some(end_idx) = line.find(']') else {
3323 continue;
3324 };
3325 let section = line[1..end_idx].trim();
3326 let section_name = section
3327 .split_whitespace()
3328 .next()
3329 .unwrap_or_default()
3330 .to_ascii_lowercase();
3331 in_extensions = section_name == "extensions";
3332 let remainder = line[end_idx + 1..].trim();
3333 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
3334 continue;
3335 }
3336 line = remainder;
3337 }
3338 if in_extensions {
3339 let Some((key, value)) = line.split_once('=') else {
3340 continue;
3341 };
3342 if key.trim().eq_ignore_ascii_case("worktreeconfig") {
3343 let v = value.trim();
3344 return v.eq_ignore_ascii_case("true")
3345 || v.eq_ignore_ascii_case("yes")
3346 || v.eq_ignore_ascii_case("on")
3347 || v == "1";
3348 }
3349 }
3350 }
3351 false
3352}
3353
3354fn open_or_create_config_file(path: &Path, scope: ConfigScope) -> Result<ConfigFile> {
3355 match ConfigFile::from_path(path, scope)? {
3356 Some(f) => Ok(f),
3357 None => {
3358 if let Some(parent) = path.parent() {
3359 fs::create_dir_all(parent).map_err(Error::Io)?;
3360 }
3361 ConfigFile::parse(path, "", scope)
3362 }
3363 }
3364}
3365
3366fn config_file_bool_true(cfg: &ConfigFile, key: &str) -> bool {
3367 cfg.get(key).is_some_and(|v| {
3368 matches!(
3369 v.trim().to_ascii_lowercase().as_str(),
3370 "true" | "yes" | "on" | "1"
3371 )
3372 })
3373}
3374
3375pub fn init_worktree_config(git_dir: &Path) -> Result<()> {
3385 let common_dir = common_git_dir_for_config(git_dir);
3386 let common_config_path = common_dir.join("config");
3387 let worktree_config_path = git_dir.join("config.worktree");
3388
3389 if worktree_config_enabled(&common_dir) {
3390 if !worktree_config_path.exists() {
3391 if let Some(parent) = worktree_config_path.parent() {
3392 fs::create_dir_all(parent).map_err(Error::Io)?;
3393 }
3394 fs::write(&worktree_config_path, "").map_err(Error::Io)?;
3395 }
3396 return Ok(());
3397 }
3398
3399 let mut common_cfg = open_or_create_config_file(&common_config_path, ConfigScope::Local)?;
3400 common_cfg.set("extensions.worktreeConfig", "true")?;
3401
3402 let mut wt_cfg = open_or_create_config_file(&worktree_config_path, ConfigScope::Worktree)?;
3403
3404 if config_file_bool_true(&common_cfg, "core.bare") {
3405 wt_cfg.set("core.bare", "true")?;
3406 common_cfg.unset("core.bare")?;
3407 }
3408 if let Some(worktree) = common_cfg.get("core.worktree") {
3409 wt_cfg.set("core.worktree", &worktree)?;
3410 common_cfg.unset("core.worktree")?;
3411 }
3412
3413 common_cfg.write()?;
3414 wt_cfg.write()?;
3415 Ok(())
3416}
3417
3418pub fn early_config_ignore_repo_reason(common_dir: &Path) -> Option<String> {
3422 let path = common_dir.join("config");
3423 let content = crate::repo_caches::read_local_config_file(&path).ok()?;
3424 early_config_ignore_repo_reason_for_text(Some(content.as_str()))
3425}
3426
3427pub fn early_config_ignore_repo_reason_for_text(content: Option<&str>) -> Option<String> {
3429 const GIT_REPO_VERSION_READ: u32 = 1;
3430 let content = content?;
3431 let mut version = 0u32;
3432 let mut in_core = false;
3433 for raw_line in content.lines() {
3434 let mut line = raw_line.trim();
3435 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
3436 continue;
3437 }
3438 if line.starts_with('[') {
3439 let Some(end_idx) = line.find(']') else {
3440 continue;
3441 };
3442 let section = line[1..end_idx].trim();
3443 let section_name = section
3444 .split_whitespace()
3445 .next()
3446 .unwrap_or_default()
3447 .to_ascii_lowercase();
3448 in_core = section_name == "core";
3449 let remainder = line[end_idx + 1..].trim();
3450 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
3451 continue;
3452 }
3453 line = remainder;
3454 }
3455 if in_core {
3456 if let Some((key, value)) = line.split_once('=') {
3457 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
3458 if let Ok(v) = value.trim().parse::<u32>() {
3459 version = v;
3460 }
3461 }
3462 }
3463 }
3464 }
3465 if version > GIT_REPO_VERSION_READ {
3466 Some(format!(
3467 "Expected git repo version <= {GIT_REPO_VERSION_READ}, found {version}"
3468 ))
3469 } else {
3470 None
3471 }
3472}
3473
3474fn path_for_ceiling_compare(path: &Path) -> String {
3475 let path = path.to_string_lossy();
3476 #[cfg(windows)]
3477 {
3478 path.replace('\\', "/")
3479 }
3480 #[cfg(not(windows))]
3481 {
3482 path.into_owned()
3483 }
3484}
3485
3486fn offset_1st_component(path: &str) -> usize {
3487 if path.starts_with('/') {
3488 1
3489 } else {
3490 0
3491 }
3492}
3493
3494fn longest_ancestor_length(path: &str, ceilings: &[String]) -> Option<usize> {
3496 if path == "/" {
3497 return None;
3498 }
3499 let mut max_len: Option<usize> = None;
3500 for ceil in ceilings {
3501 let mut len = ceil.len();
3502 while len > 0 && ceil.as_bytes().get(len - 1) == Some(&b'/') {
3503 len -= 1;
3504 }
3505 if len == 0 {
3506 continue;
3507 }
3508 if path.len() <= len + 1 {
3509 continue;
3510 }
3511 if !path.starts_with(&ceil[..len]) {
3512 continue;
3513 }
3514 if path.as_bytes().get(len) != Some(&b'/') {
3515 continue;
3516 }
3517 if path.as_bytes().get(len + 1).is_none() {
3518 continue;
3519 }
3520 max_len = Some(max_len.map_or(len, |m| m.max(len)));
3521 }
3522 max_len
3523}
3524
3525pub fn local_config_write_path(git_dir: &Path) -> PathBuf {
3530 if let Some(path) = repository_config_path(git_dir) {
3531 return path;
3532 }
3533 if let Some(common) = resolve_common_dir(git_dir) {
3534 return common.join("config");
3535 }
3536 git_dir.join("config")
3537}
3538
3539fn repository_config_path(git_dir: &Path) -> Option<PathBuf> {
3541 repository_config_path_for_caches(git_dir)
3542}
3543
3544pub(crate) fn repository_config_path_for_caches(git_dir: &Path) -> Option<PathBuf> {
3546 let local = git_dir.join("config");
3547 if local.is_file() {
3548 return Some(local);
3549 }
3550 let commondir = git_dir.join("commondir");
3551 if !commondir.is_file() {
3552 return None;
3553 }
3554 let common = resolve_common_dir(git_dir)?;
3555 let shared = common.join("config");
3556 if shared.exists() {
3557 Some(shared)
3558 } else {
3559 None
3560 }
3561}
3562
3563pub fn validate_repo_format(git_dir: &Path) -> Result<()> {
3569 validate_repository_format(git_dir)
3570}
3571
3572pub(crate) fn validate_repository_format_parsed(parsed: &RepositoryFormat) -> Result<()> {
3574 if parsed.repo_version > 1 {
3575 return Err(Error::UnsupportedRepositoryFormatVersion(
3576 parsed.repo_version,
3577 ));
3578 }
3579
3580 if let Some(raw) = parsed.ref_storage.as_deref() {
3581 crate::ref_storage::RefStorageFormat::validate_config_extension(raw)?;
3582 }
3583
3584 if let Some(msg) = parsed.format_error_message() {
3585 return Err(Error::Message(msg));
3586 }
3587
3588 Ok(())
3589}
3590
3591fn validate_repository_format(git_dir: &Path) -> Result<()> {
3592 let Some(config_path) = repository_config_path(git_dir) else {
3593 return Ok(());
3594 };
3595 let content = crate::repo_caches::read_local_config_file(&config_path)?;
3596 let parsed = parse_repository_format(&content, &config_path)?;
3597 validate_repository_format_parsed(&parsed)
3598}
3599
3600#[derive(Clone)]
3603pub(crate) struct RepositoryFormat {
3604 pub(crate) repo_version: u32,
3606 pub(crate) extensions: BTreeSet<String>,
3608 pub(crate) ref_storage: Option<String>,
3610 pub(crate) object_format: Option<String>,
3612}
3613
3614pub(crate) fn read_repository_format_from_git_dir(git_dir: &Path) -> Result<RepositoryFormat> {
3620 let Some(config_path) = repository_config_path(git_dir) else {
3621 return Ok(RepositoryFormat {
3622 repo_version: 0,
3623 extensions: BTreeSet::new(),
3624 ref_storage: None,
3625 object_format: None,
3626 });
3627 };
3628 let content = crate::repo_caches::read_local_config_file(&config_path)?;
3629 parse_repository_format(&content, &config_path)
3630}
3631
3632impl RepositoryFormat {
3633 pub(crate) fn hash_algo(&self) -> crate::objects::HashAlgo {
3636 self.object_format
3637 .as_deref()
3638 .and_then(crate::objects::HashAlgo::from_name)
3639 .unwrap_or(crate::objects::HashAlgo::Sha1)
3640 }
3641
3642 fn format_error_message(&self) -> Option<String> {
3649 let mut v1_only_found: Vec<&str> = Vec::new();
3656 let mut unknown_found: Vec<&str> = Vec::new();
3657 for extension in &self.extensions {
3658 match extension.as_str() {
3659 "noop" | "preciousobjects" | "partialclone" | "worktreeconfig" => {}
3661 "noop-v1"
3663 | "objectformat"
3664 | "compatobjectformat"
3665 | "refstorage"
3666 | "relativeworktrees"
3667 | "submodulepathconfig" => {
3668 if self.repo_version == 0 {
3669 v1_only_found.push(extension);
3670 }
3671 }
3672 _ => {
3674 if self.repo_version >= 1 {
3675 unknown_found.push(extension);
3676 }
3677 }
3678 }
3679 }
3680
3681 if !unknown_found.is_empty() {
3682 let mut msg = if unknown_found.len() == 1 {
3683 "unknown repository extension found:".to_owned()
3684 } else {
3685 "unknown repository extensions found:".to_owned()
3686 };
3687 for ext in &unknown_found {
3688 msg.push_str(&format!("\n\t{ext}"));
3689 }
3690 return Some(msg);
3691 }
3692
3693 if !v1_only_found.is_empty() {
3694 let mut msg = if v1_only_found.len() == 1 {
3695 "repo version is 0, but v1-only extension found:".to_owned()
3696 } else {
3697 "repo version is 0, but v1-only extensions found:".to_owned()
3698 };
3699 for ext in &v1_only_found {
3700 msg.push_str(&format!("\n\t{ext}"));
3701 }
3702 return Some(msg);
3703 }
3704
3705 None
3706 }
3707}
3708
3709pub(crate) fn parse_repository_format(
3716 content: &str,
3717 config_path: &Path,
3718) -> Result<RepositoryFormat> {
3719 let mut in_core = false;
3720 let mut in_extensions = false;
3721 let mut repo_version = 0u32;
3722 let mut extensions = BTreeSet::new();
3723 let mut ref_storage: Option<String> = None;
3724 let mut object_format: Option<String> = None;
3725
3726 for raw_line in content.lines() {
3727 let mut line = raw_line.trim();
3728 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
3729 continue;
3730 }
3731
3732 if line.starts_with('[') {
3733 let Some(end_idx) = line.find(']') else {
3734 return Err(Error::Config(
3735 format!("invalid config in {}", config_path.display()).into(),
3736 ));
3737 };
3738
3739 let section = line[1..end_idx].trim();
3740 let section_name = section
3741 .split_whitespace()
3742 .next()
3743 .unwrap_or_default()
3744 .to_ascii_lowercase();
3745 in_core = section_name == "core";
3746 in_extensions = section_name == "extensions";
3747
3748 let remainder = line[end_idx + 1..].trim();
3749 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
3750 continue;
3751 }
3752 line = remainder;
3753 }
3754
3755 if in_core {
3756 if let Some((key, value)) = line.split_once('=') {
3757 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
3758 if let Ok(v) = value.trim().parse::<u32>() {
3760 repo_version = v;
3761 }
3762 }
3763 }
3764 }
3765
3766 if in_extensions {
3767 let (key, value) = if let Some((key, value)) = line.split_once('=') {
3768 (key.trim(), Some(value.trim()))
3769 } else {
3770 (line, None)
3771 };
3772 if key.eq_ignore_ascii_case("refstorage") {
3773 ref_storage = value.map(str::to_owned);
3774 }
3775 if key.eq_ignore_ascii_case("objectformat") {
3776 object_format = value.map(str::to_owned);
3777 }
3778 if !key.is_empty() {
3779 extensions.insert(key.to_ascii_lowercase());
3780 }
3781 }
3782 }
3783
3784 Ok(RepositoryFormat {
3785 repo_version,
3786 extensions,
3787 ref_storage,
3788 object_format,
3789 })
3790}
3791
3792pub fn repository_format_warning(git_dir: &Path) -> Result<Option<String>> {
3809 const GIT_REPO_VERSION_READ: u32 = 1;
3810 let Some(config_path) = repository_config_path(git_dir) else {
3811 return Ok(None);
3812 };
3813 let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
3814 let parsed = parse_repository_format(&content, &config_path)?;
3815
3816 if parsed.repo_version > GIT_REPO_VERSION_READ {
3817 return Ok(Some(format!(
3818 "Expected git repo version <= {GIT_REPO_VERSION_READ}, found {}",
3819 parsed.repo_version
3820 )));
3821 }
3822
3823 Ok(parsed.format_error_message())
3824}
3825
3826struct DiscoveredAt {
3832 repo: Repository,
3833 gitfile: Option<PathBuf>,
3835}
3836
3837fn try_open_at(options: &RepositoryOptions, dir: &Path) -> Result<Option<DiscoveredAt>> {
3838 let env = &options.environment;
3839 let dot_git = dir.join(".git");
3840
3841 #[cfg(unix)]
3844 {
3845 use std::os::unix::fs::FileTypeExt;
3846 let is_special = |ft: fs::FileType| {
3847 ft.is_fifo() || ft.is_socket() || ft.is_block_device() || ft.is_char_device()
3848 };
3849 if let Ok(meta) = fs::symlink_metadata(&dot_git) {
3850 let ft = meta.file_type();
3851 let target_special = ft.is_symlink()
3852 && fs::metadata(&dot_git).is_ok_and(|target| is_special(target.file_type()));
3853 if is_special(ft) || target_special {
3854 return Err(Error::NotARepository(
3855 NotARepositoryReason::GitfileNotRegularFile { path: dot_git },
3856 ));
3857 }
3858 }
3859 }
3860
3861 if dot_git.is_file() {
3862 let git_dir = resolve_gitfile(&dot_git)?;
3864 let mut opts = options.clone();
3865 opts.validate_repository_format = false;
3866 opts.work_tree = Some(dir.to_path_buf());
3867 let mut repo = Repository::open_internal(&opts, &git_dir)?;
3868 if resolve_common_dir(&git_dir).is_some() {
3872 let cwd = env.discovery_cwd();
3873 if repo.work_tree.is_some() && !is_inside_work_tree(&repo, &cwd) {
3874 let root = if dir.is_absolute() {
3875 dir.to_path_buf()
3876 } else {
3877 cwd.join(dir)
3878 };
3879 repo.work_tree = Some(root.canonicalize_simplified().unwrap_or(root));
3880 }
3881 }
3882 let root = if dir.is_absolute() {
3883 dir.to_path_buf()
3884 } else {
3885 env.discovery_cwd().join(dir)
3886 };
3887 repo.discovery_root = Some(root.canonicalize_simplified().unwrap_or(root));
3888 repo.discovery_via_gitfile = true;
3889 return Ok(Some(DiscoveredAt {
3890 repo,
3891 gitfile: Some(dot_git.clone()),
3892 }));
3893 }
3894
3895 if dot_git.is_dir() {
3896 let open_path = if dot_git.is_symlink() {
3900 dot_git.read_link().unwrap_or_else(|_| dot_git.clone())
3902 } else {
3903 dot_git.clone()
3904 };
3905 let mut skip_opts = options.clone();
3908 skip_opts.validate_repository_format = false;
3909 skip_opts.work_tree = Some(dir.to_path_buf());
3910 match Repository::open_internal(&skip_opts, &open_path) {
3911 Ok(mut repo) => {
3912 if dot_git.is_symlink() {
3915 let abs_dot_git = if dot_git.is_absolute() {
3916 dot_git
3917 } else {
3918 dir.join(".git")
3919 };
3920 repo.git_dir = abs_dot_git;
3921 }
3922 let root = if dir.is_absolute() {
3923 dir.to_path_buf()
3924 } else {
3925 env.discovery_cwd().join(dir)
3926 };
3927 repo.discovery_root = Some(root.canonicalize_simplified().unwrap_or(root));
3928 repo.discovery_via_gitfile = false;
3929 return Ok(Some(DiscoveredAt {
3930 repo,
3931 gitfile: None,
3932 }));
3933 }
3934 Err(Error::NotARepository(_)) | Err(Error::Config(_)) => return Ok(None),
3935 Err(e) => return Err(e),
3936 }
3937 }
3938
3939 if dir.join("HEAD").is_file() && dir.join("commondir").is_file() {
3942 maybe_trace_implicit_bare_repository(dir, env);
3943 let repo = Repository::open_with_work_tree(options, dir, None)?;
3944 return Ok(Some(DiscoveredAt {
3945 repo,
3946 gitfile: None,
3947 }));
3948 }
3949
3950 if dir.join("objects").is_dir() && dir.join("HEAD").is_file() {
3952 maybe_trace_implicit_bare_repository(dir, env);
3953 if !is_inside_dot_git(dir) {
3957 if let Ok(cfg) = crate::config::ConfigSet::load(env, None, true) {
3958 if let Some(val) = cfg.get("safe.bareRepository") {
3959 if val.eq_ignore_ascii_case("explicit") {
3960 return Err(Error::ForbiddenBareRepository(path_for_error_message(dir)));
3961 }
3962 }
3963 }
3964 }
3965 let repo = Repository::open_with_work_tree(options, dir, None)?;
3966 return Ok(Some(DiscoveredAt {
3967 repo,
3968 gitfile: None,
3969 }));
3970 }
3971
3972 Ok(None)
3973}
3974
3975fn is_inside_dot_git(path: &Path) -> bool {
3976 path.components().any(|c| c.as_os_str() == ".git")
3977}
3978
3979fn maybe_trace_implicit_bare_repository(dir: &Path, environment: &Environment) {
3980 let Some(path) = environment
3981 .git_trace2_perf
3982 .as_deref()
3983 .filter(|p| !p.is_empty())
3984 else {
3985 return;
3986 };
3987
3988 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
3989 let _ = writeln!(file, "setup: implicit-bare-repository:{}", dir.display());
3990 }
3991}
3992
3993fn safe_directory_effective_values(git_dir: &Path, env: &Environment) -> Vec<String> {
3996 let cfg = crate::config::ConfigSet::load(env, Some(git_dir), true)
3997 .unwrap_or_else(|_| crate::config::ConfigSet::new());
3998 let mut values: Vec<String> = Vec::new();
3999 for e in cfg.entries() {
4000 if e.key == "safe.directory"
4001 && e.scope != crate::config::ConfigScope::Local
4002 && e.scope != crate::config::ConfigScope::Worktree
4003 {
4004 values.push(e.value.clone().unwrap_or_else(|| "true".to_owned()));
4005 }
4006 }
4007 let mut effective: Vec<String> = Vec::new();
4008 for v in values {
4009 if v.is_empty() {
4010 effective.clear();
4011 } else {
4012 effective.push(v);
4013 }
4014 }
4015 effective
4016}
4017
4018fn ensure_safe_directory_allows(
4019 git_dir: &Path,
4020 checked: &Path,
4021 environment: &Environment,
4022) -> Result<()> {
4023 let effective = safe_directory_effective_values(git_dir, environment);
4024 let checked_s = checked.to_string_lossy().to_string();
4025 let _ = environment.grit_debug_safe_dir;
4026 if effective
4027 .iter()
4028 .any(|v| safe_directory_matches(v, &checked_s, &environment.discovery_cwd()))
4029 {
4030 return Ok(());
4031 }
4032 Err(Error::DubiousOwnership(path_for_error_message(checked)))
4033}
4034
4035#[cfg(unix)]
4036fn path_lstat_uid(path: &Path) -> std::io::Result<u32> {
4037 use std::os::unix::fs::MetadataExt;
4038 let meta = fs::symlink_metadata(path)?;
4039 Ok(meta.uid())
4040}
4041
4042#[cfg(unix)]
4045fn ensure_valid_ownership(
4046 environment: &Environment,
4047 gitfile: Option<&Path>,
4048 worktree: Option<&Path>,
4049 gitdir: &Path,
4050) -> Result<()> {
4051 const ROOT_UID: u32 = 0;
4052 let sudo_euid = environment
4053 .sudo_uid
4054 .as_deref()
4055 .and_then(|s| s.parse::<u32>().ok());
4056
4057 fn owned_by_effective_user(path: &Path, sudo_euid: Option<u32>) -> std::io::Result<bool> {
4058 let st_uid = path_lstat_uid(path)?;
4059 let mut euid = nix::unistd::geteuid().as_raw();
4060 if euid == ROOT_UID {
4061 if st_uid == ROOT_UID {
4062 return Ok(true);
4063 }
4064 if let Some(sudo_uid) = sudo_euid {
4065 euid = sudo_uid;
4066 }
4067 }
4068 Ok(st_uid == euid)
4069 }
4070
4071 let assume_different = environment.test_assume_different_owner();
4072 if !assume_different {
4073 let gitfile_ok = gitfile
4074 .map(|p| owned_by_effective_user(p, sudo_euid))
4075 .transpose()?
4076 .unwrap_or(true);
4077 let wt_ok = match worktree {
4080 None => true,
4081 Some(wt) => match owned_by_effective_user(wt, sudo_euid) {
4082 Ok(ok) => ok,
4083 Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
4084 Err(e) => return Err(Error::Io(e)),
4085 },
4086 };
4087 let gd_ok = owned_by_effective_user(gitdir, sudo_euid)?;
4088 if gitfile_ok && wt_ok && gd_ok {
4089 return Ok(());
4090 }
4091 }
4092
4093 let data_path = if let Some(wt) = worktree {
4094 wt.canonicalize_simplified()
4095 .unwrap_or_else(|_| wt.to_path_buf())
4096 } else {
4097 gitdir
4098 .canonicalize_simplified()
4099 .unwrap_or_else(|_| gitdir.to_path_buf())
4100 };
4101 ensure_safe_directory_allows(gitdir, &data_path, environment)
4102}
4103
4104#[cfg(not(unix))]
4105fn ensure_valid_ownership(
4106 _environment: &Environment,
4107 _gitfile: Option<&Path>,
4108 _worktree: Option<&Path>,
4109 _gitdir: &Path,
4110) -> Result<()> {
4111 Ok(())
4112}
4113
4114impl Repository {
4115 pub fn enforce_safe_directory(&self) -> Result<()> {
4121 let assume_different =
4122 self.test_assume_different_owner || self.environment.test_assume_different_owner();
4123 if !assume_different {
4124 return Ok(());
4125 }
4126
4127 if self.explicit_git_dir {
4128 return Ok(());
4129 }
4130
4131 let checked = if let Some(wt) = &self.work_tree {
4135 let cwd = Some(self.environment.discovery_cwd());
4136 if let Some(cwd) = cwd {
4137 if cwd
4138 .canonicalize_simplified()
4139 .ok()
4140 .is_some_and(|c| c.starts_with(&self.git_dir))
4141 {
4142 self.git_dir
4143 .canonicalize_simplified()
4144 .unwrap_or_else(|_| self.git_dir.clone())
4145 } else {
4146 wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone())
4147 }
4148 } else {
4149 wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone())
4150 }
4151 } else {
4152 self.git_dir
4153 .canonicalize_simplified()
4154 .unwrap_or_else(|_| self.git_dir.clone())
4155 };
4156
4157 let _ = self.environment.grit_debug_safe_dir;
4158 self.enforce_safe_directory_checked(&checked)
4159 }
4160
4161 pub(crate) fn enforce_safe_directory_git_dir(&self) -> Result<()> {
4166 let assume_different =
4167 self.test_assume_different_owner || self.environment.test_assume_different_owner();
4168 if !assume_different {
4169 return Ok(());
4170 }
4171 let checked = self
4172 .git_dir
4173 .canonicalize_simplified()
4174 .unwrap_or_else(|_| self.git_dir.clone());
4175 let _ = self.environment.grit_debug_safe_dir;
4176 self.enforce_safe_directory_checked(&checked)
4177 }
4178
4179 fn enforce_safe_directory_checked(&self, checked: &Path) -> Result<()> {
4180 ensure_safe_directory_allows(&self.git_dir, checked, self.environment.as_ref())
4181 }
4182
4183 pub fn verify_safe_for_clone_source(&self) -> Result<()> {
4189 let assume_different =
4190 self.test_assume_different_owner || self.environment.test_assume_different_owner();
4191 if assume_different {
4192 self.enforce_safe_directory_git_dir()
4193 } else {
4194 #[cfg(unix)]
4195 {
4196 ensure_valid_ownership(self.environment.as_ref(), None, None, &self.git_dir)
4197 }
4198 #[cfg(not(unix))]
4199 {
4200 Ok(())
4201 }
4202 }
4203 }
4204}
4205
4206fn normalize_fs_path(raw: &str) -> String {
4207 use std::path::Component;
4208 let p = std::path::Path::new(raw);
4209 let mut parts: Vec<String> = Vec::new();
4210 let mut absolute = false;
4211 for c in p.components() {
4212 match c {
4213 Component::RootDir => {
4214 absolute = true;
4215 parts.clear();
4216 }
4217 Component::CurDir => {}
4218 Component::ParentDir => {
4219 if !parts.is_empty() {
4220 parts.pop();
4221 }
4222 }
4223 Component::Normal(s) => parts.push(s.to_string_lossy().to_string()),
4224 Component::Prefix(_) => {}
4225 }
4226 }
4227 let mut out = if absolute {
4228 String::from("/")
4229 } else {
4230 String::new()
4231 };
4232 out.push_str(&parts.join("/"));
4233 out
4234}
4235
4236fn safe_directory_matches(config_value: &str, checked: &str, cwd: &Path) -> bool {
4237 if config_value == "*" {
4238 return true;
4239 }
4240 if config_value == "." {
4241 let cwd_s = normalize_fs_path(&cwd.to_string_lossy());
4243 let checked_s = normalize_fs_path(checked);
4244 return cwd_s == checked_s;
4245 }
4246
4247 let canonicalize_or_normalize = |raw: &str| -> String {
4248 let p = std::path::Path::new(raw);
4249 if p.exists() {
4250 p.canonicalize_simplified()
4251 .map(|c| c.to_string_lossy().to_string())
4252 .map(|s| normalize_fs_path(&s))
4253 .unwrap_or_else(|_| normalize_fs_path(raw))
4254 } else {
4255 normalize_fs_path(raw)
4256 }
4257 };
4258
4259 let config_norm = canonicalize_or_normalize(config_value);
4260 let checked_norm = normalize_fs_path(checked);
4261
4262 if config_norm.ends_with("/*") {
4263 let prefix_raw = &config_norm[..config_norm.len() - 2];
4264 let prefix_norm = canonicalize_or_normalize(prefix_raw);
4265 let mut prefix = prefix_norm;
4266 if !prefix.ends_with('/') {
4267 prefix.push('/');
4268 }
4269 return checked_norm.starts_with(&prefix);
4270 }
4271
4272 config_norm == checked_norm
4273}
4274
4275fn warn_core_bare_worktree_conflict(options: &RepositoryOptions, repo: &Repository) {
4279 if options
4280 .environment
4281 .git_work_tree
4282 .as_deref()
4283 .is_some_and(|s| !s.trim().is_empty())
4284 {
4285 return;
4286 }
4287 let Ok(cfg) = repo.ensure_config_arc_fast() else {
4288 return;
4289 };
4290 if let (true, Some(_)) = read_core_bare_and_worktree_from_config(&cfg) {
4291 options
4292 .diagnostics
4293 .warn(diagnostics::Warning::CoreBareWithWorktree);
4294 }
4295}
4296
4297fn read_core_bare_and_worktree_from_config(cfg: &ConfigSet) -> (bool, Option<String>) {
4298 let bare = cfg
4299 .get_bool("core.bare")
4300 .and_then(|r| r.ok())
4301 .unwrap_or(false);
4302 let worktree = cfg.get("core.worktree").map(|s| s.to_owned());
4303 (bare, worktree)
4304}
4305
4306fn read_core_bare_and_worktree(git_dir: &Path) -> Result<(bool, Option<String>)> {
4307 let cfg = ConfigSet::load(
4308 &crate::environment::Environment::empty(),
4309 Some(git_dir),
4310 true,
4311 )
4312 .unwrap_or_default();
4313 Ok(read_core_bare_and_worktree_from_config(&cfg))
4314}
4315
4316fn validate_git_work_tree_path(path: &Path) -> Result<()> {
4319 if !path.is_absolute() {
4320 return Ok(());
4321 }
4322 let comps: Vec<Component<'_>> = path.components().collect();
4323 let Some(last_normal_idx) = comps
4324 .iter()
4325 .enumerate()
4326 .rev()
4327 .find_map(|(i, c)| matches!(c, Component::Normal(_)).then_some(i))
4328 else {
4329 return Ok(());
4330 };
4331 let mut cur = PathBuf::new();
4332 for (i, comp) in comps.iter().enumerate() {
4333 match comp {
4334 Component::Prefix(p) => cur.push(p.as_os_str()),
4335 Component::RootDir => cur.push(comp.as_os_str()),
4336 Component::CurDir => {}
4337 Component::ParentDir => {
4338 let _ = cur.pop();
4339 }
4340 Component::Normal(seg) => {
4341 cur.push(seg);
4342 if i != last_normal_idx && !cur.exists() {
4343 return Err(Error::PathError(format!(
4344 "Invalid path '{}': No such file or directory",
4345 cur.display()
4346 )));
4347 }
4348 }
4349 }
4350 }
4351 Ok(())
4352}
4353
4354fn resolve_core_worktree_path(git_dir: &Path, raw: &str) -> Result<PathBuf> {
4355 let p = Path::new(raw);
4356 if p.is_absolute() {
4357 return Ok(p
4358 .canonicalize_simplified()
4359 .unwrap_or_else(|_| p.to_path_buf()));
4360 }
4361 let joined = git_dir.join(p);
4362 Ok(joined.canonicalize_simplified().unwrap_or(joined))
4363}
4364
4365fn resolve_git_dir_env_path(git_dir: &Path) -> Result<PathBuf> {
4367 if git_dir.is_file() {
4368 return resolve_gitfile(git_dir);
4369 }
4370 Ok(git_dir.to_path_buf())
4371}
4372
4373pub fn resolve_git_directory_arg(git_dir: &Path) -> Result<PathBuf> {
4379 resolve_git_dir_env_path(git_dir)
4380}
4381
4382pub fn resolve_dot_git(dot_git: &Path) -> Result<PathBuf> {
4388 if dot_git.is_dir() {
4389 return dot_git
4390 .canonicalize_simplified()
4391 .map_err(|_| not_a_repository_at(dot_git));
4392 }
4393 if dot_git.is_file() {
4394 return resolve_gitfile(dot_git);
4395 }
4396 Err(not_a_repository_at(dot_git))
4397}
4398
4399fn not_a_repository_at(path: &Path) -> Error {
4400 Error::NotARepository(NotARepositoryReason::NoRepositoryAt {
4401 path: path.to_path_buf(),
4402 })
4403}
4404
4405fn canonicalize_existing_prefix(path: &Path) -> Result<PathBuf> {
4409 let absolute = std::path::absolute(path)?;
4410 let mut missing = Vec::new();
4411 let mut existing = absolute.as_path();
4412 loop {
4413 if let Ok(canonical) = existing.canonicalize_simplified() {
4414 return Ok(missing
4415 .iter()
4416 .rev()
4417 .fold(canonical, |acc: PathBuf, part| acc.join(part)));
4418 }
4419 match (existing.parent(), existing.file_name()) {
4420 (Some(parent), Some(name)) => {
4421 missing.push(name.to_owned());
4422 existing = parent;
4423 }
4424 _ => return Ok(absolute),
4425 }
4426 }
4427}
4428
4429fn resolve_objects_dir(
4439 environment: &Environment,
4440 git_dir: &Path,
4441 common_git_dir: &Path,
4442) -> Option<PathBuf> {
4443 if let Some(dir) = environment.git_object_directory.as_deref() {
4444 return (!dir.is_empty()).then(|| environment.discovery_cwd().join(dir));
4445 }
4446 let own = git_dir.join("objects");
4447 Some(if own.exists() {
4448 own
4449 } else {
4450 common_git_dir.join("objects")
4451 })
4452}
4453
4454fn head_file_present(git_dir: &Path) -> bool {
4455 let head_path = git_dir.join("HEAD");
4456 head_path.exists() || head_path.is_symlink()
4457}
4458
4459fn open_or_install_ref_store(
4462 caches: &RepoCaches,
4463 git_dir: &Path,
4464 environment: &Environment,
4465 injected: Option<Arc<dyn crate::refs::store::RefStore>>,
4466) -> Result<Arc<dyn crate::refs::store::RefStore>> {
4467 match injected {
4468 Some(store) => {
4469 let store = linked_worktree_view(git_dir, store);
4470 caches.install_ref_store(git_dir, Arc::clone(&store));
4471 Ok(store)
4472 }
4473 None => caches.open_ref_store(git_dir, environment),
4474 }
4475}
4476
4477fn linked_worktree_view(
4484 git_dir: &Path,
4485 store: Arc<dyn crate::refs::store::RefStore>,
4486) -> Arc<dyn crate::refs::store::RefStore> {
4487 use crate::refs::store::{LinkedWorktreeRefStore, RefStorageFormat};
4488 let on_disk = matches!(
4489 store.format(),
4490 RefStorageFormat::Files | RefStorageFormat::Reftable
4491 );
4492 if on_disk || !git_dir.join("commondir").is_file() {
4493 return store;
4494 }
4495 Arc::new(LinkedWorktreeRefStore::new(git_dir.to_path_buf(), store))
4496}
4497
4498fn resolve_gitfile(gitfile: &Path) -> Result<PathBuf> {
4500 let content = fs::read_to_string(gitfile).map_err(|error| {
4501 Error::NotARepository(NotARepositoryReason::GitfileUnreadable {
4502 path: gitfile.to_path_buf(),
4503 error,
4504 })
4505 })?;
4506 parse_gitfile(&content, gitfile)
4507}
4508
4509fn parse_gitfile(content: &str, gitfile: &Path) -> Result<PathBuf> {
4512 let Some(target) = content
4513 .lines()
4514 .find_map(|line| line.strip_prefix("gitdir:"))
4515 else {
4516 return Err(Error::NotARepository(
4517 NotARepositoryReason::GitfileMissingGitdir {
4518 path: gitfile.to_path_buf(),
4519 },
4520 ));
4521 };
4522 let path = gitfile
4523 .parent()
4524 .unwrap_or_else(|| Path::new(""))
4525 .join(target.trim());
4526 if !path.exists() {
4527 return Err(not_a_repository_at(&path));
4528 }
4529 Ok(path)
4530}
4531
4532pub fn resolve_initial_branch(env: &Environment, explicit: Option<&str>) -> Result<String> {
4542 let name = match explicit {
4543 Some(name) => name.to_owned(),
4544 None => ConfigSet::load(env, None, true)?
4545 .get_last_entry("init.defaultBranch")
4546 .and_then(|e| e.value.clone())
4547 .filter(|s| !s.is_empty())
4548 .unwrap_or_else(|| "main".to_owned()),
4549 };
4550 validate_branch_short_name(&name).map_err(|e| Error::InvalidRef(e.to_string()))?;
4551 Ok(name)
4552}
4553
4554fn resolve_git_dir_for_init(path: &Path, options: &InitOptions) -> Result<PathBuf> {
4555 if options.bare_with_worktree_at_path || options.bare {
4556 return Ok(path.to_path_buf());
4557 }
4558 if let Some(separate) = &options.separate_git_dir {
4559 return Ok(separate.clone());
4560 }
4561 let dot_git = path.join(".git");
4562 if dot_git.is_file() {
4563 let content = fs::read_to_string(&dot_git)?;
4564 return parse_gitfile(&content, &dot_git);
4565 }
4566 Ok(dot_git)
4567}
4568
4569fn head_branch_name_for_outcome(git_dir: &Path) -> Result<String> {
4571 if let Some(target) = crate::refs::read_symbolic_ref(git_dir, "HEAD")? {
4572 if let Some(short) = target.strip_prefix("refs/heads/") {
4573 return Ok(short.to_owned());
4574 }
4575 return Ok(target
4576 .rsplit('/')
4577 .next()
4578 .unwrap_or(target.as_str())
4579 .to_owned());
4580 }
4581 let head_text = fs::read_to_string(git_dir.join("HEAD"))?;
4582 let oid = head_text.trim();
4583 if crate::objects::ObjectId::from_hex(oid).is_ok() {
4584 let end = oid.len().min(7);
4585 return Ok(oid[..end].to_owned());
4586 }
4587 Ok("HEAD".to_owned())
4588}
4589
4590fn hash_algo_from_git_dir(git_dir: &Path) -> Result<HashAlgo> {
4591 let config_path = git_dir.join("config");
4592 if !config_path.is_file() {
4593 return Ok(HashAlgo::Sha1);
4594 }
4595 let content = fs::read_to_string(&config_path)?;
4596 Ok(parse_repository_format(&content, &config_path)?.hash_algo())
4597}
4598
4599fn ensure_git_directory_layout(
4601 git_dir: &Path,
4602 bare: bool,
4603 ref_storage: crate::ref_storage::RefStorageFormat,
4604 skip_hooks_and_info: bool,
4605) -> Result<()> {
4606 let mut subs = vec!["objects", "objects/info", "objects/pack", "refs"];
4607 if !ref_storage.is_reftable() {
4608 subs.extend(["refs/heads", "refs/tags"]);
4609 }
4610 if !bare && !skip_hooks_and_info {
4611 subs.push("info");
4612 subs.push("hooks");
4613 }
4614 for sub in subs {
4615 fs::create_dir_all(git_dir.join(sub))?;
4616 }
4617
4618 if ref_storage.is_reftable() {
4619 let reftable_dir = git_dir.join("reftable");
4620 fs::create_dir_all(&reftable_dir)?;
4621 let tables_list = reftable_dir.join("tables.list");
4622 if !tables_list.exists() {
4623 fs::write(&tables_list, "")?;
4624 }
4625 }
4626 Ok(())
4627}
4628
4629#[allow(clippy::too_many_arguments)]
4630fn write_fresh_git_directory(
4631 git_dir: &Path,
4632 bare: bool,
4633 initial_branch: &str,
4634 template_dir: Option<&Path>,
4635 ref_storage: crate::ref_storage::RefStorageFormat,
4636 object_format: HashAlgo,
4637 skip_hooks_and_info: bool,
4638 environment: &Environment,
4639) -> Result<()> {
4640 ensure_git_directory_layout(git_dir, bare, ref_storage, skip_hooks_and_info)?;
4641
4642 if let Some(tmpl) = template_dir {
4643 if tmpl.is_dir() {
4644 copy_template(tmpl, git_dir)?;
4645 }
4646 }
4647
4648 let needs_extensions = ref_storage.is_reftable() || object_format != HashAlgo::Sha1;
4649 let repo_version = if needs_extensions { 1 } else { 0 };
4650
4651 let mut config_content = String::from("[core]\n");
4652 config_content.push_str(&format!("\trepositoryformatversion = {repo_version}\n"));
4653 config_content.push_str("\tfilemode = true\n");
4654 if bare {
4655 config_content.push_str("\tbare = true\n");
4656 } else {
4657 config_content.push_str("\tbare = false\n");
4658 config_content.push_str("\tlogallrefupdates = true\n");
4659 }
4660 if needs_extensions {
4661 config_content.push_str("[extensions]\n");
4662 if ref_storage.is_reftable() {
4663 config_content.push_str("\trefStorage = reftable\n");
4664 }
4665 if object_format != HashAlgo::Sha1 {
4666 config_content.push_str(&format!("\tobjectformat = {}\n", object_format.name()));
4667 }
4668 }
4669 fs::write(git_dir.join("config"), config_content)?;
4670
4671 if let Some(tmpl) = template_dir {
4673 if tmpl.is_dir() {
4674 let tmpl_config = tmpl.join("config");
4675 if tmpl_config.is_file() {
4676 let tmpl_text = fs::read_to_string(&tmpl_config)?;
4677 let tmpl_parsed = ConfigFile::parse(&tmpl_config, &tmpl_text, ConfigScope::Local)?;
4678 let dest_path = git_dir.join("config");
4679 let dest_text = fs::read_to_string(&dest_path)?;
4680 let mut dest_parsed =
4681 ConfigFile::parse(&dest_path, &dest_text, ConfigScope::Local)?;
4682 for e in &tmpl_parsed.entries {
4683 if e.key == "core.bare" {
4685 continue;
4686 }
4687 if let Some(v) = &e.value {
4688 let _ = dest_parsed.set(&e.key, v);
4689 } else {
4690 let _ = dest_parsed.set(&e.key, "true");
4691 }
4692 }
4693 dest_parsed.write()?;
4694 }
4695 }
4696 }
4697
4698 crate::refs::store::initialize_head(
4699 git_dir,
4700 crate::refs::store::RawRef::Symbolic(format!("refs/heads/{initial_branch}")),
4701 )?;
4702
4703 apply_init_filesystem_config(git_dir, InitFilesystemConfigOptions::default(), environment)?;
4704
4705 fs::write(
4706 git_dir.join("description"),
4707 "Unnamed repository; edit this file 'description' to name the repository.\n",
4708 )?;
4709 Ok(())
4710}
4711
4712pub(crate) fn init_repository_separate_git_dir(
4721 work_tree: &Path,
4722 git_dir: &Path,
4723 initial_branch: &str,
4724 template_dir: Option<&Path>,
4725 ref_storage: crate::ref_storage::RefStorageFormat,
4726 object_format: HashAlgo,
4727 environment: &Environment,
4728) -> Result<Repository> {
4729 let skip_hooks_info = template_dir.is_some_and(|p| p.as_os_str().is_empty());
4730 fs::create_dir_all(work_tree).map_err(|source| {
4731 map_worktree_io(
4732 path_for_error_message(work_tree),
4733 WorktreePathOperation::Create,
4734 source,
4735 )
4736 })?;
4737 fs::create_dir_all(git_dir).map_err(|source| {
4738 map_git_dir_io(git_dir, git_dir, GitDirPathOperation::CreateDir, source)
4739 })?;
4740 let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
4741 if existing {
4742 ensure_git_directory_layout(git_dir, false, ref_storage, skip_hooks_info)?;
4743 } else {
4744 write_fresh_git_directory(
4745 git_dir,
4746 false,
4747 initial_branch,
4748 template_dir,
4749 ref_storage,
4750 object_format,
4751 skip_hooks_info,
4752 environment,
4753 )?;
4754 }
4755
4756 let gitfile = work_tree.join(".git");
4762 let abs_git_dir = crate::git_path::absolute_path_for_gitfile_stored_path(git_dir);
4763 fs::write(gitfile, format!("gitdir: {abs_git_dir}\n"))?;
4764
4765 Repository::open(
4766 git_dir,
4767 &RepositoryOptions::with_environment(environment.clone()).with_work_tree(work_tree),
4768 )
4769}
4770
4771pub fn ensure_core_bare(git_dir: &Path) -> Result<()> {
4786 let path = git_dir.join("config");
4787 let text = fs::read_to_string(&path).unwrap_or_default();
4788 if text.lines().any(|l| {
4789 let t = l.trim();
4790 t == "bare = true" || t == "bare=true"
4791 }) {
4792 return Ok(());
4793 }
4794 let mut out = text;
4795 if !out.ends_with('\n') && !out.is_empty() {
4796 out.push('\n');
4797 }
4798 if !out.contains("[core]") {
4799 out.push_str("[core]\n");
4800 }
4801 out.push_str("\tbare = true\n");
4802 fs::write(path, out).map_err(Error::Io)
4803}
4804
4805pub(crate) fn init_repository(
4806 path: &Path,
4807 bare: bool,
4808 initial_branch: &str,
4809 template_dir: Option<&Path>,
4810 ref_storage: crate::ref_storage::RefStorageFormat,
4811) -> Result<Repository> {
4812 init_repository_with_object_format(
4813 path,
4814 bare,
4815 initial_branch,
4816 template_dir,
4817 ref_storage,
4818 HashAlgo::Sha1,
4819 &Environment::empty(),
4820 )
4821}
4822
4823pub(crate) fn init_repository_with_object_format(
4824 path: &Path,
4825 bare: bool,
4826 initial_branch: &str,
4827 template_dir: Option<&Path>,
4828 ref_storage: crate::ref_storage::RefStorageFormat,
4829 object_format: HashAlgo,
4830 environment: &Environment,
4831) -> Result<Repository> {
4832 let skip_hooks_info = !bare && template_dir.is_some_and(|p| p.as_os_str().is_empty());
4833 let git_dir = if bare {
4834 path.to_path_buf()
4835 } else {
4836 path.join(".git")
4837 };
4838
4839 if !bare {
4840 fs::create_dir_all(path).map_err(|source| {
4841 map_worktree_io(
4842 path_for_error_message(path),
4843 WorktreePathOperation::Create,
4844 source,
4845 )
4846 })?;
4847 }
4848 fs::create_dir_all(&git_dir).map_err(|source| {
4849 if bare {
4850 map_worktree_io(
4851 path_for_error_message(path),
4852 WorktreePathOperation::Create,
4853 source,
4854 )
4855 } else {
4856 map_git_dir_io(&git_dir, &git_dir, GitDirPathOperation::CreateDir, source)
4857 }
4858 })?;
4859 let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
4860 if existing {
4861 ensure_git_directory_layout(&git_dir, bare, ref_storage, skip_hooks_info)?;
4862 } else {
4863 write_fresh_git_directory(
4864 &git_dir,
4865 bare,
4866 initial_branch,
4867 template_dir,
4868 ref_storage,
4869 object_format,
4870 skip_hooks_info,
4871 environment,
4872 )?;
4873 }
4874
4875 if bare {
4876 Repository::open(
4877 &git_dir,
4878 &RepositoryOptions::with_environment(environment.clone()),
4879 )
4880 } else {
4881 Repository::open(
4882 &git_dir,
4883 &RepositoryOptions::with_environment(environment.clone())
4884 .with_work_tree(path.to_path_buf()),
4885 )
4886 }
4887}
4888
4889pub(crate) fn init_bare_with_env_worktree(
4898 git_dir: &Path,
4899 work_tree: &Path,
4900 initial_branch: &str,
4901 template_dir: Option<&Path>,
4902 ref_storage: crate::ref_storage::RefStorageFormat,
4903 object_format: HashAlgo,
4904 environment: &Environment,
4905) -> Result<Repository> {
4906 fs::create_dir_all(git_dir)?;
4907 fs::create_dir_all(work_tree)?;
4908 let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
4909 if existing {
4910 ensure_git_directory_layout(git_dir, true, ref_storage, false)?;
4911 } else {
4912 write_fresh_git_directory(
4913 git_dir,
4914 true,
4915 initial_branch,
4916 template_dir,
4917 ref_storage,
4918 object_format,
4919 false,
4920 environment,
4921 )?;
4922 }
4923 let work_tree_abs = work_tree
4924 .canonicalize_simplified()
4925 .unwrap_or_else(|_| work_tree.to_path_buf());
4926 let config_path = git_dir.join("config");
4927 let mut config = match ConfigFile::from_path(&config_path, ConfigScope::Local)? {
4928 Some(c) => c,
4929 None => ConfigFile::parse(&config_path, "", ConfigScope::Local)?,
4930 };
4931 config.set("core.worktree", &work_tree_abs.to_string_lossy())?;
4932 config.write()?;
4933 Repository::open(
4934 git_dir,
4935 &RepositoryOptions::with_environment(environment.clone()).with_work_tree(work_tree),
4936 )
4937}
4938
4939fn copy_template(src: &Path, dst: &Path) -> Result<()> {
4941 for entry in fs::read_dir(src)? {
4942 let entry = entry?;
4943 let src_path = entry.path();
4944 let dst_path = dst.join(entry.file_name());
4945 if src_path.is_dir() {
4946 fs::create_dir_all(&dst_path)?;
4947 copy_template(&src_path, &dst_path)?;
4948 } else {
4949 fs::copy(&src_path, &dst_path)?;
4950 }
4951 }
4952 Ok(())
4953}
4954
4955fn parse_ceiling_directories(env: &Environment) -> (Vec<PathBuf>, bool) {
4964 let Some(raw) = env.git_ceiling_directories.as_deref() else {
4965 return (Vec::new(), false);
4966 };
4967 if raw.is_empty() {
4968 return (Vec::new(), false);
4969 }
4970 let (no_resolve, effective) = match raw.strip_prefix(':') {
4972 Some(rest) => (true, rest),
4973 None => (false, raw),
4974 };
4975 let paths = effective
4976 .split(':')
4977 .filter(|s| !s.is_empty())
4978 .filter_map(|s| {
4979 let p = PathBuf::from(s);
4980 if !p.is_absolute() {
4981 return None;
4982 }
4983 if no_resolve {
4984 let s = s.trim_end_matches('/');
4986 Some(PathBuf::from(s))
4987 } else {
4988 Some(p.canonicalize_simplified().unwrap_or_else(|_| {
4991 let s = s.trim_end_matches('/');
4992 PathBuf::from(s)
4993 }))
4994 }
4995 })
4996 .collect();
4997 (paths, no_resolve)
4998}
4999
5000pub fn validate_repo_config(config_text: &str) -> std::result::Result<(), String> {
5003 let mut version: u32 = 0;
5004 let mut in_core = false;
5005 for line in config_text.lines() {
5006 let trimmed = line.trim();
5007 if trimmed.starts_with('[') {
5008 in_core = trimmed.to_lowercase().starts_with("[core");
5009 continue;
5010 }
5011 if in_core {
5012 if let Some(rest) = trimmed.strip_prefix("repositoryformatversion") {
5013 let val = rest.trim_start_matches([' ', '=']).trim();
5014 if let Ok(v) = val.parse::<u32>() {
5015 version = v;
5016 }
5017 }
5018 }
5019 }
5020 if version >= 2 {
5021 return Err(format!("unknown repository format version: {version}"));
5022 }
5023 Ok(())
5024}
5025
5026pub mod caches {
5028 pub use crate::repo_caches::*;
5029}
5030
5031mod index_write_fail_inject {
5032 use std::cell::RefCell;
5033
5034 thread_local! {
5036 pub static INJECT: RefCell<bool> = const { RefCell::new(false) };
5037 pub static INJECT_REMAINING: RefCell<Option<usize>> = const { RefCell::new(None) };
5038 }
5039}
5040
5041#[doc(hidden)]
5043pub fn set_test_inject_index_write_fail(enabled: bool) {
5044 index_write_fail_inject::INJECT.with(|c| *c.borrow_mut() = enabled);
5045 if !enabled {
5046 index_write_fail_inject::INJECT_REMAINING.with(|c| *c.borrow_mut() = None);
5047 }
5048}
5049
5050#[doc(hidden)]
5052pub fn set_test_inject_index_write_fail_remaining(remaining: usize) {
5053 index_write_fail_inject::INJECT.with(|c| *c.borrow_mut() = true);
5054 index_write_fail_inject::INJECT_REMAINING.with(|c| *c.borrow_mut() = Some(remaining));
5055}
5056
5057mod rewrite_apply_inject {
5058 use std::cell::RefCell;
5059
5060 thread_local! {
5062 pub static ABORT_AFTER_REFS: RefCell<bool> = const { RefCell::new(false) };
5063 }
5064}
5065
5066#[doc(hidden)]
5068pub fn set_test_inject_rewrite_apply_abort_after_refs(enabled: bool) {
5069 rewrite_apply_inject::ABORT_AFTER_REFS.with(|c| *c.borrow_mut() = enabled);
5070}
5071
5072pub(crate) fn test_inject_rewrite_apply_abort_after_refs() -> bool {
5073 rewrite_apply_inject::ABORT_AFTER_REFS.with(|c| *c.borrow())
5074}
5075
5076fn test_inject_index_write_fail() -> Result<()> {
5077 index_write_fail_inject::INJECT.with(|enabled| {
5078 if !*enabled.borrow() {
5079 return Ok(());
5080 }
5081 index_write_fail_inject::INJECT_REMAINING.with(|remaining| {
5082 if let Some(left) = remaining.borrow_mut().as_mut() {
5083 if *left == 0 {
5084 return Ok(());
5085 }
5086 *left -= 1;
5087 }
5088 Err(Error::Io(std::io::Error::other(
5089 "injected index write failure",
5090 )))
5091 })
5092 })
5093}
5094
5095#[path = "repo_location.rs"]
5096mod location;
5097pub use location::RepositoryLocation;
5098
5099#[cfg(test)]
5100#[path = "repo_line_coverage_tests.rs"]
5101mod line_coverage_tests;
5102
5103#[cfg(test)]
5104mod config_session_tests {
5105 #![allow(clippy::unwrap_used)]
5106
5107 use super::*;
5108 use std::ffi::OsString;
5109
5110 fn options_with_global(home: &Path, global: &Path) -> RepositoryOptions {
5111 RepositoryOptions {
5112 environment: Environment::from_vars(
5113 [
5114 ("HOME", home.as_os_str()),
5115 ("GIT_CONFIG_NOSYSTEM", "1".as_ref()),
5116 ("GIT_CONFIG_GLOBAL", global.as_os_str()),
5117 ]
5118 .map(|(k, v)| (OsString::from(k), v.to_owned())),
5119 home.to_path_buf(),
5120 ),
5121 ..RepositoryOptions::default()
5122 }
5123 }
5124
5125 #[test]
5126 fn config_is_reused_until_a_cascade_file_changes() {
5127 let dir = tempfile::tempdir().unwrap();
5128 let global = dir.path().join("global.cfg");
5129 fs::write(&global, "[user]\n\tname = First\n").unwrap();
5130 let work = dir.path().join("work");
5131 let options = options_with_global(dir.path(), &global);
5132 Repository::init(
5133 &work,
5134 &InitOptions {
5135 environment: Some(options.environment.clone()),
5136 ..InitOptions::default()
5137 },
5138 )
5139 .unwrap();
5140 let repo = Repository::discover(Some(&work), &options).unwrap();
5141
5142 let first = repo.config().unwrap();
5143 assert_eq!(first.get("user.name").as_deref(), Some("First"));
5144 assert!(Arc::ptr_eq(&first, &repo.config().unwrap()));
5145
5146 fs::write(&global, "[user]\n\tname = Second one\n").unwrap();
5147 assert_eq!(
5148 repo.config().unwrap().get("user.name").as_deref(),
5149 Some("Second one")
5150 );
5151
5152 let local = repo.git_dir().join("config");
5153 let mut text = fs::read_to_string(&local).unwrap();
5154 text.push_str("[user]\n\tname = Local\n");
5155 fs::write(&local, text).unwrap();
5156 let reloaded = repo.config().unwrap();
5157 assert_eq!(reloaded.get("user.name").as_deref(), Some("Local"));
5158 assert!(Arc::ptr_eq(&reloaded, &repo.config().unwrap()));
5159
5160 fs::remove_file(&global).unwrap();
5161 fs::write(&local, "[core]\n\trepositoryformatversion = 0\n").unwrap();
5162 assert_eq!(repo.config().unwrap().get("user.name"), None);
5163 }
5164
5165 #[test]
5166 fn a_replaced_environment_is_checked_in_full_once() {
5167 let dir = tempfile::tempdir().unwrap();
5168 let global = dir.path().join("global.cfg");
5169 fs::write(&global, "[user]\n\tname = Env\n").unwrap();
5170 let work = dir.path().join("work");
5171 let options = options_with_global(dir.path(), &global);
5172 Repository::init(
5173 &work,
5174 &InitOptions {
5175 environment: Some(options.environment.clone()),
5176 ..InitOptions::default()
5177 },
5178 )
5179 .unwrap();
5180 let mut repo = Repository::discover(Some(&work), &options).unwrap();
5181 let before = repo.config().unwrap();
5182
5183 repo.environment = Arc::new(options.environment.clone());
5185 assert!(Arc::ptr_eq(&before, &repo.config().unwrap()));
5186
5187 let other = dir.path().join("other.cfg");
5189 fs::write(&other, "[user]\n\tname = Other\n").unwrap();
5190 repo.environment = Arc::new(options_with_global(dir.path(), &other).environment);
5191 assert_eq!(
5192 repo.config().unwrap().get("user.name").as_deref(),
5193 Some("Other")
5194 );
5195 }
5196}