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