1use crate::git_path::CanonicalizeExt;
30use std::collections::BTreeSet;
31use std::fs;
32use std::path::{Component, Path, PathBuf};
33use std::sync::{Arc, Mutex};
34
35use crate::change_id::ChangeIdGenerator;
36use crate::check_ref_format::validate_branch_short_name;
37use crate::command_runner::CommandRunner;
38use crate::config::{
39 config_file_stamps, stamp_for_path, ConfigCacheKey, ConfigFile, ConfigScope, ConfigSet,
40 LoadConfigOptions,
41};
42use crate::diagnostics::{self, DiagnosticsHandle};
43use crate::environment::{Environment, RepositoryOptions};
44use crate::error::{Error, NotARepositoryReason, Result};
45use crate::git_dir::io_error::{map_git_dir_io, map_index_write_error, GitDirPathOperation};
46use crate::git_path::path_for_error_message;
47use crate::hooks::run_hook;
48use crate::index::Index;
49use crate::init_filesystem::{apply_init_filesystem_config, InitFilesystemConfigOptions};
50use crate::objects::{HashAlgo, ObjectId};
51use crate::odb::Odb;
52use crate::repo_caches::RepoCaches;
53use crate::rev_parse::is_inside_work_tree;
54use crate::sparse_checkout::effective_cone_mode_for_sparse_file;
55use crate::split_index::{write_index_file_split, WriteSplitIndexRequest};
56use crate::worktree::io_error::{map_worktree_io, WorktreePathOperation};
57use crate::worktree_cwd::cwd_relative_under_work_tree;
58
59fn compute_git_prefix(env: &Environment, work_tree: &Path) -> Option<String> {
61 if let Some(prefix) = env.git_prefix.as_deref() {
62 let p = prefix.trim();
63 if !p.is_empty() {
64 return Some(p.to_owned());
65 }
66 }
67 let rel = cwd_relative_under_work_tree(work_tree, &env.cwd)?;
68 if rel.is_empty() {
69 None
70 } else {
71 Some(rel)
72 }
73}
74
75fn read_sparse_checkout_patterns(git_dir: &Path) -> Vec<String> {
76 let path = git_dir.join("info").join("sparse-checkout");
77 let Ok(content) = fs::read_to_string(&path) else {
78 return Vec::new();
79 };
80 content
81 .lines()
82 .map(|l| l.trim())
83 .filter(|l| !l.is_empty() && !l.starts_with('#'))
84 .map(String::from)
85 .collect()
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct DiscoveryInfo {
91 pub explicit_git_dir: bool,
93 pub discovery_root: Option<PathBuf>,
95 pub work_tree_from_env: bool,
97 pub discovery_via_gitfile: bool,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct InitOutcome {
104 pub reinitialized: bool,
106 pub initial_branch: String,
108}
109
110#[derive(Debug, Clone)]
112#[non_exhaustive]
113pub struct InitOptions {
114 pub bare: bool,
116 pub initial_branch: Option<String>,
122 pub template_dir: Option<PathBuf>,
124 pub ref_storage: crate::ref_storage::RefStorageFormat,
126 pub separate_git_dir: Option<PathBuf>,
128 pub bare_with_worktree_at_path: bool,
130 pub object_format: Option<HashAlgo>,
134 pub environment: Option<Environment>,
139}
140
141impl Default for InitOptions {
142 fn default() -> Self {
143 Self {
144 bare: false,
145 initial_branch: None,
146 template_dir: None,
147 ref_storage: crate::ref_storage::RefStorageFormat::Files,
148 separate_git_dir: None,
149 bare_with_worktree_at_path: false,
150 object_format: None,
151 environment: None,
152 }
153 }
154}
155
156impl InitOptions {
157 #[must_use]
159 pub fn new() -> Self {
160 Self::default()
161 }
162
163 #[must_use]
165 pub fn with_bare(mut self, bare: bool) -> Self {
166 self.bare = bare;
167 self
168 }
169
170 #[must_use]
172 pub fn with_initial_branch(mut self, initial_branch: impl Into<String>) -> Self {
173 self.initial_branch = Some(initial_branch.into());
174 self
175 }
176
177 #[must_use]
179 pub fn with_object_format(mut self, object_format: HashAlgo) -> Self {
180 self.object_format = Some(object_format);
181 self
182 }
183
184 #[must_use]
186 pub fn with_environment(mut self, environment: Environment) -> Self {
187 self.environment = Some(environment);
188 self
189 }
190
191 #[must_use]
193 pub fn with_ref_storage(mut self, ref_storage: crate::ref_storage::RefStorageFormat) -> Self {
194 self.ref_storage = ref_storage;
195 self
196 }
197
198 #[must_use]
200 pub fn with_template_dir(mut self, template_dir: Option<PathBuf>) -> Self {
201 self.template_dir = template_dir;
202 self
203 }
204
205 #[must_use]
207 pub fn with_separate_git_dir(mut self, separate_git_dir: Option<PathBuf>) -> Self {
208 self.separate_git_dir = separate_git_dir;
209 self
210 }
211
212 #[must_use]
214 pub fn with_bare_with_worktree_at_path(mut self, bare_with_worktree_at_path: bool) -> Self {
215 self.bare_with_worktree_at_path = bare_with_worktree_at_path;
216 self
217 }
218}
219
220#[must_use]
269pub struct RepositoryBuilder {
270 options: RepositoryOptions,
271 odb_builder: Option<crate::odb::OdbBuilder>,
272 ref_store: Option<Arc<dyn crate::refs::store::RefStore>>,
273 initial_branch: Option<String>,
274}
275
276impl RepositoryBuilder {
277 pub fn new(options: RepositoryOptions) -> Self {
279 Self {
280 options,
281 odb_builder: None,
282 ref_store: None,
283 initial_branch: None,
284 }
285 }
286
287 pub fn odb_builder(mut self, builder: crate::odb::OdbBuilder) -> Self {
296 self.odb_builder = Some(builder);
297 self
298 }
299
300 pub fn ref_store(mut self, store: Arc<dyn crate::refs::store::RefStore>) -> Self {
311 self.ref_store = Some(store);
312 self
313 }
314
315 pub fn initial_branch(mut self, branch: impl Into<String>) -> Self {
320 self.initial_branch = Some(branch.into());
321 self
322 }
323
324 pub fn open(self, git_dir: &Path, work_tree: Option<&Path>) -> Result<Repository> {
345 let mut options = self.options;
346 if let Some(wt) = work_tree {
347 options.work_tree = Some(wt.to_path_buf());
348 }
349 let repo = if let Some(builder) = self.odb_builder {
350 Repository::open_with_odb_internal(&options, git_dir, builder, self.ref_store)?
351 } else {
352 Repository::open_internal_with_refs(&options, git_dir, self.ref_store)?
353 };
354 if let Some(branch) = self.initial_branch {
355 seed_unborn_head(repo.refs(), &branch)?;
356 }
357 Ok(repo)
358 }
359}
360
361fn seed_unborn_head(store: &dyn crate::refs::store::RefStore, branch: &str) -> Result<()> {
363 use crate::refs::store::{Expected, RawRef, RefStoreError, RefTransaction, RefUpdate};
364
365 validate_branch_short_name(branch).map_err(|e| Error::InvalidRef(e.to_string()))?;
366 if store.read_raw("HEAD")?.is_some() {
367 return Ok(());
368 }
369 let txn = RefTransaction::new().update(RefUpdate {
370 name: "HEAD".to_owned(),
371 new_value: Some(RawRef::Symbolic(format!("refs/heads/{branch}"))),
372 expected: Expected::Missing,
373 reflog: None,
374 flags: crate::refs::store::RefUpdateFlags {
375 no_deref: true,
376 ..Default::default()
377 },
378 })?;
379 match store.prepare(txn).and_then(|prepared| prepared.commit()) {
380 Ok(()) | Err(RefStoreError::ExpectedMismatch { .. }) => Ok(()),
381 Err(err) => Err(err.into()),
382 }
383}
384
385pub struct Repository {
423 git_dir: PathBuf,
424 common_git_dir: PathBuf,
426 work_tree: Option<PathBuf>,
427 odb: Odb,
428 explicit_git_dir: bool,
429 discovery_root: Option<PathBuf>,
430 work_tree_from_env: bool,
431 discovery_via_gitfile: bool,
432 config_session: Mutex<Option<ConfigSession>>,
434 caches: Arc<RepoCaches>,
436 ref_store: crate::refs::store::ObjectCheckedRefStore,
438 environment: Arc<Environment>,
440 git_prefix: Option<String>,
442 command_runner: Arc<dyn CommandRunner>,
444 change_id_generator: Arc<dyn ChangeIdGenerator>,
446 diagnostics: DiagnosticsHandle,
447 network_trace: bool,
448 reference_unix_time: Option<i64>,
450 test_assume_different_owner: bool,
451 thread_policy: crate::parallel::ThreadPolicy,
452 hook_stdio: crate::hooks::HookStdio,
453}
454
455const _: () = {
456 fn assert_send_sync<T: Send + Sync>() {}
457 let _ = assert_send_sync::<Repository>;
458};
459
460impl std::fmt::Debug for Repository {
461 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
462 f.debug_struct("Repository")
463 .field("git_dir", &self.git_dir())
464 .field("work_tree", &self.work_tree())
465 .field("explicit_git_dir", &self.discovery_info().explicit_git_dir)
466 .field("network_trace", &self.network_trace)
467 .finish_non_exhaustive()
468 }
469}
470
471pub(crate) type RepositoryConfigSnapshot = Arc<RepoCaches>;
473
474#[derive(Debug, Clone)]
476struct RepoCachedSettings {
477 use_replace_refs: bool,
479 replace_ref_base: String,
481}
482
483#[derive(Clone)]
485struct ConfigSessionValidation {
486 key: ConfigCacheKey,
487 env_fingerprint: Vec<(String, Option<String>)>,
488 base_stamps: Vec<(PathBuf, Option<(std::time::SystemTime, u64)>)>,
489 extra_stamps: Vec<(PathBuf, Option<(std::time::SystemTime, u64)>)>,
490 environment: Arc<Environment>,
494}
495
496struct ConfigSession {
498 config: Arc<ConfigSet>,
499 validation: ConfigSessionValidation,
500 cached_settings: RepoCachedSettings,
501 replace_lookup_inactive: bool,
503}
504
505impl ConfigSessionValidation {
506 fn still_valid(
512 &mut self,
513 env: &Arc<Environment>,
514 git_dir: &Path,
515 opts: impl FnOnce() -> LoadConfigOptions,
516 ) -> bool {
517 if Arc::ptr_eq(env, &self.environment) && self.key.git_dir.as_deref() == Some(git_dir) {
518 return stamps_unchanged(&self.base_stamps) && stamps_unchanged(&self.extra_stamps);
519 }
520 let opts = &opts();
521 let Some(env_fp) = env.config_fingerprint() else {
522 return false;
523 };
524 if self.env_fingerprint.as_slice() != env_fp.as_slice() {
525 return false;
526 }
527 let key = ConfigCacheKey::new(Some(git_dir), opts);
528 if key != self.key {
529 return false;
530 }
531 let base_stamps = config_file_stamps(env, Some(git_dir), opts);
532 if self.base_stamps.as_slice() != base_stamps.as_slice()
533 || !stamps_unchanged(&self.extra_stamps)
534 {
535 return false;
536 }
537 self.environment = Arc::clone(env);
538 true
539 }
540}
541
542fn stamps_unchanged(stamps: &[(PathBuf, Option<(std::time::SystemTime, u64)>)]) -> bool {
543 stamps
544 .iter()
545 .all(|(path, stamp)| stamp_for_path(path) == *stamp)
546}
547
548#[allow(clippy::too_many_arguments)]
549fn checked_ref_store_with_hooks(
550 inner: Arc<dyn crate::refs::store::RefStore>,
551 odb: crate::odb::Odb,
552 git_dir: &Path,
553 work_tree: Option<&Path>,
554 environment: &Arc<Environment>,
555 command_runner: &Arc<dyn CommandRunner>,
556 hook_stdio: crate::hooks::HookStdio,
557 diagnostics: &crate::diagnostics::DiagnosticsHandle,
558) -> crate::refs::store::ObjectCheckedRefStore {
559 let hooks = Arc::new(crate::hooks::RefHookContext::new(
560 git_dir.to_path_buf(),
561 work_tree.map(Path::to_path_buf),
562 Arc::clone(environment),
563 Arc::clone(command_runner),
564 hook_stdio,
565 Arc::clone(diagnostics),
566 odb.hash_algo(),
567 ));
568 crate::refs::store::ObjectCheckedRefStore::new(inner, odb).with_ref_hooks(hooks)
569}
570
571impl Repository {
572 fn config_load_options(&self, include_system: bool) -> LoadConfigOptions {
573 LoadConfigOptions {
574 include_system,
575 diagnostics: Some(self.diagnostics()),
576 ..LoadConfigOptions::for_repository(
577 &self.environment,
578 Some(&self.git_dir),
579 Some(Arc::clone(&self.caches)),
580 )
581 }
582 }
583
584 fn from_canonical_git_dir(
585 options: &RepositoryOptions,
586 git_dir: PathBuf,
587 work_tree: Option<&Path>,
588 injected_refs: Option<Arc<dyn crate::refs::store::RefStore>>,
589 ) -> Result<Self> {
590 let environment = Arc::new(options.environment.clone());
591 let command_runner = Arc::clone(&options.command_runner);
592 let change_id_generator = Arc::clone(&options.change_id_generator);
593 if injected_refs.is_none() && !head_file_present(&git_dir) {
596 return Err(not_a_repository_at(&git_dir));
597 }
598
599 let common_git_dir = resolve_common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
600 let objects_dir = resolve_objects_dir(&environment, &git_dir, &common_git_dir)
601 .filter(|dir| dir.exists())
602 .ok_or_else(|| not_a_repository_at(&git_dir))?;
603
604 let work_tree = match work_tree {
605 Some(p) => {
606 let cwd = environment.discovery_cwd();
607 let mut resolved = if p.is_absolute() {
608 p.to_path_buf()
609 } else {
610 cwd.join(p)
611 };
612 if resolved.exists() {
613 resolved = resolved
614 .canonicalize_simplified()
615 .map_err(|_| Error::PathError(p.display().to_string()))?;
616 }
617 Some(resolved)
618 }
619 None => None,
620 };
621
622 let caches = RepoCaches::with_diagnostics(
623 Arc::clone(&command_runner),
624 Arc::clone(&options.diagnostics),
625 );
626 caches.set_promisor_fetcher(options.promisor_fetcher.clone());
627 let odb = if let Some(ref wt) = work_tree {
628 Odb::with_work_tree(&objects_dir, wt)
629 .with_config_git_dir(git_dir.clone())
630 .with_shared_config_state(caches.clone(), environment.clone())
631 } else {
632 Odb::new(&objects_dir)
633 .with_config_git_dir(git_dir.clone())
634 .with_shared_config_state(caches.clone(), environment.clone())
635 };
636
637 let git_prefix = work_tree
638 .as_ref()
639 .and_then(|wt| compute_git_prefix(environment.as_ref(), wt));
640
641 let ref_store = checked_ref_store_with_hooks(
642 open_or_install_ref_store(&caches, &git_dir, &environment, injected_refs)?,
643 odb.clone(),
644 &git_dir,
645 work_tree.as_deref(),
646 &environment,
647 &command_runner,
648 options.hook_stdio,
649 &options.diagnostics,
650 );
651 caches.configure_filter_subprocess(&environment, &git_dir, work_tree.as_deref());
652
653 Ok(Self {
654 git_dir,
655 common_git_dir,
656 work_tree,
657 odb,
658 explicit_git_dir: false,
659 discovery_root: None,
660 work_tree_from_env: false,
661 discovery_via_gitfile: false,
662 config_session: Mutex::new(None),
663 caches,
664 ref_store,
665 environment,
666 git_prefix,
667 command_runner,
668 change_id_generator,
669 diagnostics: Arc::clone(&options.diagnostics),
670 network_trace: options.network_trace,
671 reference_unix_time: options.reference_unix_time,
672 test_assume_different_owner: options.test_assume_different_owner,
673 thread_policy: options.thread_policy,
674 hook_stdio: options.hook_stdio,
675 })
676 }
677
678 #[must_use]
680 pub fn thread_policy(&self) -> crate::parallel::ThreadPolicy {
681 self.thread_policy
682 }
683
684 #[must_use]
686 pub fn with_thread_policy(mut self, policy: crate::parallel::ThreadPolicy) -> Self {
687 self.thread_policy = policy;
688 self
689 }
690
691 #[must_use]
693 pub(crate) fn caches(&self) -> &Arc<RepoCaches> {
694 &self.caches
695 }
696
697 #[must_use]
699 pub(crate) fn promisor_hydrate_hook(&self) -> Option<crate::blame::PromisorHydrateHook> {
700 self.caches.promisor_hydrate_hook()
701 }
702
703 #[must_use]
705 pub fn environment(&self) -> &Environment {
706 &self.environment
707 }
708
709 #[must_use]
711 pub fn reference_unix_time(&self) -> i64 {
712 self.wall_clock_epoch()
713 }
714
715 #[must_use]
717 pub fn wall_clock_epoch(&self) -> i64 {
718 if let Some(ts) = self.reference_unix_time {
719 return ts;
720 }
721 if let Some(ts) = self.environment.git_now_date_override() {
722 return ts;
723 }
724 crate::git_date::tm::process_wall_clock_sec()
725 }
726
727 #[must_use]
731 pub fn identity_now(&self) -> time::OffsetDateTime {
732 crate::environment::offset_from_epoch_and_tz(
733 self.wall_clock_epoch(),
734 self.environment.tz.as_deref(),
735 )
736 }
737
738 pub fn identity(
772 &self,
773 role: crate::config::ident_resolve::IdentRole,
774 now: time::OffsetDateTime,
775 ) -> Result<crate::ident::Signature> {
776 let config = self.config()?;
777 Ok(crate::config::ident_resolve::resolve_signature_with(
778 self.environment(),
779 &config,
780 role,
781 now,
782 )?)
783 }
784
785 #[must_use]
787 pub fn command_runner(&self) -> Arc<dyn CommandRunner> {
788 Arc::clone(&self.command_runner)
789 }
790
791 #[must_use]
794 pub fn hook_stdio(&self) -> crate::hooks::HookStdio {
795 self.hook_stdio
796 }
797
798 #[must_use]
800 pub fn change_id_generator(&self) -> Arc<dyn ChangeIdGenerator> {
801 Arc::clone(&self.change_id_generator)
802 }
803
804 #[must_use]
810 pub fn linked_worktree_discover_options(&self) -> RepositoryOptions {
811 let mut environment = (*self.environment).clone();
812 environment.git_dir = None;
813 environment.git_work_tree = None;
814 environment.git_index_file = None;
815 environment.git_prefix = None;
816 environment.git_object_directory = None;
817 RepositoryOptions {
818 environment,
819 command_runner: Arc::clone(&self.command_runner),
820 change_id_generator: Arc::clone(&self.change_id_generator),
821 diagnostics: Arc::clone(&self.diagnostics),
822 network_trace: self.network_trace,
823 test_assume_different_owner: self.test_assume_different_owner,
824 reference_unix_time: self.reference_unix_time,
825 thread_policy: self.thread_policy,
826 work_tree: None,
827 validate_repository_format: true,
828 open_for_serving: false,
829 promisor_fetcher: self.caches().promisor_fetcher(),
830 hook_stdio: self.hook_stdio,
831 }
832 }
833
834 #[must_use]
843 pub(crate) fn peer_repository_options(&self) -> RepositoryOptions {
844 let mut options = self.linked_worktree_discover_options();
845 let env = &mut options.environment;
846 env.git_alternate_object_directories = None;
847 env.git_namespace = None;
848 env.git_replace_ref_base = None;
849 env.git_no_replace_objects = false;
850 env.git_config = None;
851 env.git_config_parameters = None;
852 env.git_config_count = None;
853 env.git_config_pairs.clear();
854 options
855 }
856
857 pub fn open_linked_worktree(&self, admin_dir: &Path, work_tree: &Path) -> Result<Self> {
873 let options = self.linked_worktree_discover_options();
874 if self.refs_on_disk() && self.odb.uses_files_primary() {
875 return Self::discover(Some(work_tree), &options);
876 }
877 let git_dir = admin_dir
878 .canonicalize_simplified()
879 .map_err(|_| not_a_repository_at(admin_dir))?;
880 let work_tree = work_tree
881 .canonicalize_simplified()
882 .map_err(|_| Error::PathError(work_tree.display().to_string()))?;
883 let environment = Arc::new(options.environment);
884 let caches = RepoCaches::with_diagnostics(
885 Arc::clone(&self.command_runner),
886 Arc::clone(&self.diagnostics),
887 );
888 let ref_store = if self.refs_on_disk() {
889 caches.open_ref_store(&git_dir, &environment)?
890 } else {
891 let view: Arc<dyn crate::refs::store::RefStore> =
892 Arc::new(crate::refs::store::LinkedWorktreeRefStore::new(
893 git_dir.clone(),
894 Arc::clone(self.ref_store.inner()),
895 ));
896 caches.install_ref_store(&git_dir, Arc::clone(&view));
897 view
898 };
899 let ref_store = checked_ref_store_with_hooks(
900 ref_store,
901 self.odb.clone(),
902 &git_dir,
903 Some(work_tree.as_path()),
904 &environment,
905 &self.command_runner,
906 self.hook_stdio,
907 &self.diagnostics,
908 );
909 let repo = Self {
910 git_prefix: compute_git_prefix(&environment, &work_tree),
911 git_dir,
912 common_git_dir: self.common_git_dir.clone(),
913 work_tree: Some(work_tree),
914 odb: self.odb.clone(),
915 explicit_git_dir: false,
916 discovery_root: None,
917 work_tree_from_env: false,
918 discovery_via_gitfile: true,
919 config_session: Mutex::new(None),
920 caches,
921 ref_store,
922 environment,
923 command_runner: Arc::clone(&self.command_runner),
924 change_id_generator: Arc::clone(&self.change_id_generator),
925 diagnostics: Arc::clone(&self.diagnostics),
926 network_trace: self.network_trace,
927 reference_unix_time: self.reference_unix_time,
928 test_assume_different_owner: self.test_assume_different_owner,
929 thread_policy: self.thread_policy,
930 hook_stdio: self.hook_stdio,
931 };
932 repo.caches().configure_filter_subprocess(
933 &repo.environment,
934 repo.git_dir(),
935 repo.work_tree(),
936 );
937 repo.refresh_config_session()?;
938 Ok(repo)
939 }
940
941 fn repo_cached_settings_from_config(
942 cfg: &ConfigSet,
943 environment: &Environment,
944 ) -> RepoCachedSettings {
945 let use_replace_refs = cfg
946 .get_bool("core.useReplaceRefs")
947 .and_then(|r| r.ok())
948 .unwrap_or(true);
949 let replace_ref_base = environment
950 .git_replace_ref_base
951 .clone()
952 .filter(|s| !s.is_empty())
953 .unwrap_or_else(|| "refs/replace/".to_owned());
954 let replace_ref_base = if replace_ref_base.ends_with('/') {
955 replace_ref_base
956 } else {
957 format!("{replace_ref_base}/")
958 };
959 RepoCachedSettings {
960 use_replace_refs,
961 replace_ref_base,
962 }
963 }
964
965 fn ensure_config_arc(&self) -> Result<Arc<ConfigSet>> {
966 self.config_arc_with_validation(true)
967 }
968
969 pub(crate) fn ensure_config_arc_fast(&self) -> Result<Arc<ConfigSet>> {
971 self.config_arc_with_validation(false)
972 }
973
974 fn config_arc_with_validation(&self, validate_stamps: bool) -> Result<Arc<ConfigSet>> {
975 if validate_stamps {
976 if let Ok(mut guard) = self.config_session.lock() {
977 if let Some(session) = guard.as_mut() {
978 if session
979 .validation
980 .still_valid(&self.environment, &self.git_dir, || {
981 self.config_load_options(true)
982 })
983 {
984 return Ok(Arc::clone(&session.config));
985 }
986 }
987 }
988 } else if let Ok(guard) = self.config_session.lock() {
989 if let Some(session) = guard.as_ref() {
990 return Ok(Arc::clone(&session.config));
991 }
992 }
993 self.refresh_config_session()?;
994 self.config_session
995 .lock()
996 .map_err(|e| Error::Message(format!("config session lock poisoned: {e}")))?
997 .as_ref()
998 .map(|s| Arc::clone(&s.config))
999 .ok_or_else(|| Error::Message("config session missing after refresh".to_owned()))
1000 }
1001
1002 fn refresh_config_session(&self) -> Result<()> {
1003 crate::trace::trace_span!("grit.config.load");
1004 let opts = self.config_load_options(true);
1005 if let Ok(mut guard) = self.config_session.lock() {
1006 if let Some(session) = guard.as_mut() {
1007 if session
1008 .validation
1009 .still_valid(&self.environment, &self.git_dir, || opts.clone())
1010 {
1011 return Ok(());
1012 }
1013 let env = self.environment.as_ref();
1014 let base_stamps = config_file_stamps(env, Some(&self.git_dir), &opts);
1015 if session.validation.base_stamps.as_slice() != base_stamps.as_slice() {
1016 self.caches.invalidate_config_file_derived();
1017 }
1018 }
1019 }
1020 self.caches.clear_session_config();
1021 let env = self.environment.as_ref();
1022 let cfg = self
1023 .caches
1024 .load_config_with_options(env, Some(&self.git_dir), &opts)?;
1025 let key = ConfigCacheKey::new(Some(&self.git_dir), &opts);
1026 let (env_fingerprint, base_stamps, extra_stamps) =
1027 self.caches.config_entry_stamps(&key).unwrap_or_else(|| {
1028 (
1029 env.config_fingerprint().unwrap_or_default(),
1030 config_file_stamps(env, Some(&self.git_dir), &opts),
1031 Vec::new(),
1032 )
1033 });
1034 let cached_settings =
1035 Self::repo_cached_settings_from_config(cfg.as_ref(), self.environment.as_ref());
1036 let replace_lookup_inactive = self.replace_lookup_inactive(cfg.as_ref());
1037 self.caches
1038 .set_session_config(key.clone(), Arc::clone(&cfg));
1039 let session = ConfigSession {
1040 config: Arc::clone(&cfg),
1041 validation: ConfigSessionValidation {
1042 key,
1043 env_fingerprint,
1044 base_stamps,
1045 extra_stamps,
1046 environment: Arc::clone(&self.environment),
1047 },
1048 cached_settings,
1049 replace_lookup_inactive,
1050 };
1051 *self
1052 .config_session
1053 .lock()
1054 .map_err(|e| Error::Message(format!("config session lock poisoned: {e}")))? =
1055 Some(session);
1056 Ok(())
1057 }
1058
1059 fn replace_lookup_inactive(&self, cfg: &ConfigSet) -> bool {
1060 if self.environment.git_no_replace_objects {
1061 return true;
1062 }
1063 if !cfg
1064 .get_bool("core.useReplaceRefs")
1065 .and_then(|r| r.ok())
1066 .unwrap_or(true)
1067 {
1068 return true;
1069 }
1070 let replace_dir = self.common_git_dir.join("refs/replace");
1071 !replace_dir_is_non_empty(&replace_dir)
1072 }
1073
1074 #[must_use]
1076 pub fn diagnostics(&self) -> DiagnosticsHandle {
1077 Arc::clone(&self.diagnostics)
1078 }
1079
1080 pub fn set_diagnostics(&mut self, sink: DiagnosticsHandle) {
1082 self.caches.set_diagnostics(Arc::clone(&sink));
1083 self.diagnostics = sink;
1084 }
1085
1086 #[must_use]
1088 pub fn network_trace_enabled(&self) -> bool {
1089 self.network_trace
1090 }
1091
1092 pub fn set_network_trace(&mut self, enabled: bool) {
1094 self.network_trace = enabled;
1095 }
1096
1097 pub(crate) fn warn(&self, warning: diagnostics::Warning) {
1098 self.diagnostics.warn(warning);
1099 }
1100
1101 pub fn config(&self) -> Result<Arc<ConfigSet>> {
1112 self.ensure_config_arc()
1113 }
1114
1115 pub fn reload_config(&self) -> Result<()> {
1124 self.caches.invalidate_config_cache();
1125 if let Ok(mut guard) = self.config_session.lock() {
1126 *guard = None;
1127 }
1128 self.refresh_config_session()?;
1129 Ok(())
1130 }
1131
1132 pub(crate) fn install_config_snapshot(&self) {
1134 let _ = self
1135 .caches
1136 .open_ref_store(&self.git_dir, self.environment.as_ref());
1137 }
1138
1139 #[must_use]
1146 pub fn common_git_dir(&self) -> &Path {
1147 &self.common_git_dir
1148 }
1149
1150 fn validate_repository_format_cached(&self) -> Result<()> {
1151 let Some(config_path) = repository_config_path(&self.git_dir) else {
1152 return Ok(());
1153 };
1154 let parsed = self.caches.repository_format(&self.git_dir, &config_path)?;
1155 validate_repository_format_parsed(&parsed)
1156 }
1157
1158 #[must_use]
1167 pub fn refs(&self) -> &dyn crate::refs::store::RefStore {
1168 &self.ref_store
1169 }
1170
1171 #[must_use]
1175 pub(crate) fn refs_on_disk(&self) -> bool {
1176 use crate::refs::store::RefStorageFormat;
1177 matches!(
1178 self.refs().format(),
1179 RefStorageFormat::Files | RefStorageFormat::Reftable
1180 )
1181 }
1182
1183 #[inline]
1185 #[must_use]
1186 pub fn objects(&self) -> crate::objects::Objects<'_> {
1187 crate::objects::Objects::new(self)
1188 }
1189
1190 #[inline]
1192 #[must_use]
1193 pub fn references(&self) -> crate::refs::References<'_> {
1194 crate::refs::References::new(self)
1195 }
1196
1197 #[inline]
1199 #[must_use]
1200 pub fn index_handle(&self) -> crate::index::IndexHandle<'_> {
1201 crate::index::IndexHandle::new(self)
1202 }
1203
1204 #[inline]
1206 #[must_use]
1207 pub fn config_handle(&self) -> crate::config::ConfigHandle<'_> {
1208 crate::config::ConfigHandle::new(self)
1209 }
1210
1211 #[inline]
1213 #[must_use]
1214 pub fn rev_walk(&self) -> crate::revwalk::RevWalk<'_> {
1215 crate::revwalk::RevWalk::new(self)
1216 }
1217
1218 #[inline]
1220 #[must_use]
1221 pub fn diff(&self) -> crate::diff::Diff<'_> {
1222 crate::diff::Diff::new(self)
1223 }
1224
1225 #[must_use]
1227 pub fn remotes(&self) -> crate::repo_handles::RepositoryRemotes<'_> {
1228 crate::repo_handles::RepositoryRemotes::new(self)
1229 }
1230
1231 #[must_use]
1233 pub fn worktrees(&self) -> crate::repo_handles::RepositoryWorktrees<'_> {
1234 crate::repo_handles::RepositoryWorktrees::new(self)
1235 }
1236
1237 #[must_use]
1239 pub fn stash(&self) -> crate::repo_handles::RepositoryStash<'_> {
1240 crate::repo_handles::RepositoryStash::new(self)
1241 }
1242
1243 #[must_use]
1245 pub fn workspace_snapshot(&self) -> crate::repo_handles::RepositoryWorkspaceSnapshot<'_> {
1246 crate::repo_handles::RepositoryWorkspaceSnapshot::new(self)
1247 }
1248
1249 #[must_use]
1251 pub fn notes(&self) -> crate::repo_handles::RepositoryNotes<'_> {
1252 crate::repo_handles::RepositoryNotes::new(self)
1253 }
1254
1255 #[must_use]
1257 pub fn hooks(&self) -> crate::repo_handles::RepositoryHooks<'_> {
1258 crate::repo_handles::RepositoryHooks::new(self)
1259 }
1260
1261 pub fn fsck(
1270 &self,
1271 options: &crate::fsck::FsckOptions,
1272 progress: &mut dyn crate::progress::ProgressSink,
1273 cancel: &dyn crate::progress::Cancel,
1274 ) -> crate::error::Result<crate::fsck::FsckReport> {
1275 crate::fsck::fsck(self, options, progress, cancel)
1276 }
1277
1278 pub fn tree_sha256(
1287 &self,
1288 oid: crate::objects::ObjectId,
1289 options: &crate::tree_sha256::TreeSha256Options,
1290 ) -> crate::error::Result<crate::tree_sha256::TreeSha256Report> {
1291 crate::tree_sha256::tree_sha256(self, oid, options)
1292 }
1293
1294 pub fn verify_tag(
1303 &self,
1304 oid: crate::objects::ObjectId,
1305 options: &crate::tree_sha256::TagVerifyOptions,
1306 ) -> crate::error::Result<crate::tree_sha256::TagVerification> {
1307 crate::tree_sha256::verify_tag(self, oid, options)
1308 }
1309
1310 pub fn patch_id(
1324 &self,
1325 commit: &crate::objects::ObjectId,
1326 mode: crate::patch_ids::PatchIdMode,
1327 ) -> crate::error::Result<Option<crate::objects::ObjectId>> {
1328 crate::patch_ids::compute_patch_id(self.odb().hash_algo(), self.odb(), commit, mode)
1329 }
1330
1331 pub fn patch_ids_for_revs(
1348 &self,
1349 specs: &[&str],
1350 mode: crate::patch_ids::PatchIdMode,
1351 ) -> crate::error::Result<Vec<(crate::objects::ObjectId, crate::objects::ObjectId)>> {
1352 let walk = self.rev_walk();
1353 let walk_options = crate::revwalk::RevWalkListOptions::default();
1354 let mut out = Vec::new();
1355 for spec in specs {
1356 let commits = if crate::rev_parse::is_top_level_range(spec) {
1357 walk.commits(&[spec], &walk_options)?
1358 } else {
1359 vec![crate::rev_parse::resolve_revision_without_index_dwim(
1360 self, spec,
1361 )?]
1362 };
1363 let patch_ids =
1364 crate::rev_list::map_commits_in_parallel(&commits, |c| self.patch_id(c, mode));
1365 for (patch_id, commit) in patch_ids.into_iter().zip(commits) {
1366 if let Some(patch_id) = patch_id? {
1367 out.push((patch_id, commit));
1368 }
1369 }
1370 }
1371 Ok(out)
1372 }
1373
1374 #[must_use]
1376 pub fn reflogs(&self) -> crate::repo_handles::RepositoryReflogs<'_> {
1377 crate::repo_handles::RepositoryReflogs::new(self)
1378 }
1379
1380 #[must_use]
1382 pub fn bundles(&self) -> crate::repo_handles::RepositoryBundles<'_> {
1383 crate::repo_handles::RepositoryBundles::new(self)
1384 }
1385
1386 pub fn repack(
1392 &self,
1393 opts: &crate::maintenance::RepackOptions,
1394 ) -> crate::error::Result<crate::maintenance::RepackReport> {
1395 crate::maintenance::repack(self, opts)
1396 }
1397
1398 pub(crate) fn resolve_ref_oid(&self, refname: &str) -> Result<crate::objects::ObjectId> {
1404 self.refs().resolve(refname)
1405 }
1406
1407 #[must_use]
1412 pub fn with_ref_store(mut self, store: Arc<dyn crate::refs::store::RefStore>) -> Self {
1413 let store = linked_worktree_view(&self.git_dir, store);
1414 self.caches
1415 .install_ref_store(&self.git_dir, Arc::clone(&store));
1416 self.ref_store = checked_ref_store_with_hooks(
1417 store,
1418 self.odb.clone(),
1419 self.git_dir(),
1420 self.work_tree().map(Path::as_ref),
1421 &self.environment,
1422 &self.command_runner,
1423 self.hook_stdio,
1424 &self.diagnostics,
1425 );
1426 self
1427 }
1428
1429 #[must_use]
1431 pub(crate) fn pathspec_precompose_enabled(&self) -> bool {
1432 self.caches
1433 .pathspec_precompose_enabled(self.environment.as_ref(), &self.git_dir)
1434 }
1435
1436 #[cfg(test)]
1439 pub(crate) fn force_pathspec_precompose_for_test(&self) {
1440 self.caches.force_pathspec_precompose_for_test();
1441 }
1442
1443 pub fn resolve_worktree_pathspec(
1457 &self,
1458 pathspec: &str,
1459 prefix: Option<&str>,
1460 ) -> std::result::Result<String, crate::pathspec::PathspecResolveError> {
1461 let work_tree = self
1462 .work_tree()
1463 .ok_or(crate::pathspec::PathspecResolveError::NeedsWorkTree)?;
1464 Ok(
1465 crate::pathspec::resolve_pathspec_in_worktree_with_argv_precompose(
1466 pathspec,
1467 pathspec,
1468 work_tree,
1469 prefix,
1470 self.pathspec_precompose_enabled(),
1471 )?,
1472 )
1473 }
1474
1475 #[must_use]
1476 pub(crate) fn pathspec_match_context(&self) -> crate::pathspec::PathspecMatchContext {
1477 crate::pathspec::pathspec_match_context_with_precompose(self.pathspec_precompose_enabled())
1478 }
1479
1480 pub fn paths_match_pathspecs(&self, path: &str, specs: &[String]) -> Result<bool> {
1490 Ok(self.pathspec_matcher(specs)?.matches(path))
1491 }
1492
1493 pub fn pathspec_matcher<'s>(
1509 &self,
1510 specs: &'s [String],
1511 ) -> Result<crate::pathspec::PathspecMatcher<'s>> {
1512 let precompose = self.pathspec_precompose_enabled();
1513 if !specs
1514 .iter()
1515 .any(|spec| crate::pathspec::pathspec_has_attr_magic(spec))
1516 {
1517 return Ok(crate::pathspec::PathspecMatcher::new(
1518 specs, precompose, None, false,
1519 ));
1520 }
1521 crate::pathspec::validate_attr_pathspecs(specs)?;
1522 let attrs = match self.work_tree() {
1523 Some(work_tree) => self.caches().gitattributes_stack_shared(self, work_tree)?,
1524 None => self.caches().gitattributes_bare_shared(self)?,
1525 };
1526 let icase = crate::crlf::config_ignorecase(self.config()?.as_ref());
1527 Ok(crate::pathspec::PathspecMatcher::new(
1528 specs,
1529 precompose,
1530 Some(attrs),
1531 icase,
1532 ))
1533 }
1534
1535 #[must_use]
1540 pub fn pathspec_cwd_prefix(&self) -> Option<&str> {
1541 self.git_prefix.as_deref().filter(|p| !p.is_empty())
1542 }
1543
1544 pub fn pathspec_argv_prefix<'a>(&'a self, pathspec: &str) -> Option<&'a str> {
1546 if crate::pathspec::pathspec_has_top(pathspec) {
1547 None
1548 } else {
1549 self.pathspec_cwd_prefix()
1550 }
1551 }
1552
1553 pub fn resolve_rev_walk_pathspecs(&self, paths: &[String]) -> Result<Vec<String>> {
1563 if paths.is_empty() {
1564 return Ok(Vec::new());
1565 }
1566 let prefix = self.pathspec_cwd_prefix();
1567 let cwd_for_implicit = prefix.map(str::to_owned);
1568 let mut resolved = Vec::with_capacity(paths.len());
1569 if self.work_tree().is_some() {
1570 for pathspec in paths {
1571 let prefix = Self::pathspec_argv_prefix(self, pathspec);
1572 let spec = self
1573 .resolve_worktree_pathspec(pathspec, prefix)
1574 .map_err(|e| Error::PathError(e.to_string()))?;
1575 resolved.push(spec);
1576 }
1577 } else {
1578 let root = self.git_dir();
1579 for pathspec in paths {
1580 let prefix = Self::pathspec_argv_prefix(self, pathspec);
1581 resolved.push(crate::pathspec::resolve_pathspec(pathspec, root, prefix));
1582 }
1583 }
1584 Ok(crate::pathspec::extend_pathspec_list_implicit_cwd(
1585 &resolved,
1586 cwd_for_implicit.as_deref(),
1587 ))
1588 }
1589
1590 #[must_use]
1597 pub fn git_dir(&self) -> &Path {
1598 &self.git_dir
1599 }
1600
1601 #[must_use]
1608 pub fn work_tree(&self) -> Option<&Path> {
1609 self.work_tree.as_deref()
1610 }
1611
1612 #[must_use]
1614 pub fn odb(&self) -> &Odb {
1615 &self.odb
1616 }
1617
1618 pub fn prefetch_promised(&self, oids: &[ObjectId]) -> Result<()> {
1624 let Some(fetcher) = self.caches().promisor_fetcher() else {
1625 return Err(Error::Message(
1626 "promisor prefetch requires a PromisorFetcher on RepositoryOptions".into(),
1627 ));
1628 };
1629 fetcher.fetch_missing(self, oids)
1630 }
1631
1632 #[must_use]
1634 pub fn discovery_info(&self) -> DiscoveryInfo {
1635 DiscoveryInfo {
1636 explicit_git_dir: self.explicit_git_dir,
1637 discovery_root: self.discovery_root.clone(),
1638 work_tree_from_env: self.work_tree_from_env,
1639 discovery_via_gitfile: self.discovery_via_gitfile,
1640 }
1641 }
1642
1643 fn cached_settings(&self) -> RepoCachedSettings {
1644 if let Ok(guard) = self.config_session.lock() {
1645 if let Some(session) = guard.as_ref() {
1646 return session.cached_settings.clone();
1647 }
1648 }
1649 self.ensure_config_arc_fast()
1650 .map(|cfg| {
1651 Self::repo_cached_settings_from_config(cfg.as_ref(), self.environment.as_ref())
1652 })
1653 .unwrap_or_else(|_| {
1654 Self::repo_cached_settings_from_config(&ConfigSet::new(), self.environment.as_ref())
1655 })
1656 }
1657
1658 pub fn init(path: &Path, options: &InitOptions) -> Result<Self> {
1664 Self::init_with_outcome(path, options).map(|(repo, _)| repo)
1665 }
1666
1667 pub fn init_with_outcome(path: &Path, options: &InitOptions) -> Result<(Self, InitOutcome)> {
1674 if options.bare && options.separate_git_dir.is_some() {
1675 return Err(Error::InitBareWithSeparateGitDir);
1676 }
1677 let env = options
1678 .environment
1679 .clone()
1680 .unwrap_or_else(Environment::empty);
1681 let git_dir = resolve_git_dir_for_init(path, options)?;
1682 let reinitialized = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
1683 let existing_format = hash_algo_from_git_dir(&git_dir)?;
1684 let object_format = match (reinitialized, options.object_format) {
1685 (true, None) => existing_format,
1686 (true, Some(requested)) if requested != existing_format => {
1687 return Err(Error::InitObjectFormatChange);
1688 }
1689 (true, Some(requested)) => requested,
1690 (false, None) => HashAlgo::Sha1,
1691 (false, Some(requested)) => requested,
1692 };
1693 let initial_branch = if reinitialized {
1694 head_branch_name_for_outcome(&git_dir)?
1695 } else {
1696 resolve_initial_branch(&env, options.initial_branch.as_deref())?
1697 };
1698
1699 let separate_layout = options.separate_git_dir.is_some()
1700 || (!options.bare
1701 && !options.bare_with_worktree_at_path
1702 && path.join(".git").is_file());
1703
1704 let repo = if options.bare_with_worktree_at_path {
1705 init_bare_with_env_worktree(
1706 path,
1707 path,
1708 &initial_branch,
1709 options.template_dir.as_deref(),
1710 options.ref_storage,
1711 object_format,
1712 &env,
1713 )?
1714 } else if separate_layout {
1715 init_repository_separate_git_dir(
1716 path,
1717 &git_dir,
1718 &initial_branch,
1719 options.template_dir.as_deref(),
1720 options.ref_storage,
1721 object_format,
1722 &env,
1723 )?
1724 } else {
1725 init_repository_with_object_format(
1726 path,
1727 options.bare,
1728 &initial_branch,
1729 options.template_dir.as_deref(),
1730 options.ref_storage,
1731 object_format,
1732 &env,
1733 )?
1734 };
1735 Ok((
1736 repo,
1737 InitOutcome {
1738 reinitialized,
1739 initial_branch,
1740 },
1741 ))
1742 }
1743
1744 pub fn open(path: &Path, options: &RepositoryOptions) -> Result<Self> {
1754 if options.open_for_serving {
1755 return Self::open_for_serving_internal(path, options);
1756 }
1757 Self::open_internal(options, path)
1758 }
1759
1760 fn open_with_work_tree(
1761 options: &RepositoryOptions,
1762 git_dir: &Path,
1763 work_tree: Option<&Path>,
1764 ) -> Result<Self> {
1765 let mut opts = options.clone();
1766 opts.work_tree = work_tree.map(|p| p.to_path_buf());
1767 Self::open_internal(&opts, git_dir)
1768 }
1769
1770 fn open_internal(options: &RepositoryOptions, git_dir: &Path) -> Result<Self> {
1771 Self::open_internal_with_refs(options, git_dir, None)
1772 }
1773
1774 fn open_internal_with_refs(
1775 options: &RepositoryOptions,
1776 git_dir: &Path,
1777 injected_refs: Option<Arc<dyn crate::refs::store::RefStore>>,
1778 ) -> Result<Self> {
1779 crate::trace::trace_span!("grit.repository.open");
1780 let work_tree = options.work_tree.as_deref();
1781 let git_dir = git_dir
1782 .canonicalize_simplified()
1783 .map_err(|_| not_a_repository_at(git_dir))?;
1784
1785 let repo = Self::from_canonical_git_dir(options, git_dir, work_tree, injected_refs)?;
1786 repo.refresh_config_session()?;
1787 if options.validate_repository_format {
1788 repo.validate_repository_format_cached()?;
1789 }
1790 repo.install_config_snapshot();
1791 warn_core_bare_worktree_conflict(options, &repo);
1792 Ok(repo)
1793 }
1794
1795 fn open_for_serving_internal(path: &Path, options: &RepositoryOptions) -> Result<Self> {
1796 let mut with_git_suffix = path.as_os_str().to_owned();
1797 with_git_suffix.push(".git");
1798 let candidates = [
1799 (path.join(".git"), Some(path.to_path_buf())),
1800 (path.to_path_buf(), None),
1801 (PathBuf::from(with_git_suffix), None),
1802 ];
1803 for (git_dir, work_tree) in candidates {
1804 if !git_dir.join("HEAD").is_file() {
1805 continue;
1806 }
1807 let mut opts = options.clone();
1808 opts.work_tree = work_tree;
1809 opts.open_for_serving = false;
1810 return Self::open_internal(&opts, &git_dir);
1811 }
1812 Err(not_a_repository_at(path))
1813 }
1814
1815 fn open_with_odb_internal(
1816 options: &RepositoryOptions,
1817 git_dir: &Path,
1818 mut builder: crate::odb::OdbBuilder,
1819 injected_refs: Option<Arc<dyn crate::refs::store::RefStore>>,
1820 ) -> Result<Self> {
1821 let work_tree = options.work_tree.as_deref();
1822 let environment = Arc::new(options.environment.clone());
1823 let cwd = environment.discovery_cwd();
1824 let command_runner = Arc::clone(&options.command_runner);
1825 let change_id_generator = Arc::clone(&options.change_id_generator);
1826
1827 let fully_injected = injected_refs.is_some() && builder.has_custom_primary();
1830 let requested_git_dir = cwd.join(git_dir);
1831 let git_dir = match requested_git_dir.canonicalize_simplified() {
1832 Ok(dir) => dir,
1833 Err(_) if fully_injected => canonicalize_existing_prefix(&requested_git_dir)?,
1834 Err(_) => return Err(not_a_repository_at(git_dir)),
1835 };
1836
1837 if injected_refs.is_none() && !head_file_present(&git_dir) {
1838 return Err(not_a_repository_at(&git_dir));
1839 }
1840
1841 let common_git_dir = resolve_common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
1842 let objects_dir = resolve_objects_dir(&environment, &git_dir, &common_git_dir)
1843 .ok_or_else(|| not_a_repository_at(&git_dir))?;
1844
1845 if !builder.has_custom_primary() && !objects_dir.exists() {
1847 return Err(not_a_repository_at(&git_dir));
1848 }
1849 let objects_dir = canonicalize_existing_prefix(&objects_dir)?;
1850 let builder_objects = canonicalize_existing_prefix(&cwd.join(builder.objects_dir()))?;
1851 if builder_objects != objects_dir {
1852 return Err(Error::OdbObjectsDirMismatch {
1853 builder: builder_objects,
1854 repository: objects_dir,
1855 });
1856 }
1857 builder.set_objects_dir(objects_dir);
1858 let injected_algos: Vec<HashAlgo> = builder.injected_hash_algos().collect();
1859
1860 let work_tree = match work_tree {
1861 Some(p) => {
1862 let mut resolved = cwd.join(p);
1863 if resolved.exists() {
1864 resolved = resolved
1865 .canonicalize_simplified()
1866 .map_err(|_| Error::PathError(p.display().to_string()))?;
1867 }
1868 Some(resolved)
1869 }
1870 None => None,
1871 };
1872
1873 let caches = RepoCaches::with_diagnostics(
1874 Arc::clone(&command_runner),
1875 Arc::clone(&options.diagnostics),
1876 );
1877 let odb = builder
1878 .build()
1879 .with_config_git_dir(git_dir.clone())
1880 .with_shared_config_state(caches.clone(), environment.clone())
1881 .with_resolved_work_tree(work_tree.clone());
1882
1883 let git_prefix = work_tree
1884 .as_ref()
1885 .and_then(|wt| compute_git_prefix(environment.as_ref(), wt));
1886
1887 let ref_store = checked_ref_store_with_hooks(
1888 open_or_install_ref_store(&caches, &git_dir, &environment, injected_refs)?,
1889 odb.clone(),
1890 &git_dir,
1891 work_tree.as_deref(),
1892 &environment,
1893 &command_runner,
1894 options.hook_stdio,
1895 &options.diagnostics,
1896 );
1897 caches.configure_filter_subprocess(&environment, &git_dir, work_tree.as_deref());
1898
1899 let repo = Self {
1900 git_dir,
1901 common_git_dir,
1902 work_tree,
1903 odb,
1904 explicit_git_dir: false,
1905 discovery_root: None,
1906 work_tree_from_env: false,
1907 discovery_via_gitfile: false,
1908 config_session: Mutex::new(None),
1909 caches,
1910 ref_store,
1911 environment,
1912 git_prefix,
1913 command_runner,
1914 change_id_generator,
1915 diagnostics: Arc::clone(&options.diagnostics),
1916 network_trace: options.network_trace,
1917 reference_unix_time: options.reference_unix_time,
1918 test_assume_different_owner: options.test_assume_different_owner,
1919 thread_policy: options.thread_policy,
1920 hook_stdio: options.hook_stdio,
1921 };
1922 repo.refresh_config_session()?;
1923 if options.validate_repository_format {
1924 repo.validate_repository_format_cached()?;
1925 }
1926 repo.check_injected_object_format(&injected_algos)?;
1927 warn_core_bare_worktree_conflict(options, &repo);
1928 Ok(repo)
1929 }
1930
1931 fn check_injected_object_format(&self, injected: &[HashAlgo]) -> Result<()> {
1938 if injected.is_empty() {
1939 return Ok(());
1940 }
1941 let repository = self
1942 .ensure_config_arc_fast()?
1943 .get("extensions.objectformat")
1944 .and_then(|name| HashAlgo::from_name(&name))
1945 .unwrap_or(HashAlgo::Sha1);
1946 match injected.iter().find(|algo| **algo != repository) {
1947 Some(&store) => Err(Error::ObjectFormatMismatch { repository, store }),
1948 None => Ok(()),
1949 }
1950 }
1951
1952 pub fn discover(start: Option<&Path>, options: &RepositoryOptions) -> Result<Self> {
1954 let env = &options.environment;
1955 let cwd = env.discovery_cwd();
1956
1957 if let Some(dir) = env.git_dir.as_deref() {
1959 let mut git_dir = PathBuf::from(dir);
1960 if git_dir.is_relative() {
1961 git_dir = cwd.join(git_dir);
1962 }
1963 git_dir = resolve_git_dir_env_path(&git_dir)?;
1965 let work_tree = env.git_work_tree.as_deref().map(|wt| {
1966 let p = PathBuf::from(wt);
1967 if p.is_absolute() {
1968 p
1969 } else {
1970 cwd.join(p)
1971 }
1972 });
1973 if let Some(ref wt_path) = work_tree {
1974 if env
1975 .git_work_tree
1976 .as_deref()
1977 .is_some_and(|raw| Path::new(raw).is_absolute())
1978 {
1979 validate_git_work_tree_path(wt_path)?;
1980 }
1981 }
1982 if work_tree.is_some() {
1983 let mut repo = Self::open_with_work_tree(options, &git_dir, work_tree.as_deref())?;
1984 repo.explicit_git_dir = true;
1985 repo.discovery_root = None;
1986 repo.work_tree_from_env = false;
1987 repo.discovery_via_gitfile = false;
1988 return Ok(repo);
1989 }
1990 let (is_bare, core_wt) = read_core_bare_and_worktree(&git_dir)?;
1992 let bare_worktree_conflict = is_bare && core_wt.is_some();
1993 let resolved_wt = if is_bare {
1994 None
1995 } else if let Some(raw) = core_wt {
1996 Some(resolve_core_worktree_path(&git_dir, &raw)?)
1997 } else {
1998 Some(
2004 cwd.canonicalize_simplified()
2005 .unwrap_or_else(|_| cwd.clone()),
2006 )
2007 };
2008 let mut repo = Self::open_with_work_tree(options, &git_dir, resolved_wt.as_deref())?;
2009 repo.explicit_git_dir = true;
2010 repo.discovery_root = None;
2011 repo.work_tree_from_env = false;
2012 repo.discovery_via_gitfile = false;
2013 let _ = bare_worktree_conflict;
2014 return Ok(repo);
2015 }
2016
2017 let env_work_tree = env.git_work_tree.as_deref().map(|wt| {
2020 let p = PathBuf::from(wt);
2021 if p.is_absolute() {
2022 p
2023 } else {
2024 cwd.join(p)
2025 }
2026 });
2027 if let Some(ref p) = env_work_tree {
2028 if env
2029 .git_work_tree
2030 .as_deref()
2031 .is_some_and(|raw| Path::new(raw).is_absolute())
2032 {
2033 validate_git_work_tree_path(p)?;
2034 }
2035 }
2036 let start = start.unwrap_or(&cwd);
2037 let start = if start.is_absolute() {
2038 start.to_path_buf()
2039 } else {
2040 cwd.join(start)
2041 };
2042
2043 let (ceiling_paths, no_resolve_ceilings) = parse_ceiling_directories(env);
2047 let ceiling_dirs: Vec<String> = ceiling_paths
2048 .into_iter()
2049 .map(|p| path_for_ceiling_compare(&p))
2050 .collect();
2051
2052 let start_canon = start
2053 .canonicalize_simplified()
2054 .unwrap_or_else(|_| start.clone());
2055 let ceil_cmp_buf = if no_resolve_ceilings {
2057 path_for_ceiling_compare(&start)
2058 } else {
2059 path_for_ceiling_compare(&start_canon)
2060 };
2061 let mut dir_buf = path_for_ceiling_compare(&start_canon);
2062 let min_offset = offset_1st_component(&dir_buf);
2063 let mut ceil_offset: isize = longest_ancestor_length(&ceil_cmp_buf, &ceiling_dirs)
2064 .map(|n| n as isize)
2065 .unwrap_or(-1);
2066 if ceiling_dirs.contains(&dir_buf) {
2067 ceil_offset = dir_buf.len() as isize;
2068 } else if ceil_offset < 0 {
2069 ceil_offset = min_offset as isize - 2;
2070 }
2071
2072 loop {
2073 let current = Path::new(&dir_buf);
2074 if let Some(DiscoveredAt { mut repo, gitfile }) = try_open_at(options, current)? {
2075 repo.environment = Arc::new(env.clone());
2081 repo.refresh_config_session()?;
2082 repo.validate_repository_format_cached()?;
2083 repo.install_config_snapshot();
2084 let cfg = repo.config()?;
2085 repo.set_diagnostics(Arc::clone(&options.diagnostics));
2086 if let Some(ref wt) = env_work_tree {
2087 repo.work_tree =
2088 Some(wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone()));
2089 repo.work_tree_from_env = true;
2090 } else {
2091 repo.work_tree_from_env = false;
2092 let linked_gitfile =
2097 repo.discovery_via_gitfile && resolve_common_dir(&repo.git_dir).is_some();
2098 if !linked_gitfile {
2099 let (is_bare, core_wt) =
2100 read_core_bare_and_worktree_from_config(cfg.as_ref());
2101 if is_bare {
2102 repo.work_tree = None;
2103 } else if let Some(raw) = core_wt {
2104 repo.work_tree = Some(resolve_core_worktree_path(&repo.git_dir, &raw)?);
2105 }
2106 }
2107 }
2108 let assume_different =
2109 options.test_assume_different_owner || env.test_assume_different_owner();
2110 if assume_different {
2111 repo.enforce_safe_directory()?;
2112 } else {
2113 ensure_valid_ownership(
2114 env,
2115 gitfile.as_deref(),
2116 repo.work_tree.as_deref(),
2117 &repo.git_dir,
2118 )?;
2119 }
2120 if let Some(wt) = repo.work_tree.as_ref() {
2121 repo.git_prefix = compute_git_prefix(env, wt);
2122 }
2123 return Ok(repo);
2124 }
2125
2126 let mut offset: isize = dir_buf.len() as isize;
2127 if offset <= min_offset as isize {
2128 break;
2129 }
2130 loop {
2131 offset -= 1;
2132 if offset <= ceil_offset {
2133 break;
2134 }
2135 if dir_buf
2136 .as_bytes()
2137 .get(offset as usize)
2138 .is_some_and(|b| *b == b'/')
2139 {
2140 break;
2141 }
2142 }
2143 if offset <= ceil_offset {
2144 break;
2145 }
2146 let off_u = offset as usize;
2147 let new_len = if off_u > min_offset {
2148 off_u
2149 } else {
2150 min_offset
2151 };
2152 dir_buf.truncate(new_len);
2153 }
2154
2155 Err(not_a_repository_at(&start))
2156 }
2157
2158 #[must_use]
2160 pub(crate) fn index_path(&self) -> PathBuf {
2161 self.git_dir.join("index")
2162 }
2163
2164 pub(crate) fn index_path_for_env(&self) -> Result<PathBuf> {
2168 if let Some(raw) = self.environment.git_index_file.as_deref() {
2169 if !raw.is_empty() {
2170 let p = PathBuf::from(raw);
2171 return Ok(if p.is_absolute() {
2172 p
2173 } else {
2174 self.environment.discovery_cwd().join(p)
2175 });
2176 }
2177 }
2178 Ok(self.index_path())
2179 }
2180
2181 pub(crate) fn load_index(&self) -> Result<Index> {
2185 let path = self.index_path_for_env()?;
2186 self.load_index_at(&path)
2187 }
2188
2189 pub(crate) fn load_index_at(&self, path: &std::path::Path) -> Result<Index> {
2192 self.load_index_at_with_context(
2193 path,
2194 crate::index::IndexParseContext::from_policy(self.thread_policy()),
2195 )
2196 }
2197
2198 pub(crate) fn load_index_at_with_context(
2199 &self,
2200 path: &std::path::Path,
2201 parse_ctx: crate::index::IndexParseContext,
2202 ) -> Result<Index> {
2203 let mut idx = self.load_index_at_preserving_sparse(path, parse_ctx)?;
2204 let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
2205 idx.expand_sparse_directory_placeholders(
2206 &self.odb,
2207 crate::path_validate::ProtectOptions::from_config(cfg.as_ref()),
2208 )?;
2209 self.clear_present_skip_worktree(&mut idx, cfg.as_ref());
2210 Ok(idx)
2211 }
2212
2213 pub(crate) fn load_index_preserving_sparse(&self) -> Result<Index> {
2215 self.load_index_at_preserving_sparse(
2216 &self.index_path_for_env()?,
2217 crate::index::IndexParseContext::from_policy(self.thread_policy()),
2218 )
2219 }
2220
2221 pub(crate) fn load_index_at_preserving_sparse(
2224 &self,
2225 path: &std::path::Path,
2226 parse_ctx: crate::index::IndexParseContext,
2227 ) -> Result<Index> {
2228 crate::trace::trace_span!("grit.index.read");
2229 let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
2230 if let Some(res) = cfg.as_ref().get_bool("index.sparse") {
2231 res?;
2232 }
2233 let mut idx = Index::load_preserving_sparse_optional_with_context(
2234 path,
2235 &self.odb,
2236 Some(cfg.as_ref()),
2237 parse_ctx,
2238 )?;
2239 crate::split_index::resolve_split_index_if_needed(&mut idx, &self.git_dir, path)?;
2240 if idx.source_mtime.is_none() {
2241 idx.source_mtime = crate::index::index_file_mtime(path);
2242 }
2243 Ok(idx)
2244 }
2245
2246 pub(crate) fn clear_present_skip_worktree(&self, index: &mut Index, cfg: &ConfigSet) {
2247 if let Some(ref wt) = self.work_tree {
2248 crate::sparse_checkout::clear_skip_worktree_from_present_files(
2249 &self.git_dir,
2250 wt,
2251 index,
2252 Some(cfg),
2253 );
2254 }
2255 }
2256
2257 pub(crate) fn write_index(&self, index: &mut Index) -> Result<()> {
2260 self.write_index_with_post_index_change(index, false, false)
2261 }
2262
2263 pub(crate) fn begin_index_update(&self) -> Result<crate::index::IndexLock> {
2272 test_inject_index_write_fail()?;
2273 let _ = crate::porcelain::commit::reap_stale_next_index_files(self, false)?;
2274 let path = self.index_path_for_env()?;
2275 let cfg = self.config()?;
2276 crate::index::IndexLock::acquire(&path, Some(cfg.as_ref()))
2277 }
2278
2279 pub(crate) fn commit_index_update(
2285 &self,
2286 lock: &mut crate::index::IndexLock,
2287 index: &mut Index,
2288 updated_workdir: bool,
2289 ) -> Result<()> {
2290 self.flush_index_update(lock, index)?;
2291 self.finish_index_update(lock, updated_workdir)
2292 }
2293
2294 pub(crate) fn flush_index_update(
2296 &self,
2297 lock: &mut crate::index::IndexLock,
2298 index: &mut Index,
2299 ) -> Result<()> {
2300 let path = lock.index_path.clone();
2301 let (skip_hash, cfg) = self.prepare_index_for_disk_write(&path, index)?;
2302 self.odb.flush_for_publish()?;
2303 write_index_file_split(
2304 &path,
2305 &self.git_dir,
2306 index,
2307 cfg.as_ref(),
2308 WriteSplitIndexRequest::default(),
2309 skip_hash,
2310 self.thread_policy(),
2311 Some(lock),
2312 false,
2313 )
2314 }
2315
2316 pub(crate) fn finish_index_update(
2318 &self,
2319 lock: &mut crate::index::IndexLock,
2320 updated_workdir: bool,
2321 ) -> Result<()> {
2322 lock.commit_replace_index()?;
2323 let updated_workdir_arg = if updated_workdir { "1" } else { "0" };
2324 let _ = run_hook(self, "post-index-change", &[updated_workdir_arg, "0"], None);
2325 Ok(())
2326 }
2327
2328 pub fn write_largest_pack_bitmap(
2335 &self,
2336 options: &crate::pack_bitmap::PackBitmapWriteOptions,
2337 now: std::time::SystemTime,
2338 ) -> std::result::Result<std::path::PathBuf, crate::pack_bitmap::PackBitmapWriteError> {
2339 let path = self
2340 .odb
2341 .largest_pack_index_path()
2342 .map_err(|e| crate::pack_bitmap::PackBitmapWriteError::Object(e.to_string()))?
2343 .ok_or(crate::pack_bitmap::PackBitmapWriteError::InvalidPackPath(
2344 "no pack index in object store".to_owned(),
2345 ))?;
2346 crate::pack_bitmap::PackBitmapWriter::write(self, &path, options, now)
2347 }
2348
2349 pub(crate) fn try_write_index(&self, index: &mut Index) -> Result<bool> {
2362 self.try_write_index_at(&self.index_path_for_env()?, index)
2363 }
2364
2365 pub(crate) fn try_write_index_at(&self, path: &Path, index: &mut Index) -> Result<bool> {
2368 if !self.environment().git_optional_locks {
2369 return Ok(false);
2370 }
2371 let cfg = self.config().ok();
2372 let mut lock = match crate::index::IndexLock::acquire(path, cfg.as_deref()) {
2373 Ok(lock) => lock,
2374 Err(e) if Self::opportunistic_index_write_skippable(&e) => return Ok(false),
2375 Err(e) => return Err(e),
2376 };
2377 let on_disk = crate::index::read_index_on_disk_identity(path, index.hash_algo)?;
2378 if !crate::index::opportunistic_index_write_still_current(index, &on_disk) {
2379 return Ok(false);
2380 }
2381 match self.write_index_with_lock(&mut lock, index, false) {
2382 Ok(()) => Ok(true),
2383 Err(e) if Self::opportunistic_index_write_skippable(&e) => Ok(false),
2384 Err(e) => Err(e),
2385 }
2386 }
2387
2388 fn opportunistic_index_write_skippable(err: &crate::error::Error) -> bool {
2389 use crate::error::Error;
2390 match err {
2391 Error::LockHeld { .. } => true,
2392 Error::Io(e) => matches!(
2393 e.kind(),
2394 std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::ReadOnlyFilesystem
2395 ),
2396 _ => false,
2397 }
2398 }
2399
2400 pub(crate) fn write_index_with_post_index_change(
2411 &self,
2412 index: &mut Index,
2413 updated_workdir: bool,
2414 updated_skipworktree: bool,
2415 ) -> Result<()> {
2416 let path = self.index_path_for_env()?;
2417 self.write_index_at_with_post_index_change(
2418 &path,
2419 index,
2420 updated_workdir,
2421 updated_skipworktree,
2422 )
2423 }
2424
2425 pub(crate) fn write_index_at(&self, path: &std::path::Path, index: &mut Index) -> Result<()> {
2427 self.write_index_at_with_post_index_change(path, index, false, false)
2428 }
2429
2430 pub(crate) fn write_index_at_with_split(
2432 &self,
2433 path: &std::path::Path,
2434 index: &mut Index,
2435 split: WriteSplitIndexRequest,
2436 ) -> Result<()> {
2437 self.write_index_at_split(path, index, split)
2438 }
2439
2440 pub(crate) fn write_index_at_with_post_index_change(
2455 &self,
2456 path: &std::path::Path,
2457 index: &mut Index,
2458 updated_workdir: bool,
2459 updated_skipworktree: bool,
2460 ) -> Result<()> {
2461 self.write_index_at_split_with_post_index_change(
2462 path,
2463 index,
2464 WriteSplitIndexRequest::default(),
2465 updated_workdir,
2466 updated_skipworktree,
2467 )
2468 }
2469
2470 pub(crate) fn write_index_at_split(
2472 &self,
2473 path: &std::path::Path,
2474 index: &mut Index,
2475 split: WriteSplitIndexRequest,
2476 ) -> Result<()> {
2477 self.write_index_at_split_with_post_index_change(path, index, split, false, false)
2478 }
2479
2480 pub(crate) fn write_index_at_split_with_post_index_change(
2493 &self,
2494 path: &std::path::Path,
2495 index: &mut Index,
2496 split: WriteSplitIndexRequest,
2497 updated_workdir: bool,
2498 updated_skipworktree: bool,
2499 ) -> Result<()> {
2500 self.write_index_file(
2501 path,
2502 index,
2503 split,
2504 updated_workdir,
2505 updated_skipworktree,
2506 None,
2507 )
2508 }
2509
2510 pub(crate) fn write_index_with_lock(
2522 &self,
2523 lock: &mut crate::index::IndexLock,
2524 index: &mut Index,
2525 updated_workdir: bool,
2526 ) -> Result<()> {
2527 let path = lock.index_path.clone();
2528 self.write_index_file(
2529 &path,
2530 index,
2531 WriteSplitIndexRequest::default(),
2532 updated_workdir,
2533 false,
2534 Some(lock),
2535 )
2536 }
2537
2538 pub(crate) fn lock_index(&self) -> Result<crate::index::IndexLock> {
2550 let path = self.index_path_for_env()?;
2551 let cfg = self.config()?;
2552 crate::index::IndexLock::acquire(&path, Some(cfg.as_ref()))
2553 }
2554
2555 pub(crate) fn lock_and_load_index(&self) -> Result<(crate::index::IndexLock, Index)> {
2570 let lock = self.lock_index()?;
2571 let index = self.load_index_at(&lock.index_path)?;
2572 Ok((lock, index))
2573 }
2574
2575 pub(crate) fn lock_and_load_index_preserving_sparse(
2577 &self,
2578 ) -> Result<(crate::index::IndexLock, Index)> {
2579 let lock = self.lock_index()?;
2580 let index = self.load_index_at_preserving_sparse(
2581 &lock.index_path,
2582 crate::index::IndexParseContext::from_policy(self.thread_policy()),
2583 )?;
2584 Ok((lock, index))
2585 }
2586
2587 fn prepare_index_for_disk_write(
2589 &self,
2590 path: &std::path::Path,
2591 index: &mut Index,
2592 ) -> Result<(bool, Arc<ConfigSet>)> {
2593 test_inject_index_write_fail()?;
2594 index.hash_algo = self.odb.hash_algo();
2599 self.finalize_sparse_index_if_needed(index)?;
2600 let prev_index_mtime = crate::index::index_file_mtime(path);
2601 let cfg = self.config()?;
2602 if let Some(work_tree) = self.work_tree.as_deref() {
2603 if !index.suppress_pre_write_index_housekeeping {
2604 crate::untracked_cache::ensure_untracked_cache_shell(
2605 index,
2606 work_tree,
2607 cfg.as_ref(),
2608 );
2609 if !index.skip_racy_smudge_on_write {
2610 crate::diff::smudge_racily_clean_entries(
2611 &self.odb,
2612 &self.git_dir,
2613 index,
2614 work_tree,
2615 prev_index_mtime,
2616 Some(cfg.as_ref()),
2617 Some(self.caches().filters()),
2618 );
2619 }
2620 }
2621 }
2622 index.clear_content_verified();
2623 let skip_hash = crate::index::index_skip_hash_for_write(Some(cfg.as_ref()));
2624 Ok((skip_hash, cfg))
2625 }
2626
2627 fn write_index_file(
2628 &self,
2629 path: &std::path::Path,
2630 index: &mut Index,
2631 split: WriteSplitIndexRequest,
2632 updated_workdir: bool,
2633 updated_skipworktree: bool,
2634 existing_lock: Option<&mut crate::index::IndexLock>,
2635 ) -> Result<()> {
2636 crate::trace::trace_span!("grit.index.write");
2637 let (skip_hash, cfg) = self.prepare_index_for_disk_write(path, index)?;
2638 self.odb.flush_for_publish()?;
2639 write_index_file_split(
2640 path,
2641 &self.git_dir,
2642 index,
2643 cfg.as_ref(),
2644 split,
2645 skip_hash,
2646 self.thread_policy(),
2647 existing_lock,
2648 true,
2649 )
2650 .map_err(|err| map_index_write_error(&self.git_dir, path, err))?;
2651 let updated_workdir_arg = if updated_workdir { "1" } else { "0" };
2653 let updated_skipworktree_arg = if updated_skipworktree { "1" } else { "0" };
2654 let _ = run_hook(
2655 self,
2656 "post-index-change",
2657 &[updated_workdir_arg, updated_skipworktree_arg],
2658 None,
2659 );
2660 Ok(())
2661 }
2662
2663 fn finalize_sparse_index_if_needed(&self, index: &mut Index) -> Result<()> {
2664 let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
2665 let sparse_enabled = cfg
2666 .get("core.sparseCheckout")
2667 .map(|v| v == "true")
2668 .unwrap_or(false);
2669 if !sparse_enabled {
2670 index.sparse_directories = false;
2671 return Ok(());
2672 }
2673 let cone_cfg = cfg
2674 .get("core.sparseCheckoutCone")
2675 .and_then(|v| v.parse::<bool>().ok())
2676 .unwrap_or(true);
2677 let sparse_ix = cfg
2678 .get("index.sparse")
2679 .map(|v| v == "true")
2680 .unwrap_or(false);
2681 let patterns = read_sparse_checkout_patterns(&self.git_dir);
2682 let cone = effective_cone_mode_for_sparse_file(cone_cfg, &patterns);
2683 let tree_oid = match index.cache_tree_root {
2686 Some(tree) => Some(tree),
2687 None => crate::state::resolve_head_for_repository(self)?.tree(&self.odb)?,
2688 };
2689 if let Some(t) = tree_oid {
2690 let protect = crate::path_validate::ProtectOptions::from_config(cfg.as_ref());
2691 index.try_collapse_sparse_directories(
2692 &self.odb, &t, &patterns, cone, sparse_ix, protect,
2693 )?;
2694 } else {
2695 index.sparse_directories = false;
2696 }
2697 Ok(())
2698 }
2699
2700 #[must_use]
2705 pub(crate) fn bloom_pathspec_cwd(&self) -> Option<String> {
2706 let wt = self.work_tree.as_ref()?;
2707 let cwd = self.environment.discovery_cwd();
2708 let wt = wt.canonicalize_simplified().ok()?;
2709 let cwd = cwd.canonicalize_simplified().ok()?;
2710 let rel = cwd.strip_prefix(&wt).ok()?;
2711 let s = rel.to_string_lossy().replace('\\', "/");
2712 let s = s.trim_start_matches('/').to_string();
2713 Some(s)
2714 }
2715
2716 #[must_use]
2723 pub fn is_bare(&self) -> bool {
2724 if let Ok(cfg) = self.config() {
2725 if matches!(cfg.get_bool("core.bare"), Some(Ok(false))) {
2726 return false;
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
2809fn replace_dir_is_non_empty(replace_dir: &Path) -> bool {
2811 let Ok(entries) = fs::read_dir(replace_dir) else {
2812 return false;
2813 };
2814 entries.filter_map(|e| e.ok()).any(|e| {
2815 let Ok(ft) = e.file_type() else {
2816 return false;
2817 };
2818 ft.is_file()
2819 })
2820}
2821
2822fn resolve_common_dir(git_dir: &Path) -> Option<PathBuf> {
2824 let commondir_file = git_dir.join("commondir");
2825 if !commondir_file.is_file() {
2826 return None;
2827 }
2828 let common_raw = fs::read_to_string(commondir_file).ok()?;
2829 let common_rel = common_raw.trim();
2830 if common_rel.is_empty() {
2831 return None;
2832 }
2833 let common_dir = if Path::new(common_rel).is_absolute() {
2834 PathBuf::from(common_rel)
2835 } else {
2836 git_dir.join(common_rel)
2837 };
2838 Some(common_dir.canonicalize_simplified().unwrap_or(common_dir))
2839}
2840
2841#[must_use]
2843pub fn common_git_dir_for_config(git_dir: &Path) -> PathBuf {
2844 resolve_common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf())
2845}
2846
2847pub fn worktree_config_enabled(common_dir: &Path) -> bool {
2849 let path = common_dir.join("config");
2850 let Ok(content) = crate::repo_caches::read_local_config_file(&path) else {
2851 return false;
2852 };
2853 worktree_config_enabled_for_text(Some(content.as_str()))
2854}
2855
2856pub fn worktree_config_enabled_for_text(content: Option<&str>) -> bool {
2858 let Some(content) = content else {
2859 return false;
2860 };
2861 let mut in_extensions = false;
2862 for raw_line in content.lines() {
2863 let mut line = raw_line.trim();
2864 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
2865 continue;
2866 }
2867 if line.starts_with('[') {
2868 let Some(end_idx) = line.find(']') else {
2869 continue;
2870 };
2871 let section = line[1..end_idx].trim();
2872 let section_name = section
2873 .split_whitespace()
2874 .next()
2875 .unwrap_or_default()
2876 .to_ascii_lowercase();
2877 in_extensions = section_name == "extensions";
2878 let remainder = line[end_idx + 1..].trim();
2879 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
2880 continue;
2881 }
2882 line = remainder;
2883 }
2884 if in_extensions {
2885 let Some((key, value)) = line.split_once('=') else {
2886 continue;
2887 };
2888 if key.trim().eq_ignore_ascii_case("worktreeconfig") {
2889 let v = value.trim();
2890 return v.eq_ignore_ascii_case("true")
2891 || v.eq_ignore_ascii_case("yes")
2892 || v.eq_ignore_ascii_case("on")
2893 || v == "1";
2894 }
2895 }
2896 }
2897 false
2898}
2899
2900fn open_or_create_config_file(path: &Path, scope: ConfigScope) -> Result<ConfigFile> {
2901 match ConfigFile::from_path(path, scope)? {
2902 Some(f) => Ok(f),
2903 None => {
2904 if let Some(parent) = path.parent() {
2905 fs::create_dir_all(parent).map_err(Error::Io)?;
2906 }
2907 ConfigFile::parse(path, "", scope)
2908 }
2909 }
2910}
2911
2912fn config_file_bool_true(cfg: &ConfigFile, key: &str) -> bool {
2913 cfg.get(key).is_some_and(|v| {
2914 matches!(
2915 v.trim().to_ascii_lowercase().as_str(),
2916 "true" | "yes" | "on" | "1"
2917 )
2918 })
2919}
2920
2921pub fn init_worktree_config(git_dir: &Path) -> Result<()> {
2931 let common_dir = common_git_dir_for_config(git_dir);
2932 let common_config_path = common_dir.join("config");
2933 let worktree_config_path = git_dir.join("config.worktree");
2934
2935 if worktree_config_enabled(&common_dir) {
2936 if !worktree_config_path.exists() {
2937 if let Some(parent) = worktree_config_path.parent() {
2938 fs::create_dir_all(parent).map_err(Error::Io)?;
2939 }
2940 fs::write(&worktree_config_path, "").map_err(Error::Io)?;
2941 }
2942 return Ok(());
2943 }
2944
2945 let mut common_cfg = open_or_create_config_file(&common_config_path, ConfigScope::Local)?;
2946 common_cfg.set("extensions.worktreeConfig", "true")?;
2947
2948 let mut wt_cfg = open_or_create_config_file(&worktree_config_path, ConfigScope::Worktree)?;
2949
2950 if config_file_bool_true(&common_cfg, "core.bare") {
2951 wt_cfg.set("core.bare", "true")?;
2952 common_cfg.unset("core.bare")?;
2953 }
2954 if let Some(worktree) = common_cfg.get("core.worktree") {
2955 wt_cfg.set("core.worktree", &worktree)?;
2956 common_cfg.unset("core.worktree")?;
2957 }
2958
2959 common_cfg.write()?;
2960 wt_cfg.write()?;
2961 Ok(())
2962}
2963
2964pub fn early_config_ignore_repo_reason(common_dir: &Path) -> Option<String> {
2968 let path = common_dir.join("config");
2969 let content = crate::repo_caches::read_local_config_file(&path).ok()?;
2970 early_config_ignore_repo_reason_for_text(Some(content.as_str()))
2971}
2972
2973pub fn early_config_ignore_repo_reason_for_text(content: Option<&str>) -> Option<String> {
2975 const GIT_REPO_VERSION_READ: u32 = 1;
2976 let content = content?;
2977 let mut version = 0u32;
2978 let mut in_core = false;
2979 for raw_line in content.lines() {
2980 let mut line = raw_line.trim();
2981 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
2982 continue;
2983 }
2984 if line.starts_with('[') {
2985 let Some(end_idx) = line.find(']') else {
2986 continue;
2987 };
2988 let section = line[1..end_idx].trim();
2989 let section_name = section
2990 .split_whitespace()
2991 .next()
2992 .unwrap_or_default()
2993 .to_ascii_lowercase();
2994 in_core = section_name == "core";
2995 let remainder = line[end_idx + 1..].trim();
2996 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
2997 continue;
2998 }
2999 line = remainder;
3000 }
3001 if in_core {
3002 if let Some((key, value)) = line.split_once('=') {
3003 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
3004 if let Ok(v) = value.trim().parse::<u32>() {
3005 version = v;
3006 }
3007 }
3008 }
3009 }
3010 }
3011 if version > GIT_REPO_VERSION_READ {
3012 Some(format!(
3013 "Expected git repo version <= {GIT_REPO_VERSION_READ}, found {version}"
3014 ))
3015 } else {
3016 None
3017 }
3018}
3019
3020fn path_for_ceiling_compare(path: &Path) -> String {
3021 let path = path.to_string_lossy();
3022 #[cfg(windows)]
3023 {
3024 path.replace('\\', "/")
3025 }
3026 #[cfg(not(windows))]
3027 {
3028 path.into_owned()
3029 }
3030}
3031
3032fn offset_1st_component(path: &str) -> usize {
3033 if path.starts_with('/') {
3034 1
3035 } else {
3036 0
3037 }
3038}
3039
3040fn longest_ancestor_length(path: &str, ceilings: &[String]) -> Option<usize> {
3042 if path == "/" {
3043 return None;
3044 }
3045 let mut max_len: Option<usize> = None;
3046 for ceil in ceilings {
3047 let mut len = ceil.len();
3048 while len > 0 && ceil.as_bytes().get(len - 1) == Some(&b'/') {
3049 len -= 1;
3050 }
3051 if len == 0 {
3052 continue;
3053 }
3054 if path.len() <= len + 1 {
3055 continue;
3056 }
3057 if !path.starts_with(&ceil[..len]) {
3058 continue;
3059 }
3060 if path.as_bytes().get(len) != Some(&b'/') {
3061 continue;
3062 }
3063 if path.as_bytes().get(len + 1).is_none() {
3064 continue;
3065 }
3066 max_len = Some(max_len.map_or(len, |m| m.max(len)));
3067 }
3068 max_len
3069}
3070
3071pub fn local_config_write_path(git_dir: &Path) -> PathBuf {
3076 if let Some(path) = repository_config_path(git_dir) {
3077 return path;
3078 }
3079 if let Some(common) = resolve_common_dir(git_dir) {
3080 return common.join("config");
3081 }
3082 git_dir.join("config")
3083}
3084
3085fn repository_config_path(git_dir: &Path) -> Option<PathBuf> {
3087 repository_config_path_for_caches(git_dir)
3088}
3089
3090pub(crate) fn repository_config_path_for_caches(git_dir: &Path) -> Option<PathBuf> {
3092 let local = git_dir.join("config");
3093 if local.is_file() {
3094 return Some(local);
3095 }
3096 let commondir = git_dir.join("commondir");
3097 if !commondir.is_file() {
3098 return None;
3099 }
3100 let common = resolve_common_dir(git_dir)?;
3101 let shared = common.join("config");
3102 if shared.exists() {
3103 Some(shared)
3104 } else {
3105 None
3106 }
3107}
3108
3109pub fn validate_repo_format(git_dir: &Path) -> Result<()> {
3115 validate_repository_format(git_dir)
3116}
3117
3118pub(crate) fn validate_repository_format_parsed(parsed: &RepositoryFormat) -> Result<()> {
3120 if parsed.repo_version > 1 {
3121 return Err(Error::UnsupportedRepositoryFormatVersion(
3122 parsed.repo_version,
3123 ));
3124 }
3125
3126 if let Some(raw) = parsed.ref_storage.as_deref() {
3127 crate::ref_storage::RefStorageFormat::validate_config_extension(raw)?;
3128 }
3129
3130 if let Some(msg) = parsed.format_error_message() {
3131 return Err(Error::Message(msg));
3132 }
3133
3134 Ok(())
3135}
3136
3137fn validate_repository_format(git_dir: &Path) -> Result<()> {
3138 let Some(config_path) = repository_config_path(git_dir) else {
3139 return Ok(());
3140 };
3141 let content = crate::repo_caches::read_local_config_file(&config_path)?;
3142 let parsed = parse_repository_format(&content, &config_path)?;
3143 validate_repository_format_parsed(&parsed)
3144}
3145
3146#[derive(Clone)]
3149pub(crate) struct RepositoryFormat {
3150 pub(crate) repo_version: u32,
3152 pub(crate) extensions: BTreeSet<String>,
3154 pub(crate) ref_storage: Option<String>,
3156 pub(crate) object_format: Option<String>,
3158}
3159
3160pub(crate) fn read_repository_format_from_git_dir(git_dir: &Path) -> Result<RepositoryFormat> {
3166 let Some(config_path) = repository_config_path(git_dir) else {
3167 return Ok(RepositoryFormat {
3168 repo_version: 0,
3169 extensions: BTreeSet::new(),
3170 ref_storage: None,
3171 object_format: None,
3172 });
3173 };
3174 let content = crate::repo_caches::read_local_config_file(&config_path)?;
3175 parse_repository_format(&content, &config_path)
3176}
3177
3178impl RepositoryFormat {
3179 pub(crate) fn hash_algo(&self) -> crate::objects::HashAlgo {
3182 self.object_format
3183 .as_deref()
3184 .and_then(crate::objects::HashAlgo::from_name)
3185 .unwrap_or(crate::objects::HashAlgo::Sha1)
3186 }
3187
3188 fn format_error_message(&self) -> Option<String> {
3195 let mut v1_only_found: Vec<&str> = Vec::new();
3202 let mut unknown_found: Vec<&str> = Vec::new();
3203 for extension in &self.extensions {
3204 match extension.as_str() {
3205 "noop" | "preciousobjects" | "partialclone" | "worktreeconfig" => {}
3207 "noop-v1"
3209 | "objectformat"
3210 | "compatobjectformat"
3211 | "refstorage"
3212 | "relativeworktrees"
3213 | "submodulepathconfig" => {
3214 if self.repo_version == 0 {
3215 v1_only_found.push(extension);
3216 }
3217 }
3218 _ => {
3220 if self.repo_version >= 1 {
3221 unknown_found.push(extension);
3222 }
3223 }
3224 }
3225 }
3226
3227 if !unknown_found.is_empty() {
3228 let mut msg = if unknown_found.len() == 1 {
3229 "unknown repository extension found:".to_owned()
3230 } else {
3231 "unknown repository extensions found:".to_owned()
3232 };
3233 for ext in &unknown_found {
3234 msg.push_str(&format!("\n\t{ext}"));
3235 }
3236 return Some(msg);
3237 }
3238
3239 if !v1_only_found.is_empty() {
3240 let mut msg = if v1_only_found.len() == 1 {
3241 "repo version is 0, but v1-only extension found:".to_owned()
3242 } else {
3243 "repo version is 0, but v1-only extensions found:".to_owned()
3244 };
3245 for ext in &v1_only_found {
3246 msg.push_str(&format!("\n\t{ext}"));
3247 }
3248 return Some(msg);
3249 }
3250
3251 None
3252 }
3253}
3254
3255pub(crate) fn parse_repository_format(
3262 content: &str,
3263 config_path: &Path,
3264) -> Result<RepositoryFormat> {
3265 let mut in_core = false;
3266 let mut in_extensions = false;
3267 let mut repo_version = 0u32;
3268 let mut extensions = BTreeSet::new();
3269 let mut ref_storage: Option<String> = None;
3270 let mut object_format: Option<String> = None;
3271
3272 for raw_line in content.lines() {
3273 let mut line = raw_line.trim();
3274 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
3275 continue;
3276 }
3277
3278 if line.starts_with('[') {
3279 let Some(end_idx) = line.find(']') else {
3280 return Err(Error::Config(
3281 format!("invalid config in {}", config_path.display()).into(),
3282 ));
3283 };
3284
3285 let section = line[1..end_idx].trim();
3286 let section_name = section
3287 .split_whitespace()
3288 .next()
3289 .unwrap_or_default()
3290 .to_ascii_lowercase();
3291 in_core = section_name == "core";
3292 in_extensions = section_name == "extensions";
3293
3294 let remainder = line[end_idx + 1..].trim();
3295 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
3296 continue;
3297 }
3298 line = remainder;
3299 }
3300
3301 if in_core {
3302 if let Some((key, value)) = line.split_once('=') {
3303 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
3304 if let Ok(v) = value.trim().parse::<u32>() {
3306 repo_version = v;
3307 }
3308 }
3309 }
3310 }
3311
3312 if in_extensions {
3313 let (key, value) = if let Some((key, value)) = line.split_once('=') {
3314 (key.trim(), Some(value.trim()))
3315 } else {
3316 (line, None)
3317 };
3318 if key.eq_ignore_ascii_case("refstorage") {
3319 ref_storage = value.map(str::to_owned);
3320 }
3321 if key.eq_ignore_ascii_case("objectformat") {
3322 object_format = value.map(str::to_owned);
3323 }
3324 if !key.is_empty() {
3325 extensions.insert(key.to_ascii_lowercase());
3326 }
3327 }
3328 }
3329
3330 Ok(RepositoryFormat {
3331 repo_version,
3332 extensions,
3333 ref_storage,
3334 object_format,
3335 })
3336}
3337
3338pub fn repository_format_warning(git_dir: &Path) -> Result<Option<String>> {
3355 const GIT_REPO_VERSION_READ: u32 = 1;
3356 let Some(config_path) = repository_config_path(git_dir) else {
3357 return Ok(None);
3358 };
3359 let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
3360 let parsed = parse_repository_format(&content, &config_path)?;
3361
3362 if parsed.repo_version > GIT_REPO_VERSION_READ {
3363 return Ok(Some(format!(
3364 "Expected git repo version <= {GIT_REPO_VERSION_READ}, found {}",
3365 parsed.repo_version
3366 )));
3367 }
3368
3369 Ok(parsed.format_error_message())
3370}
3371
3372struct DiscoveredAt {
3378 repo: Repository,
3379 gitfile: Option<PathBuf>,
3381}
3382
3383fn try_open_at(options: &RepositoryOptions, dir: &Path) -> Result<Option<DiscoveredAt>> {
3384 let env = &options.environment;
3385 let dot_git = dir.join(".git");
3386
3387 #[cfg(unix)]
3390 {
3391 use std::os::unix::fs::FileTypeExt;
3392 let is_special = |ft: fs::FileType| {
3393 ft.is_fifo() || ft.is_socket() || ft.is_block_device() || ft.is_char_device()
3394 };
3395 if let Ok(meta) = fs::symlink_metadata(&dot_git) {
3396 let ft = meta.file_type();
3397 let target_special = ft.is_symlink()
3398 && fs::metadata(&dot_git).is_ok_and(|target| is_special(target.file_type()));
3399 if is_special(ft) || target_special {
3400 return Err(Error::NotARepository(
3401 NotARepositoryReason::GitfileNotRegularFile { path: dot_git },
3402 ));
3403 }
3404 }
3405 }
3406
3407 if dot_git.is_file() {
3408 let git_dir = resolve_gitfile(&dot_git)?;
3410 let mut opts = options.clone();
3411 opts.validate_repository_format = false;
3412 opts.work_tree = Some(dir.to_path_buf());
3413 let mut repo = Repository::open_internal(&opts, &git_dir)?;
3414 if resolve_common_dir(&git_dir).is_some() {
3418 let cwd = env.discovery_cwd();
3419 if repo.work_tree.is_some() && !is_inside_work_tree(&repo, &cwd) {
3420 let root = if dir.is_absolute() {
3421 dir.to_path_buf()
3422 } else {
3423 cwd.join(dir)
3424 };
3425 repo.work_tree = Some(root.canonicalize_simplified().unwrap_or(root));
3426 }
3427 }
3428 let root = if dir.is_absolute() {
3429 dir.to_path_buf()
3430 } else {
3431 env.discovery_cwd().join(dir)
3432 };
3433 repo.discovery_root = Some(root.canonicalize_simplified().unwrap_or(root));
3434 repo.discovery_via_gitfile = true;
3435 return Ok(Some(DiscoveredAt {
3436 repo,
3437 gitfile: Some(dot_git.clone()),
3438 }));
3439 }
3440
3441 if dot_git.is_dir() {
3442 let open_path = if dot_git.is_symlink() {
3446 dot_git.read_link().unwrap_or_else(|_| dot_git.clone())
3448 } else {
3449 dot_git.clone()
3450 };
3451 let mut skip_opts = options.clone();
3454 skip_opts.validate_repository_format = false;
3455 skip_opts.work_tree = Some(dir.to_path_buf());
3456 match Repository::open_internal(&skip_opts, &open_path) {
3457 Ok(mut repo) => {
3458 if dot_git.is_symlink() {
3461 let abs_dot_git = if dot_git.is_absolute() {
3462 dot_git
3463 } else {
3464 dir.join(".git")
3465 };
3466 repo.git_dir = abs_dot_git;
3467 }
3468 let root = if dir.is_absolute() {
3469 dir.to_path_buf()
3470 } else {
3471 env.discovery_cwd().join(dir)
3472 };
3473 repo.discovery_root = Some(root.canonicalize_simplified().unwrap_or(root));
3474 repo.discovery_via_gitfile = false;
3475 return Ok(Some(DiscoveredAt {
3476 repo,
3477 gitfile: None,
3478 }));
3479 }
3480 Err(Error::NotARepository(_)) | Err(Error::Config(_)) => return Ok(None),
3481 Err(e) => return Err(e),
3482 }
3483 }
3484
3485 if dir.join("HEAD").is_file() && dir.join("commondir").is_file() {
3488 let repo = Repository::open_with_work_tree(options, dir, None)?;
3489 return Ok(Some(DiscoveredAt {
3490 repo,
3491 gitfile: None,
3492 }));
3493 }
3494
3495 if dir.join("objects").is_dir() && dir.join("HEAD").is_file() {
3497 if !is_inside_dot_git(dir) {
3501 if let Ok(cfg) = crate::config::ConfigSet::load(env, None, true) {
3502 if let Some(val) = cfg.get("safe.bareRepository") {
3503 if val.eq_ignore_ascii_case("explicit") {
3504 return Err(Error::ForbiddenBareRepository(path_for_error_message(dir)));
3505 }
3506 }
3507 }
3508 }
3509 let repo = Repository::open_with_work_tree(options, dir, None)?;
3510 return Ok(Some(DiscoveredAt {
3511 repo,
3512 gitfile: None,
3513 }));
3514 }
3515
3516 Ok(None)
3517}
3518
3519fn is_inside_dot_git(path: &Path) -> bool {
3520 path.components().any(|c| c.as_os_str() == ".git")
3521}
3522
3523fn safe_directory_effective_values(git_dir: &Path, env: &Environment) -> Vec<String> {
3526 let cfg = crate::config::ConfigSet::load(env, Some(git_dir), true)
3527 .unwrap_or_else(|_| crate::config::ConfigSet::new());
3528 let mut values: Vec<String> = Vec::new();
3529 for e in cfg.entries() {
3530 if e.key == "safe.directory"
3531 && e.scope != crate::config::ConfigScope::Local
3532 && e.scope != crate::config::ConfigScope::Worktree
3533 {
3534 values.push(e.value.clone().unwrap_or_else(|| "true".to_owned()));
3535 }
3536 }
3537 let mut effective: Vec<String> = Vec::new();
3538 for v in values {
3539 if v.is_empty() {
3540 effective.clear();
3541 } else {
3542 effective.push(v);
3543 }
3544 }
3545 effective
3546}
3547
3548fn ensure_safe_directory_allows(
3549 git_dir: &Path,
3550 checked: &Path,
3551 environment: &Environment,
3552) -> Result<()> {
3553 let effective = safe_directory_effective_values(git_dir, environment);
3554 let checked_s = checked.to_string_lossy().to_string();
3555 if effective
3556 .iter()
3557 .any(|v| safe_directory_matches(v, &checked_s, &environment.discovery_cwd()))
3558 {
3559 return Ok(());
3560 }
3561 Err(Error::DubiousOwnership(path_for_error_message(checked)))
3562}
3563
3564#[cfg(unix)]
3565fn path_lstat_uid(path: &Path) -> std::io::Result<u32> {
3566 use std::os::unix::fs::MetadataExt;
3567 let meta = fs::symlink_metadata(path)?;
3568 Ok(meta.uid())
3569}
3570
3571#[cfg(unix)]
3574fn ensure_valid_ownership(
3575 environment: &Environment,
3576 gitfile: Option<&Path>,
3577 worktree: Option<&Path>,
3578 gitdir: &Path,
3579) -> Result<()> {
3580 const ROOT_UID: u32 = 0;
3581 let sudo_euid = environment
3582 .sudo_uid
3583 .as_deref()
3584 .and_then(|s| s.parse::<u32>().ok());
3585
3586 fn owned_by_effective_user(path: &Path, sudo_euid: Option<u32>) -> std::io::Result<bool> {
3587 let st_uid = path_lstat_uid(path)?;
3588 let mut euid = nix::unistd::geteuid().as_raw();
3589 if euid == ROOT_UID {
3590 if st_uid == ROOT_UID {
3591 return Ok(true);
3592 }
3593 if let Some(sudo_uid) = sudo_euid {
3594 euid = sudo_uid;
3595 }
3596 }
3597 Ok(st_uid == euid)
3598 }
3599
3600 let assume_different = environment.test_assume_different_owner();
3601 if !assume_different {
3602 let gitfile_ok = gitfile
3603 .map(|p| owned_by_effective_user(p, sudo_euid))
3604 .transpose()?
3605 .unwrap_or(true);
3606 let wt_ok = match worktree {
3609 None => true,
3610 Some(wt) => match owned_by_effective_user(wt, sudo_euid) {
3611 Ok(ok) => ok,
3612 Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
3613 Err(e) => return Err(Error::Io(e)),
3614 },
3615 };
3616 let gd_ok = owned_by_effective_user(gitdir, sudo_euid)?;
3617 if gitfile_ok && wt_ok && gd_ok {
3618 return Ok(());
3619 }
3620 }
3621
3622 let data_path = if let Some(wt) = worktree {
3623 wt.canonicalize_simplified()
3624 .unwrap_or_else(|_| wt.to_path_buf())
3625 } else {
3626 gitdir
3627 .canonicalize_simplified()
3628 .unwrap_or_else(|_| gitdir.to_path_buf())
3629 };
3630 ensure_safe_directory_allows(gitdir, &data_path, environment)
3631}
3632
3633#[cfg(not(unix))]
3634fn ensure_valid_ownership(
3635 _environment: &Environment,
3636 _gitfile: Option<&Path>,
3637 _worktree: Option<&Path>,
3638 _gitdir: &Path,
3639) -> Result<()> {
3640 Ok(())
3641}
3642
3643impl Repository {
3644 pub fn enforce_safe_directory(&self) -> Result<()> {
3650 let assume_different =
3651 self.test_assume_different_owner || self.environment.test_assume_different_owner();
3652 if !assume_different {
3653 return Ok(());
3654 }
3655
3656 if self.explicit_git_dir {
3657 return Ok(());
3658 }
3659
3660 let checked = if let Some(wt) = &self.work_tree {
3664 let cwd = Some(self.environment.discovery_cwd());
3665 if let Some(cwd) = cwd {
3666 if cwd
3667 .canonicalize_simplified()
3668 .ok()
3669 .is_some_and(|c| c.starts_with(&self.git_dir))
3670 {
3671 self.git_dir
3672 .canonicalize_simplified()
3673 .unwrap_or_else(|_| self.git_dir.clone())
3674 } else {
3675 wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone())
3676 }
3677 } else {
3678 wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone())
3679 }
3680 } else {
3681 self.git_dir
3682 .canonicalize_simplified()
3683 .unwrap_or_else(|_| self.git_dir.clone())
3684 };
3685
3686 self.enforce_safe_directory_checked(&checked)
3687 }
3688
3689 pub(crate) fn enforce_safe_directory_git_dir(&self) -> Result<()> {
3694 let assume_different =
3695 self.test_assume_different_owner || self.environment.test_assume_different_owner();
3696 if !assume_different {
3697 return Ok(());
3698 }
3699 let checked = self
3700 .git_dir
3701 .canonicalize_simplified()
3702 .unwrap_or_else(|_| self.git_dir.clone());
3703 self.enforce_safe_directory_checked(&checked)
3704 }
3705
3706 fn enforce_safe_directory_checked(&self, checked: &Path) -> Result<()> {
3707 ensure_safe_directory_allows(&self.git_dir, checked, self.environment.as_ref())
3708 }
3709
3710 pub fn verify_safe_for_clone_source(&self) -> Result<()> {
3716 let assume_different =
3717 self.test_assume_different_owner || self.environment.test_assume_different_owner();
3718 if assume_different {
3719 self.enforce_safe_directory_git_dir()
3720 } else {
3721 #[cfg(unix)]
3722 {
3723 ensure_valid_ownership(self.environment.as_ref(), None, None, &self.git_dir)
3724 }
3725 #[cfg(not(unix))]
3726 {
3727 Ok(())
3728 }
3729 }
3730 }
3731}
3732
3733fn normalize_fs_path(raw: &str) -> String {
3734 use std::path::Component;
3735 let p = std::path::Path::new(raw);
3736 let mut parts: Vec<String> = Vec::new();
3737 let mut absolute = false;
3738 for c in p.components() {
3739 match c {
3740 Component::RootDir => {
3741 absolute = true;
3742 parts.clear();
3743 }
3744 Component::CurDir => {}
3745 Component::ParentDir => {
3746 if !parts.is_empty() {
3747 parts.pop();
3748 }
3749 }
3750 Component::Normal(s) => parts.push(s.to_string_lossy().to_string()),
3751 Component::Prefix(_) => {}
3752 }
3753 }
3754 let mut out = if absolute {
3755 String::from("/")
3756 } else {
3757 String::new()
3758 };
3759 out.push_str(&parts.join("/"));
3760 out
3761}
3762
3763fn safe_directory_matches(config_value: &str, checked: &str, cwd: &Path) -> bool {
3764 if config_value == "*" {
3765 return true;
3766 }
3767 if config_value == "." {
3768 let cwd_s = normalize_fs_path(&cwd.to_string_lossy());
3770 let checked_s = normalize_fs_path(checked);
3771 return cwd_s == checked_s;
3772 }
3773
3774 let canonicalize_or_normalize = |raw: &str| -> String {
3775 let p = std::path::Path::new(raw);
3776 if p.exists() {
3777 p.canonicalize_simplified()
3778 .map(|c| c.to_string_lossy().to_string())
3779 .map(|s| normalize_fs_path(&s))
3780 .unwrap_or_else(|_| normalize_fs_path(raw))
3781 } else {
3782 normalize_fs_path(raw)
3783 }
3784 };
3785
3786 let config_norm = canonicalize_or_normalize(config_value);
3787 let checked_norm = normalize_fs_path(checked);
3788
3789 if config_norm.ends_with("/*") {
3790 let prefix_raw = &config_norm[..config_norm.len() - 2];
3791 let prefix_norm = canonicalize_or_normalize(prefix_raw);
3792 let mut prefix = prefix_norm;
3793 if !prefix.ends_with('/') {
3794 prefix.push('/');
3795 }
3796 return checked_norm.starts_with(&prefix);
3797 }
3798
3799 config_norm == checked_norm
3800}
3801
3802fn warn_core_bare_worktree_conflict(options: &RepositoryOptions, repo: &Repository) {
3806 if options
3807 .environment
3808 .git_work_tree
3809 .as_deref()
3810 .is_some_and(|s| !s.trim().is_empty())
3811 {
3812 return;
3813 }
3814 let Ok(cfg) = repo.ensure_config_arc_fast() else {
3815 return;
3816 };
3817 if let (true, Some(_)) = read_core_bare_and_worktree_from_config(&cfg) {
3818 options
3819 .diagnostics
3820 .warn(diagnostics::Warning::CoreBareWithWorktree);
3821 }
3822}
3823
3824fn read_core_bare_and_worktree_from_config(cfg: &ConfigSet) -> (bool, Option<String>) {
3825 let bare = cfg
3826 .get_bool("core.bare")
3827 .and_then(|r| r.ok())
3828 .unwrap_or(false);
3829 let worktree = cfg.get("core.worktree").map(|s| s.to_owned());
3830 (bare, worktree)
3831}
3832
3833fn read_core_bare_and_worktree(git_dir: &Path) -> Result<(bool, Option<String>)> {
3834 let cfg = ConfigSet::load(
3835 &crate::environment::Environment::empty(),
3836 Some(git_dir),
3837 true,
3838 )
3839 .unwrap_or_default();
3840 Ok(read_core_bare_and_worktree_from_config(&cfg))
3841}
3842
3843fn validate_git_work_tree_path(path: &Path) -> Result<()> {
3846 if !path.is_absolute() {
3847 return Ok(());
3848 }
3849 let comps: Vec<Component<'_>> = path.components().collect();
3850 let Some(last_normal_idx) = comps
3851 .iter()
3852 .enumerate()
3853 .rev()
3854 .find_map(|(i, c)| matches!(c, Component::Normal(_)).then_some(i))
3855 else {
3856 return Ok(());
3857 };
3858 let mut cur = PathBuf::new();
3859 for (i, comp) in comps.iter().enumerate() {
3860 match comp {
3861 Component::Prefix(p) => cur.push(p.as_os_str()),
3862 Component::RootDir => cur.push(comp.as_os_str()),
3863 Component::CurDir => {}
3864 Component::ParentDir => {
3865 let _ = cur.pop();
3866 }
3867 Component::Normal(seg) => {
3868 cur.push(seg);
3869 if i != last_normal_idx && !cur.exists() {
3870 return Err(Error::PathError(format!(
3871 "Invalid path '{}': No such file or directory",
3872 cur.display()
3873 )));
3874 }
3875 }
3876 }
3877 }
3878 Ok(())
3879}
3880
3881fn resolve_core_worktree_path(git_dir: &Path, raw: &str) -> Result<PathBuf> {
3882 let p = Path::new(raw);
3883 if p.is_absolute() {
3884 return Ok(p
3885 .canonicalize_simplified()
3886 .unwrap_or_else(|_| p.to_path_buf()));
3887 }
3888 let joined = git_dir.join(p);
3889 Ok(joined.canonicalize_simplified().unwrap_or(joined))
3890}
3891
3892fn resolve_git_dir_env_path(git_dir: &Path) -> Result<PathBuf> {
3894 if git_dir.is_file() {
3895 return resolve_gitfile(git_dir);
3896 }
3897 Ok(git_dir.to_path_buf())
3898}
3899
3900pub fn resolve_git_directory_arg(git_dir: &Path) -> Result<PathBuf> {
3906 resolve_git_dir_env_path(git_dir)
3907}
3908
3909pub fn resolve_dot_git(dot_git: &Path) -> Result<PathBuf> {
3915 if dot_git.is_dir() {
3916 return dot_git
3917 .canonicalize_simplified()
3918 .map_err(|_| not_a_repository_at(dot_git));
3919 }
3920 if dot_git.is_file() {
3921 return resolve_gitfile(dot_git);
3922 }
3923 Err(not_a_repository_at(dot_git))
3924}
3925
3926fn not_a_repository_at(path: &Path) -> Error {
3927 Error::NotARepository(NotARepositoryReason::NoRepositoryAt {
3928 path: path.to_path_buf(),
3929 })
3930}
3931
3932fn canonicalize_existing_prefix(path: &Path) -> Result<PathBuf> {
3936 let absolute = std::path::absolute(path)?;
3937 let mut missing = Vec::new();
3938 let mut existing = absolute.as_path();
3939 loop {
3940 if let Ok(canonical) = existing.canonicalize_simplified() {
3941 return Ok(missing
3942 .iter()
3943 .rev()
3944 .fold(canonical, |acc: PathBuf, part| acc.join(part)));
3945 }
3946 match (existing.parent(), existing.file_name()) {
3947 (Some(parent), Some(name)) => {
3948 missing.push(name.to_owned());
3949 existing = parent;
3950 }
3951 _ => return Ok(absolute),
3952 }
3953 }
3954}
3955
3956fn resolve_objects_dir(
3966 environment: &Environment,
3967 git_dir: &Path,
3968 common_git_dir: &Path,
3969) -> Option<PathBuf> {
3970 if let Some(dir) = environment.git_object_directory.as_deref() {
3971 return (!dir.is_empty()).then(|| environment.discovery_cwd().join(dir));
3972 }
3973 let own = git_dir.join("objects");
3974 Some(if own.exists() {
3975 own
3976 } else {
3977 common_git_dir.join("objects")
3978 })
3979}
3980
3981fn head_file_present(git_dir: &Path) -> bool {
3982 let head_path = git_dir.join("HEAD");
3983 head_path.exists() || head_path.is_symlink()
3984}
3985
3986fn open_or_install_ref_store(
3989 caches: &RepoCaches,
3990 git_dir: &Path,
3991 environment: &Environment,
3992 injected: Option<Arc<dyn crate::refs::store::RefStore>>,
3993) -> Result<Arc<dyn crate::refs::store::RefStore>> {
3994 match injected {
3995 Some(store) => {
3996 let store = linked_worktree_view(git_dir, store);
3997 caches.install_ref_store(git_dir, Arc::clone(&store));
3998 Ok(store)
3999 }
4000 None => caches.open_ref_store(git_dir, environment),
4001 }
4002}
4003
4004fn linked_worktree_view(
4011 git_dir: &Path,
4012 store: Arc<dyn crate::refs::store::RefStore>,
4013) -> Arc<dyn crate::refs::store::RefStore> {
4014 use crate::refs::store::{LinkedWorktreeRefStore, RefStorageFormat};
4015 let on_disk = matches!(
4016 store.format(),
4017 RefStorageFormat::Files | RefStorageFormat::Reftable
4018 );
4019 if on_disk || !git_dir.join("commondir").is_file() {
4020 return store;
4021 }
4022 Arc::new(LinkedWorktreeRefStore::new(git_dir.to_path_buf(), store))
4023}
4024
4025fn resolve_gitfile(gitfile: &Path) -> Result<PathBuf> {
4027 let content = fs::read_to_string(gitfile).map_err(|error| {
4028 Error::NotARepository(NotARepositoryReason::GitfileUnreadable {
4029 path: gitfile.to_path_buf(),
4030 error,
4031 })
4032 })?;
4033 parse_gitfile(&content, gitfile)
4034}
4035
4036fn parse_gitfile(content: &str, gitfile: &Path) -> Result<PathBuf> {
4039 let Some(target) = content
4040 .lines()
4041 .find_map(|line| line.strip_prefix("gitdir:"))
4042 else {
4043 return Err(Error::NotARepository(
4044 NotARepositoryReason::GitfileMissingGitdir {
4045 path: gitfile.to_path_buf(),
4046 },
4047 ));
4048 };
4049 let path = gitfile
4050 .parent()
4051 .unwrap_or_else(|| Path::new(""))
4052 .join(target.trim());
4053 if !path.exists() {
4054 return Err(not_a_repository_at(&path));
4055 }
4056 Ok(path)
4057}
4058
4059pub fn resolve_initial_branch(env: &Environment, explicit: Option<&str>) -> Result<String> {
4069 let name = match explicit {
4070 Some(name) => name.to_owned(),
4071 None => ConfigSet::load(env, None, true)?
4072 .get_last_entry("init.defaultBranch")
4073 .and_then(|e| e.value.clone())
4074 .filter(|s| !s.is_empty())
4075 .unwrap_or_else(|| "main".to_owned()),
4076 };
4077 validate_branch_short_name(&name).map_err(|e| Error::InvalidRef(e.to_string()))?;
4078 Ok(name)
4079}
4080
4081fn resolve_git_dir_for_init(path: &Path, options: &InitOptions) -> Result<PathBuf> {
4082 if options.bare_with_worktree_at_path || options.bare {
4083 return Ok(path.to_path_buf());
4084 }
4085 if let Some(separate) = &options.separate_git_dir {
4086 return Ok(separate.clone());
4087 }
4088 let dot_git = path.join(".git");
4089 if dot_git.is_file() {
4090 let content = fs::read_to_string(&dot_git)?;
4091 return parse_gitfile(&content, &dot_git);
4092 }
4093 Ok(dot_git)
4094}
4095
4096fn head_branch_name_for_outcome(git_dir: &Path) -> Result<String> {
4098 if let Some(target) = crate::refs::read_symbolic_ref(git_dir, "HEAD")? {
4099 if let Some(short) = target.strip_prefix("refs/heads/") {
4100 return Ok(short.to_owned());
4101 }
4102 return Ok(target
4103 .rsplit('/')
4104 .next()
4105 .unwrap_or(target.as_str())
4106 .to_owned());
4107 }
4108 let head_text = fs::read_to_string(git_dir.join("HEAD"))?;
4109 let oid = head_text.trim();
4110 if crate::objects::ObjectId::from_hex(oid).is_ok() {
4111 return Ok(crate::objects::short_hex(oid).to_owned());
4112 }
4113 Ok("HEAD".to_owned())
4114}
4115
4116fn hash_algo_from_git_dir(git_dir: &Path) -> Result<HashAlgo> {
4117 let config_path = git_dir.join("config");
4118 if !config_path.is_file() {
4119 return Ok(HashAlgo::Sha1);
4120 }
4121 let content = fs::read_to_string(&config_path)?;
4122 Ok(parse_repository_format(&content, &config_path)?.hash_algo())
4123}
4124
4125fn ensure_git_directory_layout(
4127 git_dir: &Path,
4128 bare: bool,
4129 ref_storage: crate::ref_storage::RefStorageFormat,
4130 skip_hooks_and_info: bool,
4131) -> Result<()> {
4132 let mut subs = vec!["objects", "objects/info", "objects/pack", "refs"];
4133 if !ref_storage.is_reftable() {
4134 subs.extend(["refs/heads", "refs/tags"]);
4135 }
4136 if !bare && !skip_hooks_and_info {
4137 subs.push("info");
4138 subs.push("hooks");
4139 }
4140 for sub in subs {
4141 fs::create_dir_all(git_dir.join(sub))?;
4142 }
4143
4144 if ref_storage.is_reftable() {
4145 let reftable_dir = git_dir.join("reftable");
4146 fs::create_dir_all(&reftable_dir)?;
4147 let tables_list = reftable_dir.join("tables.list");
4148 if !tables_list.exists() {
4149 fs::write(&tables_list, "")?;
4150 }
4151 }
4152 Ok(())
4153}
4154
4155#[allow(clippy::too_many_arguments)]
4156fn write_fresh_git_directory(
4157 git_dir: &Path,
4158 bare: bool,
4159 initial_branch: &str,
4160 template_dir: Option<&Path>,
4161 ref_storage: crate::ref_storage::RefStorageFormat,
4162 object_format: HashAlgo,
4163 skip_hooks_and_info: bool,
4164 environment: &Environment,
4165) -> Result<()> {
4166 ensure_git_directory_layout(git_dir, bare, ref_storage, skip_hooks_and_info)?;
4167
4168 if let Some(tmpl) = template_dir {
4169 if tmpl.is_dir() {
4170 copy_template(tmpl, git_dir)?;
4171 }
4172 }
4173
4174 let needs_extensions = ref_storage.is_reftable() || object_format != HashAlgo::Sha1;
4175 let repo_version = if needs_extensions { 1 } else { 0 };
4176
4177 let mut config_content = String::from("[core]\n");
4178 config_content.push_str(&format!("\trepositoryformatversion = {repo_version}\n"));
4179 config_content.push_str("\tfilemode = true\n");
4180 if bare {
4181 config_content.push_str("\tbare = true\n");
4182 } else {
4183 config_content.push_str("\tbare = false\n");
4184 config_content.push_str("\tlogallrefupdates = true\n");
4185 }
4186 if needs_extensions {
4187 config_content.push_str("[extensions]\n");
4188 if ref_storage.is_reftable() {
4189 config_content.push_str("\trefStorage = reftable\n");
4190 }
4191 if object_format != HashAlgo::Sha1 {
4192 config_content.push_str(&format!("\tobjectformat = {}\n", object_format.name()));
4193 }
4194 }
4195 fs::write(git_dir.join("config"), config_content)?;
4196
4197 if let Some(tmpl) = template_dir {
4199 if tmpl.is_dir() {
4200 let tmpl_config = tmpl.join("config");
4201 if tmpl_config.is_file() {
4202 let tmpl_text = fs::read_to_string(&tmpl_config)?;
4203 let tmpl_parsed = ConfigFile::parse(&tmpl_config, &tmpl_text, ConfigScope::Local)?;
4204 let dest_path = git_dir.join("config");
4205 let dest_text = fs::read_to_string(&dest_path)?;
4206 let mut dest_parsed =
4207 ConfigFile::parse(&dest_path, &dest_text, ConfigScope::Local)?;
4208 for e in &tmpl_parsed.entries {
4209 if e.key == "core.bare" {
4211 continue;
4212 }
4213 if let Some(v) = &e.value {
4214 let _ = dest_parsed.set(&e.key, v);
4215 } else {
4216 let _ = dest_parsed.set(&e.key, "true");
4217 }
4218 }
4219 dest_parsed.write()?;
4220 }
4221 }
4222 }
4223
4224 crate::refs::store::initialize_head(
4225 git_dir,
4226 crate::refs::store::RawRef::Symbolic(format!("refs/heads/{initial_branch}")),
4227 )?;
4228
4229 apply_init_filesystem_config(git_dir, InitFilesystemConfigOptions::default(), environment)?;
4230
4231 fs::write(
4232 git_dir.join("description"),
4233 "Unnamed repository; edit this file 'description' to name the repository.\n",
4234 )?;
4235 Ok(())
4236}
4237
4238pub(crate) fn init_repository_separate_git_dir(
4247 work_tree: &Path,
4248 git_dir: &Path,
4249 initial_branch: &str,
4250 template_dir: Option<&Path>,
4251 ref_storage: crate::ref_storage::RefStorageFormat,
4252 object_format: HashAlgo,
4253 environment: &Environment,
4254) -> Result<Repository> {
4255 let skip_hooks_info = template_dir.is_some_and(|p| p.as_os_str().is_empty());
4256 fs::create_dir_all(work_tree).map_err(|source| {
4257 map_worktree_io(
4258 path_for_error_message(work_tree),
4259 WorktreePathOperation::Create,
4260 source,
4261 )
4262 })?;
4263 fs::create_dir_all(git_dir).map_err(|source| {
4264 map_git_dir_io(git_dir, git_dir, GitDirPathOperation::CreateDir, source)
4265 })?;
4266 let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
4267 if existing {
4268 ensure_git_directory_layout(git_dir, false, ref_storage, skip_hooks_info)?;
4269 } else {
4270 write_fresh_git_directory(
4271 git_dir,
4272 false,
4273 initial_branch,
4274 template_dir,
4275 ref_storage,
4276 object_format,
4277 skip_hooks_info,
4278 environment,
4279 )?;
4280 }
4281
4282 let gitfile = work_tree.join(".git");
4288 let abs_git_dir = crate::git_path::absolute_path_for_gitfile_stored_path(git_dir);
4289 fs::write(gitfile, format!("gitdir: {abs_git_dir}\n"))?;
4290
4291 Repository::open(
4292 git_dir,
4293 &RepositoryOptions::with_environment(environment.clone()).with_work_tree(work_tree),
4294 )
4295}
4296
4297pub fn ensure_core_bare(git_dir: &Path) -> Result<()> {
4312 let path = git_dir.join("config");
4313 let text = fs::read_to_string(&path).unwrap_or_default();
4314 if text.lines().any(|l| {
4315 let t = l.trim();
4316 t == "bare = true" || t == "bare=true"
4317 }) {
4318 return Ok(());
4319 }
4320 let mut out = text;
4321 if !out.ends_with('\n') && !out.is_empty() {
4322 out.push('\n');
4323 }
4324 if !out.contains("[core]") {
4325 out.push_str("[core]\n");
4326 }
4327 out.push_str("\tbare = true\n");
4328 fs::write(path, out).map_err(Error::Io)
4329}
4330
4331pub(crate) fn init_repository(
4332 path: &Path,
4333 bare: bool,
4334 initial_branch: &str,
4335 template_dir: Option<&Path>,
4336 ref_storage: crate::ref_storage::RefStorageFormat,
4337) -> Result<Repository> {
4338 init_repository_with_object_format(
4339 path,
4340 bare,
4341 initial_branch,
4342 template_dir,
4343 ref_storage,
4344 HashAlgo::Sha1,
4345 &Environment::empty(),
4346 )
4347}
4348
4349pub(crate) fn init_repository_with_object_format(
4350 path: &Path,
4351 bare: bool,
4352 initial_branch: &str,
4353 template_dir: Option<&Path>,
4354 ref_storage: crate::ref_storage::RefStorageFormat,
4355 object_format: HashAlgo,
4356 environment: &Environment,
4357) -> Result<Repository> {
4358 let skip_hooks_info = !bare && template_dir.is_some_and(|p| p.as_os_str().is_empty());
4359 let git_dir = if bare {
4360 path.to_path_buf()
4361 } else {
4362 path.join(".git")
4363 };
4364
4365 if !bare {
4366 fs::create_dir_all(path).map_err(|source| {
4367 map_worktree_io(
4368 path_for_error_message(path),
4369 WorktreePathOperation::Create,
4370 source,
4371 )
4372 })?;
4373 }
4374 fs::create_dir_all(&git_dir).map_err(|source| {
4375 if bare {
4376 map_worktree_io(
4377 path_for_error_message(path),
4378 WorktreePathOperation::Create,
4379 source,
4380 )
4381 } else {
4382 map_git_dir_io(&git_dir, &git_dir, GitDirPathOperation::CreateDir, source)
4383 }
4384 })?;
4385 let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
4386 if existing {
4387 ensure_git_directory_layout(&git_dir, bare, ref_storage, skip_hooks_info)?;
4388 } else {
4389 write_fresh_git_directory(
4390 &git_dir,
4391 bare,
4392 initial_branch,
4393 template_dir,
4394 ref_storage,
4395 object_format,
4396 skip_hooks_info,
4397 environment,
4398 )?;
4399 }
4400
4401 if bare {
4402 Repository::open(
4403 &git_dir,
4404 &RepositoryOptions::with_environment(environment.clone()),
4405 )
4406 } else {
4407 Repository::open(
4408 &git_dir,
4409 &RepositoryOptions::with_environment(environment.clone())
4410 .with_work_tree(path.to_path_buf()),
4411 )
4412 }
4413}
4414
4415pub(crate) fn init_bare_with_env_worktree(
4424 git_dir: &Path,
4425 work_tree: &Path,
4426 initial_branch: &str,
4427 template_dir: Option<&Path>,
4428 ref_storage: crate::ref_storage::RefStorageFormat,
4429 object_format: HashAlgo,
4430 environment: &Environment,
4431) -> Result<Repository> {
4432 fs::create_dir_all(git_dir)?;
4433 fs::create_dir_all(work_tree)?;
4434 let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
4435 if existing {
4436 ensure_git_directory_layout(git_dir, true, ref_storage, false)?;
4437 } else {
4438 write_fresh_git_directory(
4439 git_dir,
4440 true,
4441 initial_branch,
4442 template_dir,
4443 ref_storage,
4444 object_format,
4445 false,
4446 environment,
4447 )?;
4448 }
4449 let work_tree_abs = work_tree
4450 .canonicalize_simplified()
4451 .unwrap_or_else(|_| work_tree.to_path_buf());
4452 let config_path = git_dir.join("config");
4453 let mut config = match ConfigFile::from_path(&config_path, ConfigScope::Local)? {
4454 Some(c) => c,
4455 None => ConfigFile::parse(&config_path, "", ConfigScope::Local)?,
4456 };
4457 config.set("core.worktree", &work_tree_abs.to_string_lossy())?;
4458 config.write()?;
4459 Repository::open(
4460 git_dir,
4461 &RepositoryOptions::with_environment(environment.clone()).with_work_tree(work_tree),
4462 )
4463}
4464
4465fn copy_template(src: &Path, dst: &Path) -> Result<()> {
4467 for entry in fs::read_dir(src)? {
4468 let entry = entry?;
4469 let src_path = entry.path();
4470 let dst_path = dst.join(entry.file_name());
4471 if src_path.is_dir() {
4472 fs::create_dir_all(&dst_path)?;
4473 copy_template(&src_path, &dst_path)?;
4474 } else {
4475 fs::copy(&src_path, &dst_path)?;
4476 }
4477 }
4478 Ok(())
4479}
4480
4481fn parse_ceiling_directories(env: &Environment) -> (Vec<PathBuf>, bool) {
4490 let Some(raw) = env.git_ceiling_directories.as_deref() else {
4491 return (Vec::new(), false);
4492 };
4493 if raw.is_empty() {
4494 return (Vec::new(), false);
4495 }
4496 let (no_resolve, effective) = match raw.strip_prefix(':') {
4498 Some(rest) => (true, rest),
4499 None => (false, raw),
4500 };
4501 let paths = effective
4502 .split(':')
4503 .filter(|s| !s.is_empty())
4504 .filter_map(|s| {
4505 let p = PathBuf::from(s);
4506 if !p.is_absolute() {
4507 return None;
4508 }
4509 if no_resolve {
4510 let s = s.trim_end_matches('/');
4512 Some(PathBuf::from(s))
4513 } else {
4514 Some(p.canonicalize_simplified().unwrap_or_else(|_| {
4517 let s = s.trim_end_matches('/');
4518 PathBuf::from(s)
4519 }))
4520 }
4521 })
4522 .collect();
4523 (paths, no_resolve)
4524}
4525
4526pub fn validate_repo_config(config_text: &str) -> std::result::Result<(), String> {
4529 let mut version: u32 = 0;
4530 let mut in_core = false;
4531 for line in config_text.lines() {
4532 let trimmed = line.trim();
4533 if trimmed.starts_with('[') {
4534 in_core = trimmed.to_lowercase().starts_with("[core");
4535 continue;
4536 }
4537 if in_core {
4538 if let Some(rest) = trimmed.strip_prefix("repositoryformatversion") {
4539 let val = rest.trim_start_matches([' ', '=']).trim();
4540 if let Ok(v) = val.parse::<u32>() {
4541 version = v;
4542 }
4543 }
4544 }
4545 }
4546 if version >= 2 {
4547 return Err(format!("unknown repository format version: {version}"));
4548 }
4549 Ok(())
4550}
4551
4552pub mod caches {
4554 pub use crate::repo_caches::*;
4555}
4556
4557mod index_write_fail_inject {
4558 use std::cell::RefCell;
4559
4560 thread_local! {
4562 pub static INJECT: RefCell<bool> = const { RefCell::new(false) };
4563 pub static INJECT_REMAINING: RefCell<Option<usize>> = const { RefCell::new(None) };
4564 }
4565}
4566
4567#[doc(hidden)]
4569pub fn set_test_inject_index_write_fail(enabled: bool) {
4570 index_write_fail_inject::INJECT.with(|c| *c.borrow_mut() = enabled);
4571 if !enabled {
4572 index_write_fail_inject::INJECT_REMAINING.with(|c| *c.borrow_mut() = None);
4573 }
4574}
4575
4576#[doc(hidden)]
4578pub fn set_test_inject_index_write_fail_remaining(remaining: usize) {
4579 index_write_fail_inject::INJECT.with(|c| *c.borrow_mut() = true);
4580 index_write_fail_inject::INJECT_REMAINING.with(|c| *c.borrow_mut() = Some(remaining));
4581}
4582
4583mod rewrite_apply_inject {
4584 use std::cell::RefCell;
4585
4586 thread_local! {
4588 pub static ABORT_AFTER_REFS: RefCell<bool> = const { RefCell::new(false) };
4589 }
4590}
4591
4592#[doc(hidden)]
4594pub fn set_test_inject_rewrite_apply_abort_after_refs(enabled: bool) {
4595 rewrite_apply_inject::ABORT_AFTER_REFS.with(|c| *c.borrow_mut() = enabled);
4596}
4597
4598pub(crate) fn test_inject_rewrite_apply_abort_after_refs() -> bool {
4599 rewrite_apply_inject::ABORT_AFTER_REFS.with(|c| *c.borrow())
4600}
4601
4602fn test_inject_index_write_fail() -> Result<()> {
4603 index_write_fail_inject::INJECT.with(|enabled| {
4604 if !*enabled.borrow() {
4605 return Ok(());
4606 }
4607 index_write_fail_inject::INJECT_REMAINING.with(|remaining| {
4608 if let Some(left) = remaining.borrow_mut().as_mut() {
4609 if *left == 0 {
4610 return Ok(());
4611 }
4612 *left -= 1;
4613 }
4614 Err(Error::Io(std::io::Error::other(
4615 "injected index write failure",
4616 )))
4617 })
4618 })
4619}
4620
4621#[path = "repo_location.rs"]
4622mod location;
4623pub use location::RepositoryLocation;
4624
4625#[cfg(test)]
4626#[path = "repo_line_coverage_tests.rs"]
4627mod line_coverage_tests;
4628
4629#[cfg(test)]
4630mod config_session_tests {
4631 #![allow(clippy::unwrap_used)]
4632
4633 use super::*;
4634 use std::ffi::OsString;
4635
4636 fn options_with_global(home: &Path, global: &Path) -> RepositoryOptions {
4637 RepositoryOptions {
4638 environment: Environment::from_vars(
4639 [
4640 ("HOME", home.as_os_str()),
4641 ("GIT_CONFIG_NOSYSTEM", "1".as_ref()),
4642 ("GIT_CONFIG_GLOBAL", global.as_os_str()),
4643 ]
4644 .map(|(k, v)| (OsString::from(k), v.to_owned())),
4645 home.to_path_buf(),
4646 ),
4647 ..RepositoryOptions::default()
4648 }
4649 }
4650
4651 #[test]
4652 fn config_is_reused_until_a_cascade_file_changes() {
4653 let dir = tempfile::tempdir().unwrap();
4654 let global = dir.path().join("global.cfg");
4655 fs::write(&global, "[user]\n\tname = First\n").unwrap();
4656 let work = dir.path().join("work");
4657 let options = options_with_global(dir.path(), &global);
4658 Repository::init(
4659 &work,
4660 &InitOptions {
4661 environment: Some(options.environment.clone()),
4662 ..InitOptions::default()
4663 },
4664 )
4665 .unwrap();
4666 let repo = Repository::discover(Some(&work), &options).unwrap();
4667
4668 let first = repo.config().unwrap();
4669 assert_eq!(first.get("user.name").as_deref(), Some("First"));
4670 assert!(Arc::ptr_eq(&first, &repo.config().unwrap()));
4671
4672 fs::write(&global, "[user]\n\tname = Second one\n").unwrap();
4673 assert_eq!(
4674 repo.config().unwrap().get("user.name").as_deref(),
4675 Some("Second one")
4676 );
4677
4678 let local = repo.git_dir().join("config");
4679 let mut text = fs::read_to_string(&local).unwrap();
4680 text.push_str("[user]\n\tname = Local\n");
4681 fs::write(&local, text).unwrap();
4682 let reloaded = repo.config().unwrap();
4683 assert_eq!(reloaded.get("user.name").as_deref(), Some("Local"));
4684 assert!(Arc::ptr_eq(&reloaded, &repo.config().unwrap()));
4685
4686 fs::remove_file(&global).unwrap();
4687 fs::write(&local, "[core]\n\trepositoryformatversion = 0\n").unwrap();
4688 assert_eq!(repo.config().unwrap().get("user.name"), None);
4689 }
4690
4691 #[test]
4692 fn a_replaced_environment_is_checked_in_full_once() {
4693 let dir = tempfile::tempdir().unwrap();
4694 let global = dir.path().join("global.cfg");
4695 fs::write(&global, "[user]\n\tname = Env\n").unwrap();
4696 let work = dir.path().join("work");
4697 let options = options_with_global(dir.path(), &global);
4698 Repository::init(
4699 &work,
4700 &InitOptions {
4701 environment: Some(options.environment.clone()),
4702 ..InitOptions::default()
4703 },
4704 )
4705 .unwrap();
4706 let mut repo = Repository::discover(Some(&work), &options).unwrap();
4707 let before = repo.config().unwrap();
4708
4709 repo.environment = Arc::new(options.environment.clone());
4711 assert!(Arc::ptr_eq(&before, &repo.config().unwrap()));
4712
4713 let other = dir.path().join("other.cfg");
4715 fs::write(&other, "[user]\n\tname = Other\n").unwrap();
4716 repo.environment = Arc::new(options_with_global(dir.path(), &other).environment);
4717 assert_eq!(
4718 repo.config().unwrap().get("user.name").as_deref(),
4719 Some("Other")
4720 );
4721 }
4722}