1use crate::git_path::CanonicalizeExt;
30use std::collections::BTreeSet;
31use std::fs;
32use std::fs::OpenOptions;
33use std::io::Write;
34use std::path::{Component, Path, PathBuf};
35use std::sync::{Arc, Mutex};
36
37use crate::change_id::ChangeIdGenerator;
38use crate::check_ref_format::validate_branch_short_name;
39use crate::command_runner::CommandRunner;
40use crate::config::{
41 config_file_stamps, stamp_for_path, ConfigCacheKey, ConfigFile, ConfigScope, ConfigSet,
42 LoadConfigOptions,
43};
44use crate::diagnostics::{self, DiagnosticsHandle};
45use crate::environment::{Environment, RepositoryOptions};
46use crate::error::{Error, NotARepositoryReason, Result};
47use crate::git_dir::io_error::map_index_write_error;
48use crate::git_path::path_for_error_message;
49use crate::hooks::run_hook;
50use crate::index::Index;
51use crate::init_filesystem::{apply_init_filesystem_config, InitFilesystemConfigOptions};
52use crate::objects::{parse_commit, HashAlgo, ObjectId};
53use crate::odb::Odb;
54use crate::repo_caches::RepoCaches;
55use crate::rev_parse::is_inside_work_tree;
56use crate::sparse_checkout::effective_cone_mode_for_sparse_file;
57use crate::split_index::{write_index_file_split, WriteSplitIndexRequest};
58use crate::state::resolve_head;
59use crate::worktree_cwd::cwd_relative_under_work_tree;
60
61fn compute_git_prefix(env: &Environment, work_tree: &Path) -> Option<String> {
63 if let Some(prefix) = env.git_prefix.as_deref() {
64 let p = prefix.trim();
65 if !p.is_empty() {
66 return Some(p.to_owned());
67 }
68 }
69 let rel = cwd_relative_under_work_tree(work_tree, &env.cwd)?;
70 if rel.is_empty() {
71 None
72 } else {
73 Some(rel)
74 }
75}
76
77fn read_sparse_checkout_patterns(git_dir: &Path) -> Vec<String> {
78 let path = git_dir.join("info").join("sparse-checkout");
79 let Ok(content) = fs::read_to_string(&path) else {
80 return Vec::new();
81 };
82 content
83 .lines()
84 .map(|l| l.trim())
85 .filter(|l| !l.is_empty() && !l.starts_with('#'))
86 .map(String::from)
87 .collect()
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct DiscoveryInfo {
93 pub explicit_git_dir: bool,
95 pub discovery_root: Option<PathBuf>,
97 pub work_tree_from_env: bool,
99 pub discovery_via_gitfile: bool,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct InitOutcome {
106 pub reinitialized: bool,
108 pub initial_branch: String,
110}
111
112#[derive(Debug, Clone)]
114#[non_exhaustive]
115pub struct InitOptions {
116 pub bare: bool,
118 pub initial_branch: Option<String>,
124 pub template_dir: Option<PathBuf>,
126 pub ref_storage: crate::ref_storage::RefStorageFormat,
128 pub separate_git_dir: Option<PathBuf>,
130 pub bare_with_worktree_at_path: bool,
132 pub object_format: Option<HashAlgo>,
136 pub environment: Option<Environment>,
138}
139
140impl Default for InitOptions {
141 fn default() -> Self {
142 Self {
143 bare: false,
144 initial_branch: None,
145 template_dir: None,
146 ref_storage: crate::ref_storage::RefStorageFormat::Files,
147 separate_git_dir: None,
148 bare_with_worktree_at_path: false,
149 object_format: None,
150 environment: None,
151 }
152 }
153}
154
155impl InitOptions {
156 #[must_use]
158 pub fn new() -> Self {
159 Self::default()
160 }
161
162 #[must_use]
164 pub fn with_bare(mut self, bare: bool) -> Self {
165 self.bare = bare;
166 self
167 }
168
169 #[must_use]
171 pub fn with_initial_branch(mut self, initial_branch: impl Into<String>) -> Self {
172 self.initial_branch = Some(initial_branch.into());
173 self
174 }
175
176 #[must_use]
178 pub fn with_object_format(mut self, object_format: HashAlgo) -> Self {
179 self.object_format = Some(object_format);
180 self
181 }
182
183 #[must_use]
185 pub fn with_environment(mut self, environment: Environment) -> Self {
186 self.environment = Some(environment);
187 self
188 }
189
190 #[must_use]
192 pub fn with_ref_storage(mut self, ref_storage: crate::ref_storage::RefStorageFormat) -> Self {
193 self.ref_storage = ref_storage;
194 self
195 }
196
197 #[must_use]
199 pub fn with_template_dir(mut self, template_dir: Option<PathBuf>) -> Self {
200 self.template_dir = template_dir;
201 self
202 }
203
204 #[must_use]
206 pub fn with_separate_git_dir(mut self, separate_git_dir: Option<PathBuf>) -> Self {
207 self.separate_git_dir = separate_git_dir;
208 self
209 }
210
211 #[must_use]
213 pub fn with_bare_with_worktree_at_path(mut self, bare_with_worktree_at_path: bool) -> Self {
214 self.bare_with_worktree_at_path = bare_with_worktree_at_path;
215 self
216 }
217}
218
219#[must_use]
263pub struct RepositoryBuilder {
264 options: RepositoryOptions,
265 odb_builder: Option<crate::odb::OdbBuilder>,
266 ref_store: Option<Arc<dyn crate::refs::store::RefStore>>,
267 initial_branch: Option<String>,
268}
269
270impl RepositoryBuilder {
271 pub fn new(options: RepositoryOptions) -> Self {
273 Self {
274 options,
275 odb_builder: None,
276 ref_store: None,
277 initial_branch: None,
278 }
279 }
280
281 pub fn odb_builder(mut self, builder: crate::odb::OdbBuilder) -> Self {
290 self.odb_builder = Some(builder);
291 self
292 }
293
294 pub fn ref_store(mut self, store: Arc<dyn crate::refs::store::RefStore>) -> Self {
305 self.ref_store = Some(store);
306 self
307 }
308
309 pub fn initial_branch(mut self, branch: impl Into<String>) -> Self {
314 self.initial_branch = Some(branch.into());
315 self
316 }
317
318 pub fn open(self, git_dir: &Path, work_tree: Option<&Path>) -> Result<Repository> {
338 let mut options = self.options;
339 if let Some(wt) = work_tree {
340 options.work_tree = Some(wt.to_path_buf());
341 }
342 let repo = if let Some(builder) = self.odb_builder {
343 Repository::open_with_odb_internal(&options, git_dir, builder, self.ref_store)?
344 } else {
345 Repository::open_internal_with_refs(&options, git_dir, self.ref_store)?
346 };
347 if let Some(branch) = self.initial_branch {
348 seed_unborn_head(repo.refs(), &branch)?;
349 }
350 Ok(repo)
351 }
352}
353
354fn seed_unborn_head(store: &dyn crate::refs::store::RefStore, branch: &str) -> Result<()> {
356 use crate::refs::store::{Expected, RawRef, RefStoreError, RefTransaction, RefUpdate};
357
358 validate_branch_short_name(branch).map_err(|e| Error::InvalidRef(e.to_string()))?;
359 if store.read_raw("HEAD")?.is_some() {
360 return Ok(());
361 }
362 let txn = RefTransaction::new().update(RefUpdate {
363 name: "HEAD".to_owned(),
364 new_value: Some(RawRef::Symbolic(format!("refs/heads/{branch}"))),
365 expected: Expected::Missing,
366 reflog: None,
367 flags: crate::refs::store::RefUpdateFlags {
368 no_deref: true,
369 ..Default::default()
370 },
371 })?;
372 match store.prepare(txn).and_then(|prepared| prepared.commit()) {
373 Ok(()) | Err(RefStoreError::ExpectedMismatch { .. }) => Ok(()),
374 Err(err) => Err(err.into()),
375 }
376}
377
378pub struct Repository {
416 git_dir: PathBuf,
417 common_git_dir: PathBuf,
419 work_tree: Option<PathBuf>,
420 odb: Odb,
421 explicit_git_dir: bool,
422 discovery_root: Option<PathBuf>,
423 work_tree_from_env: bool,
424 discovery_via_gitfile: bool,
425 config_session: Mutex<Option<ConfigSession>>,
427 caches: Arc<RepoCaches>,
429 ref_store: Arc<dyn crate::refs::store::RefStore>,
431 environment: Arc<Environment>,
433 git_prefix: Option<String>,
435 command_runner: Arc<dyn CommandRunner>,
437 change_id_generator: Arc<dyn ChangeIdGenerator>,
439 diagnostics: DiagnosticsHandle,
440 network_trace: bool,
441 reference_unix_time: Option<i64>,
443 test_assume_different_owner: bool,
444 thread_policy: crate::parallel::ThreadPolicy,
445 hook_stdio: crate::hooks::HookStdio,
446}
447
448const _: () = {
449 fn assert_send_sync<T: Send + Sync>() {}
450 let _ = assert_send_sync::<Repository>;
451};
452
453impl std::fmt::Debug for Repository {
454 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455 f.debug_struct("Repository")
456 .field("git_dir", &self.git_dir())
457 .field("work_tree", &self.work_tree())
458 .field("explicit_git_dir", &self.discovery_info().explicit_git_dir)
459 .field("network_trace", &self.network_trace)
460 .finish_non_exhaustive()
461 }
462}
463
464pub(crate) type RepositoryConfigSnapshot = Arc<RepoCaches>;
466
467#[derive(Debug, Clone)]
469struct RepoCachedSettings {
470 use_replace_refs: bool,
472 replace_ref_base: String,
474}
475
476#[derive(Clone)]
478struct ConfigSessionValidation {
479 key: ConfigCacheKey,
480 env_fingerprint: Vec<(String, Option<String>)>,
481 base_stamps: Vec<(PathBuf, Option<(std::time::SystemTime, u64)>)>,
482 extra_stamps: Vec<(PathBuf, Option<(std::time::SystemTime, u64)>)>,
483}
484
485struct ConfigSession {
487 config: Arc<ConfigSet>,
488 validation: ConfigSessionValidation,
489 cached_settings: RepoCachedSettings,
490 replace_lookup_inactive: bool,
492}
493
494impl ConfigSessionValidation {
495 fn still_valid(&self, env: &Environment, git_dir: &Path, opts: &LoadConfigOptions) -> bool {
496 let Some(env_fp) = env.config_fingerprint() else {
497 return false;
498 };
499 if self.env_fingerprint.as_slice() != env_fp.as_slice() {
500 return false;
501 }
502 let key = ConfigCacheKey::new(Some(git_dir), opts);
503 if key != self.key {
504 return false;
505 }
506 let base_stamps = config_file_stamps(env, Some(git_dir), opts);
507 if self.base_stamps.as_slice() != base_stamps.as_slice() {
508 return false;
509 }
510 for (path, stamp) in &self.extra_stamps {
511 if stamp_for_path(path) != *stamp {
512 return false;
513 }
514 }
515 true
516 }
517}
518
519impl Repository {
520 fn config_load_options(&self, include_system: bool) -> LoadConfigOptions {
521 LoadConfigOptions {
522 include_system,
523 include_ctx: crate::config::IncludeContext {
524 git_dir: Some(self.git_dir.clone()),
525 cwd: self.environment.cwd.clone(),
526 pwd: self.environment.pwd.clone(),
527 ..Default::default()
528 },
529 diagnostics: Some(self.diagnostics()),
530 repo_caches: Some(Arc::clone(&self.caches)),
531 ..Default::default()
532 }
533 }
534
535 fn from_canonical_git_dir(
536 options: &RepositoryOptions,
537 git_dir: PathBuf,
538 work_tree: Option<&Path>,
539 injected_refs: Option<Arc<dyn crate::refs::store::RefStore>>,
540 ) -> Result<Self> {
541 let environment = Arc::new(options.environment.clone());
542 let command_runner = Arc::clone(&options.command_runner);
543 let change_id_generator = Arc::clone(&options.change_id_generator);
544 if injected_refs.is_none() && !head_file_present(&git_dir) {
547 return Err(not_a_repository_at(&git_dir));
548 }
549
550 let common_git_dir = resolve_common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
551 let objects_dir = resolve_objects_dir(&environment, &git_dir, &common_git_dir)
552 .filter(|dir| dir.exists())
553 .ok_or_else(|| not_a_repository_at(&git_dir))?;
554
555 let work_tree = match work_tree {
556 Some(p) => {
557 let cwd = environment.discovery_cwd();
558 let mut resolved = if p.is_absolute() {
559 p.to_path_buf()
560 } else {
561 cwd.join(p)
562 };
563 if resolved.exists() {
564 resolved = resolved
565 .canonicalize_simplified()
566 .map_err(|_| Error::PathError(p.display().to_string()))?;
567 }
568 Some(resolved)
569 }
570 None => None,
571 };
572
573 let caches = RepoCaches::with_diagnostics(
574 Arc::clone(&command_runner),
575 Arc::clone(&options.diagnostics),
576 );
577 caches.set_promisor_fetcher(options.promisor_fetcher.clone());
578 let odb = if let Some(ref wt) = work_tree {
579 Odb::with_work_tree(&objects_dir, wt)
580 .with_config_git_dir(git_dir.clone())
581 .with_shared_config_state(caches.clone(), environment.clone())
582 } else {
583 Odb::new(&objects_dir)
584 .with_config_git_dir(git_dir.clone())
585 .with_shared_config_state(caches.clone(), environment.clone())
586 };
587
588 let git_prefix = work_tree
589 .as_ref()
590 .and_then(|wt| compute_git_prefix(environment.as_ref(), wt));
591
592 let ref_store = open_or_install_ref_store(&caches, &git_dir, &environment, injected_refs)?;
593 caches.configure_filter_subprocess(environment.as_ref(), &git_dir, work_tree.as_deref());
594
595 Ok(Self {
596 git_dir,
597 common_git_dir,
598 work_tree,
599 odb,
600 explicit_git_dir: false,
601 discovery_root: None,
602 work_tree_from_env: false,
603 discovery_via_gitfile: false,
604 config_session: Mutex::new(None),
605 caches,
606 ref_store,
607 environment,
608 git_prefix,
609 command_runner,
610 change_id_generator,
611 diagnostics: Arc::clone(&options.diagnostics),
612 network_trace: options.network_trace,
613 reference_unix_time: options.reference_unix_time,
614 test_assume_different_owner: options.test_assume_different_owner,
615 thread_policy: options.thread_policy,
616 hook_stdio: options.hook_stdio,
617 })
618 }
619
620 #[must_use]
622 pub fn thread_policy(&self) -> crate::parallel::ThreadPolicy {
623 self.thread_policy
624 }
625
626 #[must_use]
628 pub fn with_thread_policy(mut self, policy: crate::parallel::ThreadPolicy) -> Self {
629 self.thread_policy = policy;
630 self
631 }
632
633 #[must_use]
635 pub(crate) fn caches(&self) -> &Arc<RepoCaches> {
636 &self.caches
637 }
638
639 #[must_use]
641 pub(crate) fn promisor_hydrate_hook(&self) -> Option<crate::blame::PromisorHydrateHook> {
642 self.caches.promisor_hydrate_hook()
643 }
644
645 #[must_use]
647 pub fn environment(&self) -> &Environment {
648 &self.environment
649 }
650
651 #[must_use]
653 pub fn reference_unix_time(&self) -> i64 {
654 self.wall_clock_epoch()
655 }
656
657 #[must_use]
659 pub fn wall_clock_epoch(&self) -> i64 {
660 if let Some(ts) = self.reference_unix_time {
661 return ts;
662 }
663 if let Some(ts) = self.environment.git_now_date_override() {
664 return ts;
665 }
666 crate::git_date::tm::process_wall_clock_sec()
667 }
668
669 #[must_use]
673 pub fn identity_now(&self) -> time::OffsetDateTime {
674 crate::environment::offset_from_epoch_and_tz(
675 self.wall_clock_epoch(),
676 self.environment.tz.as_deref(),
677 )
678 }
679
680 pub fn identity(
714 &self,
715 role: crate::config::ident_resolve::IdentRole,
716 now: time::OffsetDateTime,
717 ) -> Result<crate::ident::Signature> {
718 let config = self.config()?;
719 Ok(crate::config::ident_resolve::resolve_signature_with(
720 self.environment(),
721 &config,
722 role,
723 now,
724 )?)
725 }
726
727 #[must_use]
729 pub fn command_runner(&self) -> Arc<dyn CommandRunner> {
730 Arc::clone(&self.command_runner)
731 }
732
733 #[must_use]
736 pub fn hook_stdio(&self) -> crate::hooks::HookStdio {
737 self.hook_stdio
738 }
739
740 #[must_use]
742 pub fn change_id_generator(&self) -> Arc<dyn ChangeIdGenerator> {
743 Arc::clone(&self.change_id_generator)
744 }
745
746 #[must_use]
752 pub fn linked_worktree_discover_options(&self) -> RepositoryOptions {
753 let mut environment = (*self.environment).clone();
754 environment.git_dir = None;
755 environment.git_work_tree = None;
756 environment.git_index_file = None;
757 environment.git_prefix = None;
758 environment.git_object_directory = None;
759 RepositoryOptions {
760 environment,
761 command_runner: Arc::clone(&self.command_runner),
762 change_id_generator: Arc::clone(&self.change_id_generator),
763 diagnostics: Arc::clone(&self.diagnostics),
764 network_trace: self.network_trace,
765 test_assume_different_owner: self.test_assume_different_owner,
766 reference_unix_time: self.reference_unix_time,
767 thread_policy: self.thread_policy,
768 work_tree: None,
769 validate_repository_format: true,
770 open_for_serving: false,
771 promisor_fetcher: self.caches().promisor_fetcher(),
772 hook_stdio: self.hook_stdio,
773 }
774 }
775
776 pub fn open_linked_worktree(&self, admin_dir: &Path, work_tree: &Path) -> Result<Self> {
792 let options = self.linked_worktree_discover_options();
793 if self.refs_on_disk() && self.odb.uses_files_primary() {
794 return Self::discover(Some(work_tree), &options);
795 }
796 let git_dir = admin_dir
797 .canonicalize_simplified()
798 .map_err(|_| not_a_repository_at(admin_dir))?;
799 let work_tree = work_tree
800 .canonicalize_simplified()
801 .map_err(|_| Error::PathError(work_tree.display().to_string()))?;
802 let environment = Arc::new(options.environment);
803 let caches = RepoCaches::with_diagnostics(
804 Arc::clone(&self.command_runner),
805 Arc::clone(&self.diagnostics),
806 );
807 let ref_store = if self.refs_on_disk() {
808 caches.open_ref_store(&git_dir, &environment)?
809 } else {
810 let view: Arc<dyn crate::refs::store::RefStore> =
811 Arc::new(crate::refs::store::LinkedWorktreeRefStore::new(
812 git_dir.clone(),
813 Arc::clone(&self.ref_store),
814 ));
815 caches.install_ref_store(&git_dir, Arc::clone(&view));
816 view
817 };
818 let repo = Self {
819 git_prefix: compute_git_prefix(&environment, &work_tree),
820 git_dir,
821 common_git_dir: self.common_git_dir.clone(),
822 work_tree: Some(work_tree),
823 odb: self.odb.clone(),
824 explicit_git_dir: false,
825 discovery_root: None,
826 work_tree_from_env: false,
827 discovery_via_gitfile: true,
828 config_session: Mutex::new(None),
829 caches,
830 ref_store,
831 environment,
832 command_runner: Arc::clone(&self.command_runner),
833 change_id_generator: Arc::clone(&self.change_id_generator),
834 diagnostics: Arc::clone(&self.diagnostics),
835 network_trace: self.network_trace,
836 reference_unix_time: self.reference_unix_time,
837 test_assume_different_owner: self.test_assume_different_owner,
838 thread_policy: self.thread_policy,
839 hook_stdio: self.hook_stdio,
840 };
841 repo.caches().configure_filter_subprocess(
842 repo.environment.as_ref(),
843 repo.git_dir(),
844 repo.work_tree(),
845 );
846 repo.refresh_config_session()?;
847 Ok(repo)
848 }
849
850 fn repo_cached_settings_from_config(
851 cfg: &ConfigSet,
852 environment: &Environment,
853 ) -> RepoCachedSettings {
854 let use_replace_refs = cfg
855 .get_bool("core.useReplaceRefs")
856 .and_then(|r| r.ok())
857 .unwrap_or(true);
858 let replace_ref_base = environment
859 .git_replace_ref_base
860 .clone()
861 .filter(|s| !s.is_empty())
862 .unwrap_or_else(|| "refs/replace/".to_owned());
863 let replace_ref_base = if replace_ref_base.ends_with('/') {
864 replace_ref_base
865 } else {
866 format!("{replace_ref_base}/")
867 };
868 RepoCachedSettings {
869 use_replace_refs,
870 replace_ref_base,
871 }
872 }
873
874 fn ensure_config_arc(&self) -> Result<Arc<ConfigSet>> {
875 self.config_arc_with_validation(true)
876 }
877
878 pub(crate) fn ensure_config_arc_fast(&self) -> Result<Arc<ConfigSet>> {
880 self.config_arc_with_validation(false)
881 }
882
883 fn config_arc_with_validation(&self, validate_stamps: bool) -> Result<Arc<ConfigSet>> {
884 if validate_stamps {
885 if let Ok(guard) = self.config_session.lock() {
886 if let Some(session) = guard.as_ref() {
887 let opts = self.config_load_options(true);
888 if session.validation.still_valid(
889 self.environment.as_ref(),
890 &self.git_dir,
891 &opts,
892 ) {
893 return Ok(Arc::clone(&session.config));
894 }
895 }
896 }
897 } else if let Ok(guard) = self.config_session.lock() {
898 if let Some(session) = guard.as_ref() {
899 return Ok(Arc::clone(&session.config));
900 }
901 }
902 self.refresh_config_session()?;
903 self.config_session
904 .lock()
905 .map_err(|e| Error::Message(format!("config session lock poisoned: {e}")))?
906 .as_ref()
907 .map(|s| Arc::clone(&s.config))
908 .ok_or_else(|| Error::Message("config session missing after refresh".to_owned()))
909 }
910
911 fn refresh_config_session(&self) -> Result<()> {
912 crate::trace::trace_span!("grit.config.load");
913 let opts = self.config_load_options(true);
914 if let Ok(guard) = self.config_session.lock() {
915 if let Some(session) = guard.as_ref() {
916 if session
917 .validation
918 .still_valid(self.environment.as_ref(), &self.git_dir, &opts)
919 {
920 return Ok(());
921 }
922 let env = self.environment.as_ref();
923 let base_stamps = config_file_stamps(env, Some(&self.git_dir), &opts);
924 if session.validation.base_stamps.as_slice() != base_stamps.as_slice() {
925 self.caches.invalidate_config_file_derived();
926 }
927 }
928 }
929 self.caches.clear_session_config();
930 let env = self.environment.as_ref();
931 let cfg = self
932 .caches
933 .load_config_with_options(env, Some(&self.git_dir), &opts)?;
934 let key = ConfigCacheKey::new(Some(&self.git_dir), &opts);
935 let (env_fingerprint, base_stamps, extra_stamps) =
936 self.caches.config_entry_stamps(&key).unwrap_or_else(|| {
937 (
938 env.config_fingerprint().unwrap_or_default(),
939 config_file_stamps(env, Some(&self.git_dir), &opts),
940 Vec::new(),
941 )
942 });
943 let cached_settings =
944 Self::repo_cached_settings_from_config(cfg.as_ref(), self.environment.as_ref());
945 let replace_lookup_inactive = self.replace_lookup_inactive(cfg.as_ref());
946 self.caches
947 .set_session_config(key.clone(), Arc::clone(&cfg));
948 let session = ConfigSession {
949 config: Arc::clone(&cfg),
950 validation: ConfigSessionValidation {
951 key,
952 env_fingerprint,
953 base_stamps,
954 extra_stamps,
955 },
956 cached_settings,
957 replace_lookup_inactive,
958 };
959 *self
960 .config_session
961 .lock()
962 .map_err(|e| Error::Message(format!("config session lock poisoned: {e}")))? =
963 Some(session);
964 Ok(())
965 }
966
967 fn replace_lookup_inactive(&self, cfg: &ConfigSet) -> bool {
968 if self.environment.git_no_replace_objects {
969 return true;
970 }
971 if !cfg
972 .get_bool("core.useReplaceRefs")
973 .and_then(|r| r.ok())
974 .unwrap_or(true)
975 {
976 return true;
977 }
978 let replace_dir = self.common_git_dir.join("refs/replace");
979 !replace_dir_is_non_empty(&replace_dir)
980 }
981
982 #[must_use]
984 pub fn diagnostics(&self) -> DiagnosticsHandle {
985 Arc::clone(&self.diagnostics)
986 }
987
988 pub fn set_diagnostics(&mut self, sink: DiagnosticsHandle) {
990 self.caches.set_diagnostics(Arc::clone(&sink));
991 self.diagnostics = sink;
992 }
993
994 #[must_use]
996 pub fn network_trace_enabled(&self) -> bool {
997 self.network_trace
998 }
999
1000 pub fn set_network_trace(&mut self, enabled: bool) {
1002 self.network_trace = enabled;
1003 }
1004
1005 pub(crate) fn warn(&self, warning: diagnostics::Warning) {
1006 self.diagnostics.warn(warning);
1007 }
1008
1009 pub fn config(&self) -> Result<Arc<ConfigSet>> {
1020 self.ensure_config_arc()
1021 }
1022
1023 pub fn reload_config(&self) -> Result<()> {
1032 self.caches.invalidate_config_cache();
1033 if let Ok(mut guard) = self.config_session.lock() {
1034 *guard = None;
1035 }
1036 self.refresh_config_session()?;
1037 Ok(())
1038 }
1039
1040 pub(crate) fn install_config_snapshot(&self) {
1042 let _ = self
1043 .caches
1044 .open_ref_store(&self.git_dir, self.environment.as_ref());
1045 }
1046
1047 #[must_use]
1054 pub fn common_git_dir(&self) -> &Path {
1055 &self.common_git_dir
1056 }
1057
1058 fn validate_repository_format_cached(&self) -> Result<()> {
1059 let Some(config_path) = repository_config_path(&self.git_dir) else {
1060 return Ok(());
1061 };
1062 let parsed = self.caches.repository_format(&self.git_dir, &config_path)?;
1063 validate_repository_format_parsed(&parsed)
1064 }
1065
1066 #[must_use]
1068 pub fn refs(&self) -> &dyn crate::refs::store::RefStore {
1069 self.ref_store.as_ref()
1070 }
1071
1072 #[must_use]
1076 pub(crate) fn refs_on_disk(&self) -> bool {
1077 use crate::refs::store::RefStorageFormat;
1078 matches!(
1079 self.ref_store.format(),
1080 RefStorageFormat::Files | RefStorageFormat::Reftable
1081 )
1082 }
1083
1084 #[inline]
1086 #[must_use]
1087 pub fn objects(&self) -> crate::objects::Objects<'_> {
1088 crate::objects::Objects::new(self)
1089 }
1090
1091 #[inline]
1093 #[must_use]
1094 pub fn references(&self) -> crate::refs::References<'_> {
1095 crate::refs::References::new(self)
1096 }
1097
1098 #[inline]
1100 #[must_use]
1101 pub fn index_handle(&self) -> crate::index::IndexHandle<'_> {
1102 crate::index::IndexHandle::new(self)
1103 }
1104
1105 #[inline]
1107 #[must_use]
1108 pub fn config_handle(&self) -> crate::config::ConfigHandle<'_> {
1109 crate::config::ConfigHandle::new(self)
1110 }
1111
1112 #[inline]
1114 #[must_use]
1115 pub fn rev_walk(&self) -> crate::revwalk::RevWalk<'_> {
1116 crate::revwalk::RevWalk::new(self)
1117 }
1118
1119 #[inline]
1121 #[must_use]
1122 pub fn diff(&self) -> crate::diff::Diff<'_> {
1123 crate::diff::Diff::new(self)
1124 }
1125
1126 #[must_use]
1128 pub fn remotes(&self) -> crate::repo_handles::RepositoryRemotes<'_> {
1129 crate::repo_handles::RepositoryRemotes::new(self)
1130 }
1131
1132 #[must_use]
1134 pub fn worktrees(&self) -> crate::repo_handles::RepositoryWorktrees<'_> {
1135 crate::repo_handles::RepositoryWorktrees::new(self)
1136 }
1137
1138 #[must_use]
1140 pub fn stash(&self) -> crate::repo_handles::RepositoryStash<'_> {
1141 crate::repo_handles::RepositoryStash::new(self)
1142 }
1143
1144 #[must_use]
1146 pub fn notes(&self) -> crate::repo_handles::RepositoryNotes<'_> {
1147 crate::repo_handles::RepositoryNotes::new(self)
1148 }
1149
1150 #[must_use]
1152 pub fn hooks(&self) -> crate::repo_handles::RepositoryHooks<'_> {
1153 crate::repo_handles::RepositoryHooks::new(self)
1154 }
1155
1156 pub fn fsck(
1165 &self,
1166 options: &crate::fsck::FsckOptions,
1167 progress: &mut dyn crate::progress::ProgressSink,
1168 cancel: &dyn crate::progress::Cancel,
1169 ) -> crate::error::Result<crate::fsck::FsckReport> {
1170 crate::fsck::fsck(self, options, progress, cancel)
1171 }
1172
1173 pub fn tree_sha256(
1182 &self,
1183 oid: crate::objects::ObjectId,
1184 options: &crate::tree_sha256::TreeSha256Options,
1185 ) -> crate::error::Result<crate::tree_sha256::TreeSha256Report> {
1186 crate::tree_sha256::tree_sha256(self, oid, options)
1187 }
1188
1189 pub fn verify_tag(
1198 &self,
1199 oid: crate::objects::ObjectId,
1200 options: &crate::tree_sha256::TagVerifyOptions,
1201 ) -> crate::error::Result<crate::tree_sha256::TagVerification> {
1202 crate::tree_sha256::verify_tag(self, oid, options)
1203 }
1204
1205 pub fn patch_id(
1215 &self,
1216 commit: &crate::objects::ObjectId,
1217 mode: crate::patch_ids::PatchIdMode,
1218 ) -> crate::error::Result<Option<crate::objects::ObjectId>> {
1219 crate::patch_ids::compute_patch_id(self.odb().hash_algo(), self.odb(), commit, mode)
1220 }
1221
1222 #[must_use]
1224 pub fn reflogs(&self) -> crate::repo_handles::RepositoryReflogs<'_> {
1225 crate::repo_handles::RepositoryReflogs::new(self)
1226 }
1227
1228 #[must_use]
1230 pub fn bundles(&self) -> crate::repo_handles::RepositoryBundles<'_> {
1231 crate::repo_handles::RepositoryBundles::new(self)
1232 }
1233
1234 pub fn repack(
1240 &self,
1241 opts: &crate::maintenance::RepackOptions,
1242 ) -> crate::error::Result<crate::maintenance::RepackReport> {
1243 crate::maintenance::repack(self, opts)
1244 }
1245
1246 pub(crate) fn resolve_ref_oid(&self, refname: &str) -> Result<crate::objects::ObjectId> {
1252 self.ref_store.resolve(refname)
1253 }
1254
1255 #[must_use]
1260 pub fn with_ref_store(mut self, store: Arc<dyn crate::refs::store::RefStore>) -> Self {
1261 let store = linked_worktree_view(&self.git_dir, store);
1262 self.caches
1263 .install_ref_store(&self.git_dir, Arc::clone(&store));
1264 self.ref_store = store;
1265 self
1266 }
1267
1268 #[must_use]
1270 pub(crate) fn pathspec_precompose_enabled(&self) -> bool {
1271 self.caches
1272 .pathspec_precompose_enabled(self.environment.as_ref(), &self.git_dir)
1273 }
1274
1275 #[cfg(test)]
1278 pub(crate) fn force_pathspec_precompose_for_test(&self) {
1279 self.caches.force_pathspec_precompose_for_test();
1280 }
1281
1282 pub fn resolve_worktree_pathspec(
1296 &self,
1297 pathspec: &str,
1298 prefix: Option<&str>,
1299 ) -> std::result::Result<String, crate::pathspec::PathspecResolveError> {
1300 let work_tree = self
1301 .work_tree()
1302 .ok_or(crate::pathspec::PathspecResolveError::NeedsWorkTree)?;
1303 Ok(
1304 crate::pathspec::resolve_pathspec_in_worktree_with_argv_precompose(
1305 pathspec,
1306 pathspec,
1307 work_tree,
1308 prefix,
1309 self.pathspec_precompose_enabled(),
1310 )?,
1311 )
1312 }
1313
1314 #[must_use]
1315 pub(crate) fn pathspec_match_context(&self) -> crate::pathspec::PathspecMatchContext {
1316 crate::pathspec::pathspec_match_context_with_precompose(self.pathspec_precompose_enabled())
1317 }
1318
1319 pub fn paths_match_pathspecs(&self, path: &str, specs: &[String]) -> Result<bool> {
1329 Ok(self.pathspec_matcher(specs)?.matches(path))
1330 }
1331
1332 pub fn pathspec_matcher<'s>(
1348 &self,
1349 specs: &'s [String],
1350 ) -> Result<crate::pathspec::PathspecMatcher<'s>> {
1351 let precompose = self.pathspec_precompose_enabled();
1352 if !specs
1353 .iter()
1354 .any(|spec| crate::pathspec::pathspec_has_attr_magic(spec))
1355 {
1356 return Ok(crate::pathspec::PathspecMatcher::new(
1357 specs, precompose, None, false,
1358 ));
1359 }
1360 crate::pathspec::validate_attr_pathspecs(specs)?;
1361 let attrs = match self.work_tree() {
1362 Some(work_tree) => self.caches().gitattributes_stack_shared(self, work_tree)?,
1363 None => self.caches().gitattributes_bare_shared(self)?,
1364 };
1365 let icase = crate::crlf::config_ignorecase(self.config()?.as_ref());
1366 Ok(crate::pathspec::PathspecMatcher::new(
1367 specs,
1368 precompose,
1369 Some(attrs),
1370 icase,
1371 ))
1372 }
1373
1374 #[must_use]
1379 pub fn pathspec_cwd_prefix(&self) -> Option<&str> {
1380 self.git_prefix.as_deref().filter(|p| !p.is_empty())
1381 }
1382
1383 pub fn pathspec_argv_prefix<'a>(&'a self, pathspec: &str) -> Option<&'a str> {
1385 if crate::pathspec::pathspec_has_top(pathspec) {
1386 None
1387 } else {
1388 self.pathspec_cwd_prefix()
1389 }
1390 }
1391
1392 pub fn resolve_rev_walk_pathspecs(&self, paths: &[String]) -> Result<Vec<String>> {
1402 if paths.is_empty() {
1403 return Ok(Vec::new());
1404 }
1405 let prefix = self.pathspec_cwd_prefix();
1406 let cwd_for_implicit = prefix.map(str::to_owned);
1407 let mut resolved = Vec::with_capacity(paths.len());
1408 if self.work_tree().is_some() {
1409 for pathspec in paths {
1410 let prefix = Self::pathspec_argv_prefix(self, pathspec);
1411 let spec = self
1412 .resolve_worktree_pathspec(pathspec, prefix)
1413 .map_err(|e| Error::PathError(e.to_string()))?;
1414 resolved.push(spec);
1415 }
1416 } else {
1417 let root = self.git_dir();
1418 for pathspec in paths {
1419 let prefix = Self::pathspec_argv_prefix(self, pathspec);
1420 resolved.push(crate::pathspec::resolve_pathspec(pathspec, root, prefix));
1421 }
1422 }
1423 Ok(crate::pathspec::extend_pathspec_list_implicit_cwd(
1424 &resolved,
1425 cwd_for_implicit.as_deref(),
1426 ))
1427 }
1428
1429 #[must_use]
1436 pub fn git_dir(&self) -> &Path {
1437 &self.git_dir
1438 }
1439
1440 #[must_use]
1447 pub fn work_tree(&self) -> Option<&Path> {
1448 self.work_tree.as_deref()
1449 }
1450
1451 #[must_use]
1453 pub fn odb(&self) -> &Odb {
1454 &self.odb
1455 }
1456
1457 pub fn prefetch_promised(&self, oids: &[ObjectId]) -> Result<()> {
1463 let Some(fetcher) = self.caches().promisor_fetcher() else {
1464 return Err(Error::Message(
1465 "promisor prefetch requires a PromisorFetcher on RepositoryOptions".into(),
1466 ));
1467 };
1468 fetcher.fetch_missing(self, oids)
1469 }
1470
1471 #[must_use]
1473 pub fn discovery_info(&self) -> DiscoveryInfo {
1474 DiscoveryInfo {
1475 explicit_git_dir: self.explicit_git_dir,
1476 discovery_root: self.discovery_root.clone(),
1477 work_tree_from_env: self.work_tree_from_env,
1478 discovery_via_gitfile: self.discovery_via_gitfile,
1479 }
1480 }
1481
1482 fn cached_settings(&self) -> RepoCachedSettings {
1483 if let Ok(guard) = self.config_session.lock() {
1484 if let Some(session) = guard.as_ref() {
1485 return session.cached_settings.clone();
1486 }
1487 }
1488 self.ensure_config_arc_fast()
1489 .map(|cfg| {
1490 Self::repo_cached_settings_from_config(cfg.as_ref(), self.environment.as_ref())
1491 })
1492 .unwrap_or_else(|_| {
1493 Self::repo_cached_settings_from_config(&ConfigSet::new(), self.environment.as_ref())
1494 })
1495 }
1496
1497 pub fn init(path: &Path, options: &InitOptions) -> Result<Self> {
1503 Self::init_with_outcome(path, options).map(|(repo, _)| repo)
1504 }
1505
1506 pub fn init_with_outcome(path: &Path, options: &InitOptions) -> Result<(Self, InitOutcome)> {
1513 if options.bare && options.separate_git_dir.is_some() {
1514 return Err(Error::InitBareWithSeparateGitDir);
1515 }
1516 let env = options
1517 .environment
1518 .clone()
1519 .unwrap_or_else(Environment::empty);
1520 let git_dir = resolve_git_dir_for_init(path, options)?;
1521 let reinitialized = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
1522 let existing_format = hash_algo_from_git_dir(&git_dir)?;
1523 let object_format = match (reinitialized, options.object_format) {
1524 (true, None) => existing_format,
1525 (true, Some(requested)) if requested != existing_format => {
1526 return Err(Error::InitObjectFormatChange);
1527 }
1528 (true, Some(requested)) => requested,
1529 (false, None) => HashAlgo::Sha1,
1530 (false, Some(requested)) => requested,
1531 };
1532 let initial_branch = if reinitialized {
1533 head_branch_name_for_outcome(&git_dir)?
1534 } else {
1535 resolve_initial_branch(&env, options.initial_branch.as_deref())?
1536 };
1537
1538 let separate_layout = options.separate_git_dir.is_some()
1539 || (!options.bare
1540 && !options.bare_with_worktree_at_path
1541 && path.join(".git").is_file());
1542
1543 let repo = if options.bare_with_worktree_at_path {
1544 init_bare_with_env_worktree(
1545 path,
1546 path,
1547 &initial_branch,
1548 options.template_dir.as_deref(),
1549 options.ref_storage,
1550 object_format,
1551 )?
1552 } else if separate_layout {
1553 init_repository_separate_git_dir(
1554 path,
1555 &git_dir,
1556 &initial_branch,
1557 options.template_dir.as_deref(),
1558 options.ref_storage,
1559 object_format,
1560 )?
1561 } else {
1562 init_repository_with_object_format(
1563 path,
1564 options.bare,
1565 &initial_branch,
1566 options.template_dir.as_deref(),
1567 options.ref_storage,
1568 object_format,
1569 )?
1570 };
1571 Ok((
1572 repo,
1573 InitOutcome {
1574 reinitialized,
1575 initial_branch,
1576 },
1577 ))
1578 }
1579
1580 pub fn open(path: &Path, options: &RepositoryOptions) -> Result<Self> {
1590 if options.open_for_serving {
1591 return Self::open_for_serving_internal(path, options);
1592 }
1593 Self::open_internal(options, path)
1594 }
1595
1596 fn open_with_work_tree(
1597 options: &RepositoryOptions,
1598 git_dir: &Path,
1599 work_tree: Option<&Path>,
1600 ) -> Result<Self> {
1601 let mut opts = options.clone();
1602 opts.work_tree = work_tree.map(|p| p.to_path_buf());
1603 Self::open_internal(&opts, git_dir)
1604 }
1605
1606 fn open_internal(options: &RepositoryOptions, git_dir: &Path) -> Result<Self> {
1607 Self::open_internal_with_refs(options, git_dir, None)
1608 }
1609
1610 fn open_internal_with_refs(
1611 options: &RepositoryOptions,
1612 git_dir: &Path,
1613 injected_refs: Option<Arc<dyn crate::refs::store::RefStore>>,
1614 ) -> Result<Self> {
1615 crate::trace::trace_span!("grit.repository.open");
1616 let work_tree = options.work_tree.as_deref();
1617 let git_dir = git_dir
1618 .canonicalize_simplified()
1619 .map_err(|_| not_a_repository_at(git_dir))?;
1620
1621 let repo = Self::from_canonical_git_dir(options, git_dir, work_tree, injected_refs)?;
1622 repo.refresh_config_session()?;
1623 if options.validate_repository_format {
1624 repo.validate_repository_format_cached()?;
1625 }
1626 repo.install_config_snapshot();
1627 warn_core_bare_worktree_conflict(options, &repo);
1628 let _ = trace_repo_setup_if_requested(&repo);
1629 Ok(repo)
1630 }
1631
1632 fn open_for_serving_internal(path: &Path, options: &RepositoryOptions) -> Result<Self> {
1633 let mut with_git_suffix = path.as_os_str().to_owned();
1634 with_git_suffix.push(".git");
1635 let candidates = [
1636 (path.join(".git"), Some(path.to_path_buf())),
1637 (path.to_path_buf(), None),
1638 (PathBuf::from(with_git_suffix), None),
1639 ];
1640 for (git_dir, work_tree) in candidates {
1641 if !git_dir.join("HEAD").is_file() {
1642 continue;
1643 }
1644 let mut opts = options.clone();
1645 opts.work_tree = work_tree;
1646 opts.open_for_serving = false;
1647 return Self::open_internal(&opts, &git_dir);
1648 }
1649 Err(not_a_repository_at(path))
1650 }
1651
1652 fn open_with_odb_internal(
1653 options: &RepositoryOptions,
1654 git_dir: &Path,
1655 mut builder: crate::odb::OdbBuilder,
1656 injected_refs: Option<Arc<dyn crate::refs::store::RefStore>>,
1657 ) -> Result<Self> {
1658 let work_tree = options.work_tree.as_deref();
1659 let environment = Arc::new(options.environment.clone());
1660 let cwd = environment.discovery_cwd();
1661 let command_runner = Arc::clone(&options.command_runner);
1662 let change_id_generator = Arc::clone(&options.change_id_generator);
1663
1664 let fully_injected = injected_refs.is_some() && builder.has_custom_primary();
1667 let requested_git_dir = cwd.join(git_dir);
1668 let git_dir = match requested_git_dir.canonicalize_simplified() {
1669 Ok(dir) => dir,
1670 Err(_) if fully_injected => canonicalize_existing_prefix(&requested_git_dir)?,
1671 Err(_) => return Err(not_a_repository_at(git_dir)),
1672 };
1673
1674 if injected_refs.is_none() && !head_file_present(&git_dir) {
1675 return Err(not_a_repository_at(&git_dir));
1676 }
1677
1678 let common_git_dir = resolve_common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
1679 let objects_dir = resolve_objects_dir(&environment, &git_dir, &common_git_dir)
1680 .ok_or_else(|| not_a_repository_at(&git_dir))?;
1681
1682 if !builder.has_custom_primary() && !objects_dir.exists() {
1684 return Err(not_a_repository_at(&git_dir));
1685 }
1686 let objects_dir = canonicalize_existing_prefix(&objects_dir)?;
1687 let builder_objects = canonicalize_existing_prefix(&cwd.join(builder.objects_dir()))?;
1688 if builder_objects != objects_dir {
1689 return Err(Error::OdbObjectsDirMismatch {
1690 builder: builder_objects,
1691 repository: objects_dir,
1692 });
1693 }
1694 builder.set_objects_dir(objects_dir);
1695 let injected_algos: Vec<HashAlgo> = builder.injected_hash_algos().collect();
1696
1697 let work_tree = match work_tree {
1698 Some(p) => {
1699 let mut resolved = cwd.join(p);
1700 if resolved.exists() {
1701 resolved = resolved
1702 .canonicalize_simplified()
1703 .map_err(|_| Error::PathError(p.display().to_string()))?;
1704 }
1705 Some(resolved)
1706 }
1707 None => None,
1708 };
1709
1710 let caches = RepoCaches::with_diagnostics(
1711 Arc::clone(&command_runner),
1712 Arc::clone(&options.diagnostics),
1713 );
1714 let odb = builder
1715 .build()
1716 .with_config_git_dir(git_dir.clone())
1717 .with_shared_config_state(caches.clone(), environment.clone())
1718 .with_resolved_work_tree(work_tree.clone());
1719
1720 let git_prefix = work_tree
1721 .as_ref()
1722 .and_then(|wt| compute_git_prefix(environment.as_ref(), wt));
1723
1724 let ref_store = open_or_install_ref_store(&caches, &git_dir, &environment, injected_refs)?;
1725 caches.configure_filter_subprocess(environment.as_ref(), &git_dir, work_tree.as_deref());
1726
1727 let repo = Self {
1728 git_dir,
1729 common_git_dir,
1730 work_tree,
1731 odb,
1732 explicit_git_dir: false,
1733 discovery_root: None,
1734 work_tree_from_env: false,
1735 discovery_via_gitfile: false,
1736 config_session: Mutex::new(None),
1737 caches,
1738 ref_store,
1739 environment,
1740 git_prefix,
1741 command_runner,
1742 change_id_generator,
1743 diagnostics: Arc::clone(&options.diagnostics),
1744 network_trace: options.network_trace,
1745 reference_unix_time: options.reference_unix_time,
1746 test_assume_different_owner: options.test_assume_different_owner,
1747 thread_policy: options.thread_policy,
1748 hook_stdio: options.hook_stdio,
1749 };
1750 repo.refresh_config_session()?;
1751 if options.validate_repository_format {
1752 repo.validate_repository_format_cached()?;
1753 }
1754 repo.check_injected_object_format(&injected_algos)?;
1755 warn_core_bare_worktree_conflict(options, &repo);
1756 let _ = trace_repo_setup_if_requested(&repo);
1757 Ok(repo)
1758 }
1759
1760 fn check_injected_object_format(&self, injected: &[HashAlgo]) -> Result<()> {
1767 if injected.is_empty() {
1768 return Ok(());
1769 }
1770 let repository = self
1771 .ensure_config_arc_fast()?
1772 .get("extensions.objectformat")
1773 .and_then(|name| HashAlgo::from_name(&name))
1774 .unwrap_or(HashAlgo::Sha1);
1775 match injected.iter().find(|algo| **algo != repository) {
1776 Some(&store) => Err(Error::ObjectFormatMismatch { repository, store }),
1777 None => Ok(()),
1778 }
1779 }
1780
1781 pub fn discover(start: Option<&Path>, options: &RepositoryOptions) -> Result<Self> {
1783 let env = &options.environment;
1784 let cwd = env.discovery_cwd();
1785
1786 if let Some(dir) = env.git_dir.as_deref() {
1788 let mut git_dir = PathBuf::from(dir);
1789 if git_dir.is_relative() {
1790 git_dir = cwd.join(git_dir);
1791 }
1792 git_dir = resolve_git_dir_env_path(&git_dir)?;
1794 let work_tree = env.git_work_tree.as_deref().map(|wt| {
1795 let p = PathBuf::from(wt);
1796 if p.is_absolute() {
1797 p
1798 } else {
1799 cwd.join(p)
1800 }
1801 });
1802 if let Some(ref wt_path) = work_tree {
1803 if env
1804 .git_work_tree
1805 .as_deref()
1806 .is_some_and(|raw| Path::new(raw).is_absolute())
1807 {
1808 validate_git_work_tree_path(wt_path)?;
1809 }
1810 }
1811 if work_tree.is_some() {
1812 let mut repo = Self::open_with_work_tree(options, &git_dir, work_tree.as_deref())?;
1813 repo.explicit_git_dir = true;
1814 repo.discovery_root = None;
1815 repo.work_tree_from_env = false;
1816 repo.discovery_via_gitfile = false;
1817 return Ok(repo);
1818 }
1819 let (is_bare, core_wt) = read_core_bare_and_worktree(&git_dir)?;
1821 let bare_worktree_conflict = is_bare && core_wt.is_some();
1822 let resolved_wt = if is_bare {
1823 None
1824 } else if let Some(raw) = core_wt {
1825 Some(resolve_core_worktree_path(&git_dir, &raw)?)
1826 } else {
1827 Some(
1833 cwd.canonicalize_simplified()
1834 .unwrap_or_else(|_| cwd.clone()),
1835 )
1836 };
1837 let mut repo = Self::open_with_work_tree(options, &git_dir, resolved_wt.as_deref())?;
1838 repo.explicit_git_dir = true;
1839 repo.discovery_root = None;
1840 repo.work_tree_from_env = false;
1841 repo.discovery_via_gitfile = false;
1842 let _ = bare_worktree_conflict;
1843 return Ok(repo);
1844 }
1845
1846 let env_work_tree = env.git_work_tree.as_deref().map(|wt| {
1849 let p = PathBuf::from(wt);
1850 if p.is_absolute() {
1851 p
1852 } else {
1853 cwd.join(p)
1854 }
1855 });
1856 if let Some(ref p) = env_work_tree {
1857 if env
1858 .git_work_tree
1859 .as_deref()
1860 .is_some_and(|raw| Path::new(raw).is_absolute())
1861 {
1862 validate_git_work_tree_path(p)?;
1863 }
1864 }
1865 let start = start.unwrap_or(&cwd);
1866 let start = if start.is_absolute() {
1867 start.to_path_buf()
1868 } else {
1869 cwd.join(start)
1870 };
1871
1872 let (ceiling_paths, no_resolve_ceilings) = parse_ceiling_directories(env);
1876 let ceiling_dirs: Vec<String> = ceiling_paths
1877 .into_iter()
1878 .map(|p| path_for_ceiling_compare(&p))
1879 .collect();
1880
1881 let start_canon = start
1882 .canonicalize_simplified()
1883 .unwrap_or_else(|_| start.clone());
1884 let ceil_cmp_buf = if no_resolve_ceilings {
1886 path_for_ceiling_compare(&start)
1887 } else {
1888 path_for_ceiling_compare(&start_canon)
1889 };
1890 let mut dir_buf = path_for_ceiling_compare(&start_canon);
1891 let min_offset = offset_1st_component(&dir_buf);
1892 let mut ceil_offset: isize = longest_ancestor_length(&ceil_cmp_buf, &ceiling_dirs)
1893 .map(|n| n as isize)
1894 .unwrap_or(-1);
1895 if ceiling_dirs.contains(&dir_buf) {
1896 ceil_offset = dir_buf.len() as isize;
1897 } else if ceil_offset < 0 {
1898 ceil_offset = min_offset as isize - 2;
1899 }
1900
1901 loop {
1902 let current = Path::new(&dir_buf);
1903 if let Some(DiscoveredAt { mut repo, gitfile }) = try_open_at(options, current)? {
1904 repo.environment = Arc::new(env.clone());
1910 repo.refresh_config_session()?;
1911 repo.validate_repository_format_cached()?;
1912 repo.install_config_snapshot();
1913 let cfg = repo.config()?;
1914 repo.set_diagnostics(Arc::clone(&options.diagnostics));
1915 if let Some(ref wt) = env_work_tree {
1916 repo.work_tree =
1917 Some(wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone()));
1918 repo.work_tree_from_env = true;
1919 } else {
1920 repo.work_tree_from_env = false;
1921 let linked_gitfile =
1926 repo.discovery_via_gitfile && resolve_common_dir(&repo.git_dir).is_some();
1927 if !linked_gitfile {
1928 let (is_bare, core_wt) =
1929 read_core_bare_and_worktree_from_config(cfg.as_ref());
1930 if is_bare {
1931 repo.work_tree = None;
1932 } else if let Some(raw) = core_wt {
1933 repo.work_tree = Some(resolve_core_worktree_path(&repo.git_dir, &raw)?);
1934 }
1935 }
1936 }
1937 let assume_different =
1938 options.test_assume_different_owner || env.test_assume_different_owner();
1939 if assume_different {
1940 repo.enforce_safe_directory()?;
1941 } else {
1942 ensure_valid_ownership(
1943 env,
1944 gitfile.as_deref(),
1945 repo.work_tree.as_deref(),
1946 &repo.git_dir,
1947 )?;
1948 }
1949 if let Some(wt) = repo.work_tree.as_ref() {
1950 repo.git_prefix = compute_git_prefix(env, wt);
1951 }
1952 let _ = trace_repo_setup_if_requested(&repo);
1953 return Ok(repo);
1954 }
1955
1956 let mut offset: isize = dir_buf.len() as isize;
1957 if offset <= min_offset as isize {
1958 break;
1959 }
1960 loop {
1961 offset -= 1;
1962 if offset <= ceil_offset {
1963 break;
1964 }
1965 if dir_buf
1966 .as_bytes()
1967 .get(offset as usize)
1968 .is_some_and(|b| *b == b'/')
1969 {
1970 break;
1971 }
1972 }
1973 if offset <= ceil_offset {
1974 break;
1975 }
1976 let off_u = offset as usize;
1977 let new_len = if off_u > min_offset {
1978 off_u
1979 } else {
1980 min_offset
1981 };
1982 dir_buf.truncate(new_len);
1983 }
1984
1985 Err(not_a_repository_at(&start))
1986 }
1987
1988 #[must_use]
1990 pub(crate) fn index_path(&self) -> PathBuf {
1991 self.git_dir.join("index")
1992 }
1993
1994 pub(crate) fn index_path_for_env(&self) -> Result<PathBuf> {
1998 if let Some(raw) = self.environment.git_index_file.as_deref() {
1999 if !raw.is_empty() {
2000 let p = PathBuf::from(raw);
2001 return Ok(if p.is_absolute() {
2002 p
2003 } else {
2004 self.environment.discovery_cwd().join(p)
2005 });
2006 }
2007 }
2008 Ok(self.index_path())
2009 }
2010
2011 pub(crate) fn load_index(&self) -> Result<Index> {
2015 let path = self.index_path_for_env()?;
2016 self.load_index_at(&path)
2017 }
2018
2019 pub(crate) fn load_index_at(&self, path: &std::path::Path) -> Result<Index> {
2022 self.load_index_at_with_context(
2023 path,
2024 crate::index::IndexParseContext::from_policy(self.thread_policy()),
2025 )
2026 }
2027
2028 pub(crate) fn load_index_at_with_context(
2029 &self,
2030 path: &std::path::Path,
2031 parse_ctx: crate::index::IndexParseContext,
2032 ) -> Result<Index> {
2033 let mut idx = self.load_index_at_preserving_sparse(path, parse_ctx)?;
2034 let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
2035 idx.expand_sparse_directory_placeholders(
2036 &self.odb,
2037 crate::path_validate::ProtectOptions::from_config(cfg.as_ref()),
2038 )?;
2039 self.clear_present_skip_worktree(&mut idx, cfg.as_ref());
2040 Ok(idx)
2041 }
2042
2043 pub(crate) fn load_index_preserving_sparse(&self) -> Result<Index> {
2045 self.load_index_at_preserving_sparse(
2046 &self.index_path_for_env()?,
2047 crate::index::IndexParseContext::from_policy(self.thread_policy()),
2048 )
2049 }
2050
2051 fn load_index_at_preserving_sparse(
2052 &self,
2053 path: &std::path::Path,
2054 parse_ctx: crate::index::IndexParseContext,
2055 ) -> Result<Index> {
2056 crate::trace::trace_span!("grit.index.read");
2057 let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
2058 if let Some(res) = cfg.as_ref().get_bool("index.sparse") {
2059 res.map_err(|s| Error::Config(s.into()))?;
2060 }
2061 let mut idx = Index::load_preserving_sparse_optional_with_context(
2062 path,
2063 &self.odb,
2064 Some(cfg.as_ref()),
2065 parse_ctx,
2066 )?;
2067 crate::split_index::resolve_split_index_if_needed(&mut idx, &self.git_dir, path)?;
2068 if idx.source_mtime.is_none() {
2069 idx.source_mtime = crate::index::index_file_mtime(path);
2070 }
2071 Ok(idx)
2072 }
2073
2074 pub(crate) fn clear_present_skip_worktree(&self, index: &mut Index, cfg: &ConfigSet) {
2075 if let Some(ref wt) = self.work_tree {
2076 crate::sparse_checkout::clear_skip_worktree_from_present_files(
2077 &self.git_dir,
2078 wt,
2079 index,
2080 Some(cfg),
2081 );
2082 }
2083 }
2084
2085 pub(crate) fn write_index(&self, index: &mut Index) -> Result<()> {
2088 self.write_index_with_post_index_change(index, false, false)
2089 }
2090
2091 pub(crate) fn begin_index_update(&self) -> Result<crate::index::IndexLock> {
2100 test_inject_index_write_fail()?;
2101 let path = self.index_path();
2102 let cfg = self.config()?;
2103 crate::index::IndexLock::acquire(&path, Some(cfg.as_ref()))
2104 }
2105
2106 pub(crate) fn commit_index_update(
2112 &self,
2113 lock: &mut crate::index::IndexLock,
2114 index: &mut Index,
2115 updated_workdir: bool,
2116 ) -> Result<()> {
2117 self.flush_index_update(lock, index)?;
2118 self.finish_index_update(lock, updated_workdir)
2119 }
2120
2121 pub(crate) fn flush_index_update(
2123 &self,
2124 lock: &mut crate::index::IndexLock,
2125 index: &mut Index,
2126 ) -> Result<()> {
2127 let path = self.index_path();
2128 let (skip_hash, cfg) = self.prepare_index_for_disk_write(&path, index)?;
2129 write_index_file_split(
2130 &path,
2131 &self.git_dir,
2132 index,
2133 cfg.as_ref(),
2134 WriteSplitIndexRequest::default(),
2135 skip_hash,
2136 self.thread_policy(),
2137 Some(lock),
2138 false,
2139 )
2140 }
2141
2142 pub(crate) fn finish_index_update(
2144 &self,
2145 lock: &mut crate::index::IndexLock,
2146 updated_workdir: bool,
2147 ) -> Result<()> {
2148 lock.commit_replace_index()?;
2149 let updated_workdir_arg = if updated_workdir { "1" } else { "0" };
2150 let _ = run_hook(self, "post-index-change", &[updated_workdir_arg, "0"], None);
2151 Ok(())
2152 }
2153
2154 pub fn write_largest_pack_bitmap(
2161 &self,
2162 options: &crate::pack_bitmap::PackBitmapWriteOptions,
2163 now: std::time::SystemTime,
2164 ) -> std::result::Result<std::path::PathBuf, crate::pack_bitmap::PackBitmapWriteError> {
2165 let path = self
2166 .odb
2167 .largest_pack_index_path()
2168 .map_err(|e| crate::pack_bitmap::PackBitmapWriteError::Object(e.to_string()))?
2169 .ok_or(crate::pack_bitmap::PackBitmapWriteError::InvalidPackPath(
2170 "no pack index in object store".to_owned(),
2171 ))?;
2172 crate::pack_bitmap::PackBitmapWriter::write(self, &path, options, now)
2173 }
2174
2175 pub(crate) fn try_write_index(&self, index: &mut Index) -> Result<bool> {
2188 self.try_write_index_at(&self.index_path(), index)
2189 }
2190
2191 pub(crate) fn try_write_index_at(&self, path: &Path, index: &mut Index) -> Result<bool> {
2194 match self.write_index_at(path, index) {
2195 Ok(()) => Ok(true),
2196 Err(e) if Self::opportunistic_index_write_skippable(&e) => Ok(false),
2197 Err(e) => Err(e),
2198 }
2199 }
2200
2201 fn opportunistic_index_write_skippable(err: &crate::error::Error) -> bool {
2202 use crate::error::Error;
2203 match err {
2204 Error::LockHeld { .. } => true,
2205 Error::Io(e) => matches!(
2206 e.kind(),
2207 std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::ReadOnlyFilesystem
2208 ),
2209 _ => false,
2210 }
2211 }
2212
2213 pub(crate) fn write_index_with_post_index_change(
2224 &self,
2225 index: &mut Index,
2226 updated_workdir: bool,
2227 updated_skipworktree: bool,
2228 ) -> Result<()> {
2229 self.write_index_at_with_post_index_change(
2230 &self.index_path(),
2231 index,
2232 updated_workdir,
2233 updated_skipworktree,
2234 )
2235 }
2236
2237 pub(crate) fn write_index_at(&self, path: &std::path::Path, index: &mut Index) -> Result<()> {
2239 self.write_index_at_with_post_index_change(path, index, false, false)
2240 }
2241
2242 pub(crate) fn write_index_at_with_split(
2244 &self,
2245 path: &std::path::Path,
2246 index: &mut Index,
2247 split: WriteSplitIndexRequest,
2248 ) -> Result<()> {
2249 self.write_index_at_split(path, index, split)
2250 }
2251
2252 pub(crate) fn write_index_at_with_post_index_change(
2267 &self,
2268 path: &std::path::Path,
2269 index: &mut Index,
2270 updated_workdir: bool,
2271 updated_skipworktree: bool,
2272 ) -> Result<()> {
2273 self.write_index_at_split_with_post_index_change(
2274 path,
2275 index,
2276 WriteSplitIndexRequest::default(),
2277 updated_workdir,
2278 updated_skipworktree,
2279 )
2280 }
2281
2282 pub(crate) fn write_index_at_split(
2284 &self,
2285 path: &std::path::Path,
2286 index: &mut Index,
2287 split: WriteSplitIndexRequest,
2288 ) -> Result<()> {
2289 self.write_index_at_split_with_post_index_change(path, index, split, false, false)
2290 }
2291
2292 pub(crate) fn write_index_at_split_with_post_index_change(
2305 &self,
2306 path: &std::path::Path,
2307 index: &mut Index,
2308 split: WriteSplitIndexRequest,
2309 updated_workdir: bool,
2310 updated_skipworktree: bool,
2311 ) -> Result<()> {
2312 self.write_index_file(
2313 path,
2314 index,
2315 split,
2316 updated_workdir,
2317 updated_skipworktree,
2318 None,
2319 )
2320 }
2321
2322 pub(crate) fn write_index_with_lock(
2334 &self,
2335 lock: &mut crate::index::IndexLock,
2336 index: &mut Index,
2337 updated_workdir: bool,
2338 ) -> Result<()> {
2339 let path = lock.index_path.clone();
2340 self.write_index_file(
2341 &path,
2342 index,
2343 WriteSplitIndexRequest::default(),
2344 updated_workdir,
2345 false,
2346 Some(lock),
2347 )
2348 }
2349
2350 pub(crate) fn lock_index(&self) -> Result<crate::index::IndexLock> {
2362 let path = self.index_path_for_env()?;
2363 let cfg = self.config()?;
2364 crate::index::IndexLock::acquire(&path, Some(cfg.as_ref()))
2365 }
2366
2367 pub(crate) fn lock_and_load_index(&self) -> Result<(crate::index::IndexLock, Index)> {
2382 let lock = self.lock_index()?;
2383 let index = self.load_index_at(&lock.index_path)?;
2384 Ok((lock, index))
2385 }
2386
2387 fn prepare_index_for_disk_write(
2389 &self,
2390 path: &std::path::Path,
2391 index: &mut Index,
2392 ) -> Result<(bool, Arc<ConfigSet>)> {
2393 test_inject_index_write_fail()?;
2394 index.hash_algo = self.odb.hash_algo();
2399 self.finalize_sparse_index_if_needed(index)?;
2400 let prev_index_mtime = crate::index::index_file_mtime(path);
2401 let cfg = self.config()?;
2402 if let Some(work_tree) = self.work_tree.as_deref() {
2403 if !index.suppress_pre_write_index_housekeeping {
2404 crate::untracked_cache::ensure_untracked_cache_shell(
2405 index,
2406 work_tree,
2407 cfg.as_ref(),
2408 );
2409 if !index.skip_racy_smudge_on_write {
2410 crate::diff::smudge_racily_clean_entries(
2411 &self.odb,
2412 &self.git_dir,
2413 index,
2414 work_tree,
2415 prev_index_mtime,
2416 Some(cfg.as_ref()),
2417 Some(self.caches().filters()),
2418 );
2419 }
2420 }
2421 }
2422 index.content_verified.clear();
2423 let skip_hash = crate::index::index_skip_hash_for_write(Some(cfg.as_ref()));
2424 Ok((skip_hash, cfg))
2425 }
2426
2427 fn write_index_file(
2428 &self,
2429 path: &std::path::Path,
2430 index: &mut Index,
2431 split: WriteSplitIndexRequest,
2432 updated_workdir: bool,
2433 updated_skipworktree: bool,
2434 existing_lock: Option<&mut crate::index::IndexLock>,
2435 ) -> Result<()> {
2436 crate::trace::trace_span!("grit.index.write");
2437 let (skip_hash, cfg) = self.prepare_index_for_disk_write(path, index)?;
2438 write_index_file_split(
2439 path,
2440 &self.git_dir,
2441 index,
2442 cfg.as_ref(),
2443 split,
2444 skip_hash,
2445 self.thread_policy(),
2446 existing_lock,
2447 true,
2448 )
2449 .map_err(|err| map_index_write_error(&self.git_dir, path, err))?;
2450 let updated_workdir_arg = if updated_workdir { "1" } else { "0" };
2452 let updated_skipworktree_arg = if updated_skipworktree { "1" } else { "0" };
2453 let _ = run_hook(
2454 self,
2455 "post-index-change",
2456 &[updated_workdir_arg, updated_skipworktree_arg],
2457 None,
2458 );
2459 Ok(())
2460 }
2461
2462 fn finalize_sparse_index_if_needed(&self, index: &mut Index) -> Result<()> {
2463 let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
2464 let sparse_enabled = cfg
2465 .get("core.sparseCheckout")
2466 .map(|v| v == "true")
2467 .unwrap_or(false);
2468 if !sparse_enabled {
2469 index.sparse_directories = false;
2470 return Ok(());
2471 }
2472 let cone_cfg = cfg
2473 .get("core.sparseCheckoutCone")
2474 .and_then(|v| v.parse::<bool>().ok())
2475 .unwrap_or(true);
2476 let sparse_ix = cfg
2477 .get("index.sparse")
2478 .map(|v| v == "true")
2479 .unwrap_or(false);
2480 let patterns = read_sparse_checkout_patterns(&self.git_dir);
2481 let cone = effective_cone_mode_for_sparse_file(cone_cfg, &patterns);
2482 let tree_oid = match index.cache_tree_root {
2485 Some(tree) => Some(tree),
2486 None => match resolve_head(&self.git_dir)?.oid() {
2487 Some(oid) => Some(parse_commit(&self.odb.read(oid)?.data)?.tree),
2488 None => None,
2489 },
2490 };
2491 if let Some(t) = tree_oid {
2492 let protect = crate::path_validate::ProtectOptions::from_config(cfg.as_ref());
2493 index.try_collapse_sparse_directories(
2494 &self.odb, &t, &patterns, cone, sparse_ix, protect,
2495 )?;
2496 } else {
2497 index.sparse_directories = false;
2498 }
2499 Ok(())
2500 }
2501
2502 #[must_use]
2507 pub(crate) fn bloom_pathspec_cwd(&self) -> Option<String> {
2508 let wt = self.work_tree.as_ref()?;
2509 let cwd = self.environment.discovery_cwd();
2510 let wt = wt.canonicalize_simplified().ok()?;
2511 let cwd = cwd.canonicalize_simplified().ok()?;
2512 let rel = cwd.strip_prefix(&wt).ok()?;
2513 let s = rel.to_string_lossy().replace('\\', "/");
2514 let s = s.trim_start_matches('/').to_string();
2515 Some(s)
2516 }
2517
2518 #[must_use]
2520 pub fn is_bare(&self) -> bool {
2521 if let Ok(cfg) = self.config() {
2522 if let Some(Ok(bare)) = cfg.get_bool("core.bare") {
2523 return bare;
2524 }
2525 }
2526 self.work_tree.is_none()
2527 }
2528
2529 pub(crate) fn replace_ref_base_if_enabled(&self) -> Option<String> {
2533 if self.environment.git_no_replace_objects {
2534 return None;
2535 }
2536 let settings = self.cached_settings();
2537 settings
2538 .use_replace_refs
2539 .then_some(settings.replace_ref_base)
2540 }
2541
2542 pub(crate) fn object_id_after_replace(
2544 &self,
2545 oid: &crate::objects::ObjectId,
2546 ) -> crate::objects::ObjectId {
2547 self.resolve_replace_redirect(oid).unwrap_or(*oid)
2548 }
2549
2550 pub(crate) fn read_info_replaced(
2552 &self,
2553 oid: &crate::objects::ObjectId,
2554 ) -> Result<crate::objects::ObjectInfo> {
2555 let effective = self.object_id_after_replace(oid);
2556 self.odb.read_info(&effective)
2557 }
2558
2559 pub(crate) fn read_replaced(
2566 &self,
2567 oid: &crate::objects::ObjectId,
2568 ) -> Result<crate::objects::Object> {
2569 let effective = self.object_id_after_replace(oid);
2570 self.odb.read(&effective)
2571 }
2572
2573 fn resolve_replace_redirect(
2574 &self,
2575 oid: &crate::objects::ObjectId,
2576 ) -> Option<crate::objects::ObjectId> {
2577 if self.environment.git_no_replace_objects {
2578 return None;
2579 }
2580 if let Ok(guard) = self.config_session.lock() {
2581 if guard.as_ref().is_some_and(|s| s.replace_lookup_inactive) {
2582 return None;
2583 }
2584 }
2585 let settings = self.cached_settings();
2586 if !settings.use_replace_refs {
2587 return None;
2588 }
2589 let replace_ref =
2590 self.common_git_dir
2591 .join(format!("{}{}", settings.replace_ref_base, oid.to_hex()));
2592 if !replace_ref.is_file() {
2593 return None;
2594 }
2595 let content = std::fs::read_to_string(&replace_ref).ok()?;
2596 let hex = content.trim();
2597 let replacement_oid = hex.parse::<crate::objects::ObjectId>().ok()?;
2598 if self.odb.read_info(&replacement_oid).is_ok() {
2599 Some(replacement_oid)
2600 } else {
2601 None
2602 }
2603 }
2604}
2605
2606pub(crate) fn trace_repo_setup_if_requested(repo: &Repository) -> std::io::Result<()> {
2611 let Some(path) = repo.environment.git_trace_setup.as_deref() else {
2612 return Ok(());
2613 };
2614 if path.is_empty() || path == "0" {
2615 return Ok(());
2616 }
2617 let trace_path = Path::new(path);
2618 if !trace_path.is_absolute() {
2619 return Ok(());
2620 }
2621
2622 let actual_cwd = repo.environment.discovery_cwd();
2623 let actual_cwd = actual_cwd
2624 .canonicalize_simplified()
2625 .unwrap_or_else(|_| actual_cwd.clone());
2626
2627 let (trace_cwd, prefix) = if let Some(ref wt) = repo.work_tree {
2630 let wt_canon = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
2631 if actual_cwd.starts_with(&wt_canon) {
2632 let rel = actual_cwd
2633 .strip_prefix(&wt_canon)
2634 .map(|p| p.to_path_buf())
2635 .unwrap_or_default();
2636 let prefix = if rel.as_os_str().is_empty() {
2637 "(null)".to_owned()
2638 } else {
2639 let mut s = rel.to_string_lossy().replace('\\', "/");
2640 if !s.ends_with('/') {
2641 s.push('/');
2642 }
2643 s
2644 };
2645 (wt_canon, prefix)
2646 } else {
2647 (actual_cwd.clone(), "(null)".to_owned())
2648 }
2649 } else {
2650 (actual_cwd.clone(), "(null)".to_owned())
2651 };
2652
2653 let git_dir_display =
2654 display_git_dir_for_setup_trace(repo, &trace_cwd, &actual_cwd, prefix.as_str());
2655 let common_display = display_common_dir_for_setup_trace(
2656 repo,
2657 &trace_cwd,
2658 &actual_cwd,
2659 prefix.as_str(),
2660 &git_dir_display,
2661 );
2662 let worktree_display = repo
2663 .work_tree
2664 .as_ref()
2665 .map(|p| {
2666 p.canonicalize_simplified()
2667 .unwrap_or_else(|_| lexical_normalize_path(p))
2668 .display()
2669 .to_string()
2670 })
2671 .unwrap_or_else(|| "(null)".to_owned());
2672
2673 let mut f = OpenOptions::new()
2674 .create(true)
2675 .append(true)
2676 .open(trace_path)?;
2677 writeln!(f, "setup: git_dir: {git_dir_display}")?;
2678 writeln!(f, "setup: git_common_dir: {common_display}")?;
2679 writeln!(f, "setup: worktree: {worktree_display}")?;
2680 writeln!(f, "setup: cwd: {}", trace_cwd.display())?;
2681 writeln!(f, "setup: prefix: {prefix}")?;
2682 Ok(())
2683}
2684
2685fn lexical_normalize_path(path: &Path) -> PathBuf {
2687 let mut out = PathBuf::new();
2688 let mut absolute = false;
2689 for c in path.components() {
2690 match c {
2691 Component::Prefix(p) => {
2692 out.push(p.as_os_str());
2693 }
2694 Component::RootDir => {
2695 absolute = true;
2696 out.push(c.as_os_str());
2697 }
2698 Component::CurDir => {}
2699 Component::ParentDir => {
2700 if absolute {
2701 let _ = out.pop();
2702 } else if !out.pop() {
2703 out.push("..");
2704 }
2705 }
2706 Component::Normal(s) => out.push(s),
2707 }
2708 }
2709 if out.as_os_str().is_empty() {
2710 PathBuf::from(".")
2711 } else {
2712 out
2713 }
2714}
2715
2716fn path_relative_to(target: &Path, base: &Path) -> Option<PathBuf> {
2718 let t = target.canonicalize_simplified().ok()?;
2719 let b = base.canonicalize_simplified().ok()?;
2720 let tc: Vec<_> = t.components().collect();
2721 let bc: Vec<_> = b.components().collect();
2722 let mut i = 0usize;
2723 while i < tc.len() && i < bc.len() && tc[i] == bc[i] {
2724 i += 1;
2725 }
2726 let up = bc.len().saturating_sub(i);
2727 let mut out = PathBuf::new();
2728 for _ in 0..up {
2729 out.push("..");
2730 }
2731 for comp in &tc[i..] {
2732 out.push(comp.as_os_str());
2733 }
2734 Some(out)
2735}
2736
2737fn rel_path_for_setup_trace(target: &Path, trace_cwd: &Path) -> String {
2738 let t = target
2739 .canonicalize_simplified()
2740 .unwrap_or_else(|_| target.to_path_buf());
2741 let tc = trace_cwd
2742 .canonicalize_simplified()
2743 .unwrap_or_else(|_| trace_cwd.to_path_buf());
2744 if let Some(rel) = path_relative_to(&t, &tc) {
2745 let s = rel.to_string_lossy().replace('\\', "/");
2746 return if s.is_empty() || s == "." {
2747 ".".to_owned()
2748 } else {
2749 s
2750 };
2751 }
2752 t.display().to_string()
2753}
2754
2755fn trace_cwd_strictly_inside_git_parent(trace_cwd: &Path, git_dir: &Path) -> bool {
2756 let tc = trace_cwd
2757 .canonicalize_simplified()
2758 .unwrap_or_else(|_| trace_cwd.to_path_buf());
2759 let gd = git_dir
2760 .canonicalize_simplified()
2761 .unwrap_or_else(|_| git_dir.to_path_buf());
2762 let Some(parent) = gd.parent() else {
2763 return false;
2764 };
2765 let parent = parent.to_path_buf();
2766 if tc == parent {
2767 return false;
2768 }
2769 tc.starts_with(&parent) && tc != parent
2770}
2771
2772fn display_git_dir_for_setup_trace(
2773 repo: &Repository,
2774 trace_cwd: &Path,
2775 actual_cwd: &Path,
2776 setup_prefix: &str,
2777) -> String {
2778 let gd = repo
2779 .git_dir
2780 .canonicalize_simplified()
2781 .unwrap_or_else(|_| repo.git_dir.clone());
2782 let tc = trace_cwd
2783 .canonicalize_simplified()
2784 .unwrap_or_else(|_| trace_cwd.to_path_buf());
2785 let ac = actual_cwd
2786 .canonicalize_simplified()
2787 .unwrap_or_else(|_| actual_cwd.to_path_buf());
2788
2789 if repo.work_tree.is_none() && !repo.explicit_git_dir {
2792 if ac == gd {
2793 return ".".to_owned();
2794 }
2795 if ac.starts_with(&gd) && ac != gd {
2796 return gd.display().to_string();
2797 }
2798 }
2799
2800 if !repo.explicit_git_dir {
2802 if let Some(wt) = &repo.work_tree {
2803 let wt = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
2804 if ac.starts_with(&gd) && ac != wt {
2805 return gd.display().to_string();
2806 }
2807 }
2808 }
2809
2810 if repo.explicit_git_dir {
2814 if repo.work_tree.is_none() {
2815 if let Some(raw) = repo.environment.git_dir.as_deref() {
2816 let p = Path::new(raw.trim());
2817 if p.is_absolute() {
2818 return gd.display().to_string();
2819 }
2820 let joined = ac.join(p);
2821 if joined.is_file() {
2822 return gd.display().to_string();
2823 }
2824 if let Some(rel) = path_relative_to(&gd, &tc) {
2825 let s = rel.to_string_lossy().replace('\\', "/");
2826 return if s.is_empty() || s == "." {
2827 ".".to_owned()
2828 } else {
2829 s
2830 };
2831 }
2832 }
2833 return gd.display().to_string();
2834 }
2835 if let Some(wt) = &repo.work_tree {
2836 let wt = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
2837 let strictly_inside_wt = ac.starts_with(&wt) && ac != wt;
2838 if strictly_inside_wt {
2839 return gd.display().to_string();
2840 }
2841 if let Some(raw) = repo.environment.git_dir.as_deref() {
2842 let p = Path::new(raw.trim());
2843 if p.is_relative() {
2844 let joined = ac.join(p);
2845 if joined.is_file() {
2846 return gd.display().to_string();
2848 }
2849 if let Some(rel) = path_relative_to(&gd, &tc) {
2850 let s = rel.to_string_lossy().replace('\\', "/");
2851 return if s.is_empty() || s == "." {
2852 ".".to_owned()
2853 } else {
2854 s
2855 };
2856 }
2857 }
2858 return gd.display().to_string();
2859 }
2860 }
2861 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
2862 return rel_path_for_setup_trace(&gd, trace_cwd);
2863 }
2864 return gd.display().to_string();
2865 }
2866
2867 let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
2868 (Some(root), Some(wt)) if !repo.work_tree_from_env => {
2869 let r = root
2870 .canonicalize_simplified()
2871 .unwrap_or_else(|_| root.clone());
2872 let w = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
2873 r != w
2874 }
2875 _ => false,
2876 };
2877
2878 if repo.work_tree_from_env {
2879 if !repo.discovery_via_gitfile {
2880 if setup_prefix == "(null)" {
2881 if let (Some(root), Some(wt)) = (&repo.discovery_root, &repo.work_tree) {
2882 let r = root
2883 .canonicalize_simplified()
2884 .unwrap_or_else(|_| root.clone());
2885 let w = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
2886 if r == w {
2887 let dot_git = r.join(".git");
2888 let dot_git = dot_git.canonicalize_simplified().unwrap_or(dot_git);
2889 if gd == dot_git {
2890 return ".git".to_owned();
2891 }
2892 }
2893 }
2894 }
2895 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
2896 return rel_path_for_setup_trace(&gd, trace_cwd);
2897 }
2898 }
2899 return gd.display().to_string();
2900 }
2901
2902 if work_relocated {
2903 if let Some(wt) = &repo.work_tree {
2904 let wt = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
2905 if ac == wt {
2906 return gd.display().to_string();
2907 }
2908 let inside_wt = ac.starts_with(&wt) && ac != wt;
2909 if inside_wt {
2910 if let Some(rel) = path_relative_to(&gd, &ac) {
2911 let s = rel.to_string_lossy().replace('\\', "/");
2912 return if s.is_empty() || s == "." {
2913 ".".to_owned()
2914 } else {
2915 s
2916 };
2917 }
2918 }
2919 }
2920 }
2921 if repo.work_tree.is_some() {
2922 if let Some(root) = &repo.discovery_root {
2923 let r = root
2924 .canonicalize_simplified()
2925 .unwrap_or_else(|_| root.clone());
2926 let dot_git = r.join(".git");
2927 let dot_git = dot_git.canonicalize_simplified().unwrap_or(dot_git);
2928 if gd == dot_git {
2929 return ".git".to_owned();
2930 }
2931 } else if let Some(wt) = &repo.work_tree {
2932 let wt = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
2933 let dot_git = wt.join(".git");
2934 let dot_git = dot_git.canonicalize_simplified().unwrap_or(dot_git);
2935 if gd == dot_git {
2936 return ".git".to_owned();
2937 }
2938 }
2939 }
2940
2941 if repo.discovery_via_gitfile && !repo.explicit_git_dir {
2942 return gd.display().to_string();
2943 }
2944
2945 if repo.work_tree.is_none() && !repo.explicit_git_dir {
2949 if let Some(gp) = gd.parent() {
2950 let gp = gp
2951 .canonicalize_simplified()
2952 .unwrap_or_else(|_| gp.to_path_buf());
2953 let gdc = gd.canonicalize_simplified().unwrap_or_else(|_| gd.clone());
2954 if tc.starts_with(&gp) && tc != gp && !tc.starts_with(&gdc) {
2955 return gdc.display().to_string();
2956 }
2957 if tc == gp {
2958 return rel_path_for_setup_trace(&gd, trace_cwd);
2959 }
2960 }
2961 }
2962
2963 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
2964 rel_path_for_setup_trace(&gd, trace_cwd)
2965 } else {
2966 gd.display().to_string()
2967 }
2968}
2969
2970fn display_common_dir_for_setup_trace(
2971 repo: &Repository,
2972 trace_cwd: &Path,
2973 actual_cwd: &Path,
2974 _setup_prefix: &str,
2975 git_dir_display: &str,
2976) -> String {
2977 let gd = repo
2978 .git_dir
2979 .canonicalize_simplified()
2980 .unwrap_or_else(|_| repo.git_dir.clone());
2981 let Some(common) = resolve_common_dir(&gd) else {
2982 return git_dir_display.to_owned();
2983 };
2984 let common = common.canonicalize_simplified().unwrap_or(common);
2985 if common == gd {
2986 return git_dir_display.to_owned();
2987 }
2988
2989 let ac = actual_cwd
2990 .canonicalize_simplified()
2991 .unwrap_or_else(|_| actual_cwd.to_path_buf());
2992 if repo.work_tree.is_none() && !repo.explicit_git_dir {
2993 if ac == common {
2994 return ".".to_owned();
2995 }
2996 if ac.starts_with(&common) && ac != common {
2997 return common.display().to_string();
2998 }
2999 }
3000
3001 let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
3002 (Some(root), Some(wt)) if !repo.work_tree_from_env => {
3003 let r = root
3004 .canonicalize_simplified()
3005 .unwrap_or_else(|_| root.clone());
3006 let w = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3007 r != w
3008 }
3009 _ => false,
3010 };
3011 if work_relocated {
3012 if let Some(wt) = &repo.work_tree {
3013 let wt = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3014 if ac == wt {
3015 return common.display().to_string();
3016 }
3017 let inside_wt = ac.starts_with(&wt) && ac != wt;
3018 if inside_wt {
3019 if let Some(rel) = path_relative_to(&common, &ac) {
3020 let s = rel.to_string_lossy().replace('\\', "/");
3021 return if s.is_empty() || s == "." {
3022 ".".to_owned()
3023 } else {
3024 s
3025 };
3026 }
3027 }
3028 }
3029 }
3030
3031 if repo.discovery_via_gitfile && !repo.explicit_git_dir {
3032 return common.display().to_string();
3033 }
3034
3035 if repo.work_tree.is_none() && !repo.explicit_git_dir {
3036 let tc = trace_cwd
3037 .canonicalize_simplified()
3038 .unwrap_or_else(|_| trace_cwd.to_path_buf());
3039 if let Some(cp) = common.parent() {
3040 let cp = cp
3041 .canonicalize_simplified()
3042 .unwrap_or_else(|_| cp.to_path_buf());
3043 let comc = common
3044 .canonicalize_simplified()
3045 .unwrap_or_else(|_| common.clone());
3046 if tc.starts_with(&cp) && tc != cp && !tc.starts_with(&comc) {
3047 return comc.display().to_string();
3048 }
3049 if tc == cp {
3050 return rel_path_for_setup_trace(&common, trace_cwd);
3051 }
3052 }
3053 }
3054
3055 if trace_cwd_strictly_inside_git_parent(trace_cwd, &common) {
3056 rel_path_for_setup_trace(&common, trace_cwd)
3057 } else {
3058 common.display().to_string()
3059 }
3060}
3061
3062fn replace_dir_is_non_empty(replace_dir: &Path) -> bool {
3064 let Ok(entries) = fs::read_dir(replace_dir) else {
3065 return false;
3066 };
3067 entries.filter_map(|e| e.ok()).any(|e| {
3068 let Ok(ft) = e.file_type() else {
3069 return false;
3070 };
3071 ft.is_file()
3072 })
3073}
3074
3075fn resolve_common_dir(git_dir: &Path) -> Option<PathBuf> {
3077 let commondir_file = git_dir.join("commondir");
3078 if !commondir_file.is_file() {
3079 return None;
3080 }
3081 let common_raw = fs::read_to_string(commondir_file).ok()?;
3082 let common_rel = common_raw.trim();
3083 if common_rel.is_empty() {
3084 return None;
3085 }
3086 let common_dir = if Path::new(common_rel).is_absolute() {
3087 PathBuf::from(common_rel)
3088 } else {
3089 git_dir.join(common_rel)
3090 };
3091 Some(common_dir.canonicalize_simplified().unwrap_or(common_dir))
3092}
3093
3094#[must_use]
3096pub fn common_git_dir_for_config(git_dir: &Path) -> PathBuf {
3097 resolve_common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf())
3098}
3099
3100pub fn worktree_config_enabled(common_dir: &Path) -> bool {
3102 let path = common_dir.join("config");
3103 let Ok(content) = crate::repo_caches::read_local_config_file(&path) else {
3104 return false;
3105 };
3106 worktree_config_enabled_for_text(Some(content.as_str()))
3107}
3108
3109pub fn worktree_config_enabled_for_text(content: Option<&str>) -> bool {
3111 let Some(content) = content else {
3112 return false;
3113 };
3114 let mut in_extensions = false;
3115 for raw_line in content.lines() {
3116 let mut line = raw_line.trim();
3117 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
3118 continue;
3119 }
3120 if line.starts_with('[') {
3121 let Some(end_idx) = line.find(']') else {
3122 continue;
3123 };
3124 let section = line[1..end_idx].trim();
3125 let section_name = section
3126 .split_whitespace()
3127 .next()
3128 .unwrap_or_default()
3129 .to_ascii_lowercase();
3130 in_extensions = section_name == "extensions";
3131 let remainder = line[end_idx + 1..].trim();
3132 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
3133 continue;
3134 }
3135 line = remainder;
3136 }
3137 if in_extensions {
3138 let Some((key, value)) = line.split_once('=') else {
3139 continue;
3140 };
3141 if key.trim().eq_ignore_ascii_case("worktreeconfig") {
3142 let v = value.trim();
3143 return v.eq_ignore_ascii_case("true")
3144 || v.eq_ignore_ascii_case("yes")
3145 || v.eq_ignore_ascii_case("on")
3146 || v == "1";
3147 }
3148 }
3149 }
3150 false
3151}
3152
3153fn open_or_create_config_file(path: &Path, scope: ConfigScope) -> Result<ConfigFile> {
3154 match ConfigFile::from_path(path, scope)? {
3155 Some(f) => Ok(f),
3156 None => {
3157 if let Some(parent) = path.parent() {
3158 fs::create_dir_all(parent).map_err(Error::Io)?;
3159 }
3160 ConfigFile::parse(path, "", scope)
3161 }
3162 }
3163}
3164
3165fn config_file_bool_true(cfg: &ConfigFile, key: &str) -> bool {
3166 cfg.get(key).is_some_and(|v| {
3167 matches!(
3168 v.trim().to_ascii_lowercase().as_str(),
3169 "true" | "yes" | "on" | "1"
3170 )
3171 })
3172}
3173
3174pub fn init_worktree_config(git_dir: &Path) -> Result<()> {
3184 let common_dir = common_git_dir_for_config(git_dir);
3185 let common_config_path = common_dir.join("config");
3186 let worktree_config_path = git_dir.join("config.worktree");
3187
3188 if worktree_config_enabled(&common_dir) {
3189 if !worktree_config_path.exists() {
3190 if let Some(parent) = worktree_config_path.parent() {
3191 fs::create_dir_all(parent).map_err(Error::Io)?;
3192 }
3193 fs::write(&worktree_config_path, "").map_err(Error::Io)?;
3194 }
3195 return Ok(());
3196 }
3197
3198 let mut common_cfg = open_or_create_config_file(&common_config_path, ConfigScope::Local)?;
3199 common_cfg.set("extensions.worktreeConfig", "true")?;
3200
3201 let mut wt_cfg = open_or_create_config_file(&worktree_config_path, ConfigScope::Worktree)?;
3202
3203 if config_file_bool_true(&common_cfg, "core.bare") {
3204 wt_cfg.set("core.bare", "true")?;
3205 common_cfg.unset("core.bare")?;
3206 }
3207 if let Some(worktree) = common_cfg.get("core.worktree") {
3208 wt_cfg.set("core.worktree", &worktree)?;
3209 common_cfg.unset("core.worktree")?;
3210 }
3211
3212 common_cfg.write()?;
3213 wt_cfg.write()?;
3214 Ok(())
3215}
3216
3217pub fn early_config_ignore_repo_reason(common_dir: &Path) -> Option<String> {
3221 let path = common_dir.join("config");
3222 let content = crate::repo_caches::read_local_config_file(&path).ok()?;
3223 early_config_ignore_repo_reason_for_text(Some(content.as_str()))
3224}
3225
3226pub fn early_config_ignore_repo_reason_for_text(content: Option<&str>) -> Option<String> {
3228 const GIT_REPO_VERSION_READ: u32 = 1;
3229 let content = content?;
3230 let mut version = 0u32;
3231 let mut in_core = false;
3232 for raw_line in content.lines() {
3233 let mut line = raw_line.trim();
3234 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
3235 continue;
3236 }
3237 if line.starts_with('[') {
3238 let Some(end_idx) = line.find(']') else {
3239 continue;
3240 };
3241 let section = line[1..end_idx].trim();
3242 let section_name = section
3243 .split_whitespace()
3244 .next()
3245 .unwrap_or_default()
3246 .to_ascii_lowercase();
3247 in_core = section_name == "core";
3248 let remainder = line[end_idx + 1..].trim();
3249 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
3250 continue;
3251 }
3252 line = remainder;
3253 }
3254 if in_core {
3255 if let Some((key, value)) = line.split_once('=') {
3256 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
3257 if let Ok(v) = value.trim().parse::<u32>() {
3258 version = v;
3259 }
3260 }
3261 }
3262 }
3263 }
3264 if version > GIT_REPO_VERSION_READ {
3265 Some(format!(
3266 "Expected git repo version <= {GIT_REPO_VERSION_READ}, found {version}"
3267 ))
3268 } else {
3269 None
3270 }
3271}
3272
3273fn path_for_ceiling_compare(path: &Path) -> String {
3274 let path = path.to_string_lossy();
3275 #[cfg(windows)]
3276 {
3277 path.replace('\\', "/")
3278 }
3279 #[cfg(not(windows))]
3280 {
3281 path.into_owned()
3282 }
3283}
3284
3285fn offset_1st_component(path: &str) -> usize {
3286 if path.starts_with('/') {
3287 1
3288 } else {
3289 0
3290 }
3291}
3292
3293fn longest_ancestor_length(path: &str, ceilings: &[String]) -> Option<usize> {
3295 if path == "/" {
3296 return None;
3297 }
3298 let mut max_len: Option<usize> = None;
3299 for ceil in ceilings {
3300 let mut len = ceil.len();
3301 while len > 0 && ceil.as_bytes().get(len - 1) == Some(&b'/') {
3302 len -= 1;
3303 }
3304 if len == 0 {
3305 continue;
3306 }
3307 if path.len() <= len + 1 {
3308 continue;
3309 }
3310 if !path.starts_with(&ceil[..len]) {
3311 continue;
3312 }
3313 if path.as_bytes().get(len) != Some(&b'/') {
3314 continue;
3315 }
3316 if path.as_bytes().get(len + 1).is_none() {
3317 continue;
3318 }
3319 max_len = Some(max_len.map_or(len, |m| m.max(len)));
3320 }
3321 max_len
3322}
3323
3324pub fn local_config_write_path(git_dir: &Path) -> PathBuf {
3329 if let Some(path) = repository_config_path(git_dir) {
3330 return path;
3331 }
3332 if let Some(common) = resolve_common_dir(git_dir) {
3333 return common.join("config");
3334 }
3335 git_dir.join("config")
3336}
3337
3338fn repository_config_path(git_dir: &Path) -> Option<PathBuf> {
3340 repository_config_path_for_caches(git_dir)
3341}
3342
3343pub(crate) fn repository_config_path_for_caches(git_dir: &Path) -> Option<PathBuf> {
3345 let local = git_dir.join("config");
3346 if local.is_file() {
3347 return Some(local);
3348 }
3349 let commondir = git_dir.join("commondir");
3350 if !commondir.is_file() {
3351 return None;
3352 }
3353 let common = resolve_common_dir(git_dir)?;
3354 let shared = common.join("config");
3355 if shared.exists() {
3356 Some(shared)
3357 } else {
3358 None
3359 }
3360}
3361
3362pub fn validate_repo_format(git_dir: &Path) -> Result<()> {
3368 validate_repository_format(git_dir)
3369}
3370
3371pub(crate) fn validate_repository_format_parsed(parsed: &RepositoryFormat) -> Result<()> {
3373 if parsed.repo_version > 1 {
3374 return Err(Error::UnsupportedRepositoryFormatVersion(
3375 parsed.repo_version,
3376 ));
3377 }
3378
3379 if let Some(raw) = parsed.ref_storage.as_deref() {
3380 crate::ref_storage::RefStorageFormat::validate_config_extension(raw)?;
3381 }
3382
3383 if let Some(msg) = parsed.format_error_message() {
3384 return Err(Error::Message(msg));
3385 }
3386
3387 Ok(())
3388}
3389
3390fn validate_repository_format(git_dir: &Path) -> Result<()> {
3391 let Some(config_path) = repository_config_path(git_dir) else {
3392 return Ok(());
3393 };
3394 let content = crate::repo_caches::read_local_config_file(&config_path)?;
3395 let parsed = parse_repository_format(&content, &config_path)?;
3396 validate_repository_format_parsed(&parsed)
3397}
3398
3399#[derive(Clone)]
3402pub(crate) struct RepositoryFormat {
3403 pub(crate) repo_version: u32,
3405 pub(crate) extensions: BTreeSet<String>,
3407 pub(crate) ref_storage: Option<String>,
3409 pub(crate) object_format: Option<String>,
3411}
3412
3413pub(crate) fn read_repository_format_from_git_dir(git_dir: &Path) -> Result<RepositoryFormat> {
3419 let Some(config_path) = repository_config_path(git_dir) else {
3420 return Ok(RepositoryFormat {
3421 repo_version: 0,
3422 extensions: BTreeSet::new(),
3423 ref_storage: None,
3424 object_format: None,
3425 });
3426 };
3427 let content = crate::repo_caches::read_local_config_file(&config_path)?;
3428 parse_repository_format(&content, &config_path)
3429}
3430
3431impl RepositoryFormat {
3432 pub(crate) fn hash_algo(&self) -> crate::objects::HashAlgo {
3435 self.object_format
3436 .as_deref()
3437 .and_then(crate::objects::HashAlgo::from_name)
3438 .unwrap_or(crate::objects::HashAlgo::Sha1)
3439 }
3440
3441 fn format_error_message(&self) -> Option<String> {
3448 let mut v1_only_found: Vec<&str> = Vec::new();
3455 let mut unknown_found: Vec<&str> = Vec::new();
3456 for extension in &self.extensions {
3457 match extension.as_str() {
3458 "noop" | "preciousobjects" | "partialclone" | "worktreeconfig" => {}
3460 "noop-v1"
3462 | "objectformat"
3463 | "compatobjectformat"
3464 | "refstorage"
3465 | "relativeworktrees"
3466 | "submodulepathconfig" => {
3467 if self.repo_version == 0 {
3468 v1_only_found.push(extension);
3469 }
3470 }
3471 _ => {
3473 if self.repo_version >= 1 {
3474 unknown_found.push(extension);
3475 }
3476 }
3477 }
3478 }
3479
3480 if !unknown_found.is_empty() {
3481 let mut msg = if unknown_found.len() == 1 {
3482 "unknown repository extension found:".to_owned()
3483 } else {
3484 "unknown repository extensions found:".to_owned()
3485 };
3486 for ext in &unknown_found {
3487 msg.push_str(&format!("\n\t{ext}"));
3488 }
3489 return Some(msg);
3490 }
3491
3492 if !v1_only_found.is_empty() {
3493 let mut msg = if v1_only_found.len() == 1 {
3494 "repo version is 0, but v1-only extension found:".to_owned()
3495 } else {
3496 "repo version is 0, but v1-only extensions found:".to_owned()
3497 };
3498 for ext in &v1_only_found {
3499 msg.push_str(&format!("\n\t{ext}"));
3500 }
3501 return Some(msg);
3502 }
3503
3504 None
3505 }
3506}
3507
3508pub(crate) fn parse_repository_format(
3515 content: &str,
3516 config_path: &Path,
3517) -> Result<RepositoryFormat> {
3518 let mut in_core = false;
3519 let mut in_extensions = false;
3520 let mut repo_version = 0u32;
3521 let mut extensions = BTreeSet::new();
3522 let mut ref_storage: Option<String> = None;
3523 let mut object_format: Option<String> = None;
3524
3525 for raw_line in content.lines() {
3526 let mut line = raw_line.trim();
3527 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
3528 continue;
3529 }
3530
3531 if line.starts_with('[') {
3532 let Some(end_idx) = line.find(']') else {
3533 return Err(Error::Config(
3534 format!("invalid config in {}", config_path.display()).into(),
3535 ));
3536 };
3537
3538 let section = line[1..end_idx].trim();
3539 let section_name = section
3540 .split_whitespace()
3541 .next()
3542 .unwrap_or_default()
3543 .to_ascii_lowercase();
3544 in_core = section_name == "core";
3545 in_extensions = section_name == "extensions";
3546
3547 let remainder = line[end_idx + 1..].trim();
3548 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
3549 continue;
3550 }
3551 line = remainder;
3552 }
3553
3554 if in_core {
3555 if let Some((key, value)) = line.split_once('=') {
3556 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
3557 if let Ok(v) = value.trim().parse::<u32>() {
3559 repo_version = v;
3560 }
3561 }
3562 }
3563 }
3564
3565 if in_extensions {
3566 let (key, value) = if let Some((key, value)) = line.split_once('=') {
3567 (key.trim(), Some(value.trim()))
3568 } else {
3569 (line, None)
3570 };
3571 if key.eq_ignore_ascii_case("refstorage") {
3572 ref_storage = value.map(str::to_owned);
3573 }
3574 if key.eq_ignore_ascii_case("objectformat") {
3575 object_format = value.map(str::to_owned);
3576 }
3577 if !key.is_empty() {
3578 extensions.insert(key.to_ascii_lowercase());
3579 }
3580 }
3581 }
3582
3583 Ok(RepositoryFormat {
3584 repo_version,
3585 extensions,
3586 ref_storage,
3587 object_format,
3588 })
3589}
3590
3591pub fn repository_format_warning(git_dir: &Path) -> Result<Option<String>> {
3608 const GIT_REPO_VERSION_READ: u32 = 1;
3609 let Some(config_path) = repository_config_path(git_dir) else {
3610 return Ok(None);
3611 };
3612 let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
3613 let parsed = parse_repository_format(&content, &config_path)?;
3614
3615 if parsed.repo_version > GIT_REPO_VERSION_READ {
3616 return Ok(Some(format!(
3617 "Expected git repo version <= {GIT_REPO_VERSION_READ}, found {}",
3618 parsed.repo_version
3619 )));
3620 }
3621
3622 Ok(parsed.format_error_message())
3623}
3624
3625struct DiscoveredAt {
3631 repo: Repository,
3632 gitfile: Option<PathBuf>,
3634}
3635
3636fn try_open_at(options: &RepositoryOptions, dir: &Path) -> Result<Option<DiscoveredAt>> {
3637 let env = &options.environment;
3638 let dot_git = dir.join(".git");
3639
3640 #[cfg(unix)]
3643 {
3644 use std::os::unix::fs::FileTypeExt;
3645 let is_special = |ft: fs::FileType| {
3646 ft.is_fifo() || ft.is_socket() || ft.is_block_device() || ft.is_char_device()
3647 };
3648 if let Ok(meta) = fs::symlink_metadata(&dot_git) {
3649 let ft = meta.file_type();
3650 let target_special = ft.is_symlink()
3651 && fs::metadata(&dot_git).is_ok_and(|target| is_special(target.file_type()));
3652 if is_special(ft) || target_special {
3653 return Err(Error::NotARepository(
3654 NotARepositoryReason::GitfileNotRegularFile { path: dot_git },
3655 ));
3656 }
3657 }
3658 }
3659
3660 if dot_git.is_file() {
3661 let git_dir = resolve_gitfile(&dot_git)?;
3663 let mut opts = options.clone();
3664 opts.validate_repository_format = false;
3665 opts.work_tree = Some(dir.to_path_buf());
3666 let mut repo = Repository::open_internal(&opts, &git_dir)?;
3667 if resolve_common_dir(&git_dir).is_some() {
3671 let cwd = env.discovery_cwd();
3672 if repo.work_tree.is_some() && !is_inside_work_tree(&repo, &cwd) {
3673 let root = if dir.is_absolute() {
3674 dir.to_path_buf()
3675 } else {
3676 cwd.join(dir)
3677 };
3678 repo.work_tree = Some(root.canonicalize_simplified().unwrap_or(root));
3679 }
3680 }
3681 let root = if dir.is_absolute() {
3682 dir.to_path_buf()
3683 } else {
3684 env.discovery_cwd().join(dir)
3685 };
3686 repo.discovery_root = Some(root.canonicalize_simplified().unwrap_or(root));
3687 repo.discovery_via_gitfile = true;
3688 return Ok(Some(DiscoveredAt {
3689 repo,
3690 gitfile: Some(dot_git.clone()),
3691 }));
3692 }
3693
3694 if dot_git.is_dir() {
3695 let open_path = if dot_git.is_symlink() {
3699 dot_git.read_link().unwrap_or_else(|_| dot_git.clone())
3701 } else {
3702 dot_git.clone()
3703 };
3704 let mut skip_opts = options.clone();
3707 skip_opts.validate_repository_format = false;
3708 skip_opts.work_tree = Some(dir.to_path_buf());
3709 match Repository::open_internal(&skip_opts, &open_path) {
3710 Ok(mut repo) => {
3711 if dot_git.is_symlink() {
3714 let abs_dot_git = if dot_git.is_absolute() {
3715 dot_git
3716 } else {
3717 dir.join(".git")
3718 };
3719 repo.git_dir = abs_dot_git;
3720 }
3721 let root = if dir.is_absolute() {
3722 dir.to_path_buf()
3723 } else {
3724 env.discovery_cwd().join(dir)
3725 };
3726 repo.discovery_root = Some(root.canonicalize_simplified().unwrap_or(root));
3727 repo.discovery_via_gitfile = false;
3728 return Ok(Some(DiscoveredAt {
3729 repo,
3730 gitfile: None,
3731 }));
3732 }
3733 Err(Error::NotARepository(_)) | Err(Error::Config(_)) => return Ok(None),
3734 Err(e) => return Err(e),
3735 }
3736 }
3737
3738 if dir.join("HEAD").is_file() && dir.join("commondir").is_file() {
3741 maybe_trace_implicit_bare_repository(dir, env);
3742 let repo = Repository::open_with_work_tree(options, dir, None)?;
3743 return Ok(Some(DiscoveredAt {
3744 repo,
3745 gitfile: None,
3746 }));
3747 }
3748
3749 if dir.join("objects").is_dir() && dir.join("HEAD").is_file() {
3751 maybe_trace_implicit_bare_repository(dir, env);
3752 if !is_inside_dot_git(dir) {
3756 if let Ok(cfg) = crate::config::ConfigSet::load(env, None, true) {
3757 if let Some(val) = cfg.get("safe.bareRepository") {
3758 if val.eq_ignore_ascii_case("explicit") {
3759 return Err(Error::ForbiddenBareRepository(path_for_error_message(dir)));
3760 }
3761 }
3762 }
3763 }
3764 let repo = Repository::open_with_work_tree(options, dir, None)?;
3765 return Ok(Some(DiscoveredAt {
3766 repo,
3767 gitfile: None,
3768 }));
3769 }
3770
3771 Ok(None)
3772}
3773
3774fn is_inside_dot_git(path: &Path) -> bool {
3775 path.components().any(|c| c.as_os_str() == ".git")
3776}
3777
3778fn maybe_trace_implicit_bare_repository(dir: &Path, environment: &Environment) {
3779 let Some(path) = environment
3780 .git_trace2_perf
3781 .as_deref()
3782 .filter(|p| !p.is_empty())
3783 else {
3784 return;
3785 };
3786
3787 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
3788 let _ = writeln!(file, "setup: implicit-bare-repository:{}", dir.display());
3789 }
3790}
3791
3792fn safe_directory_effective_values(git_dir: &Path, env: &Environment) -> Vec<String> {
3795 let cfg = crate::config::ConfigSet::load(env, Some(git_dir), true)
3796 .unwrap_or_else(|_| crate::config::ConfigSet::new());
3797 let mut values: Vec<String> = Vec::new();
3798 for e in cfg.entries() {
3799 if e.key == "safe.directory"
3800 && e.scope != crate::config::ConfigScope::Local
3801 && e.scope != crate::config::ConfigScope::Worktree
3802 {
3803 values.push(e.value.clone().unwrap_or_else(|| "true".to_owned()));
3804 }
3805 }
3806 let mut effective: Vec<String> = Vec::new();
3807 for v in values {
3808 if v.is_empty() {
3809 effective.clear();
3810 } else {
3811 effective.push(v);
3812 }
3813 }
3814 effective
3815}
3816
3817fn ensure_safe_directory_allows(
3818 git_dir: &Path,
3819 checked: &Path,
3820 environment: &Environment,
3821) -> Result<()> {
3822 let effective = safe_directory_effective_values(git_dir, environment);
3823 let checked_s = checked.to_string_lossy().to_string();
3824 let _ = environment.grit_debug_safe_dir;
3825 if effective
3826 .iter()
3827 .any(|v| safe_directory_matches(v, &checked_s, &environment.discovery_cwd()))
3828 {
3829 return Ok(());
3830 }
3831 Err(Error::DubiousOwnership(path_for_error_message(checked)))
3832}
3833
3834#[cfg(unix)]
3835fn path_lstat_uid(path: &Path) -> std::io::Result<u32> {
3836 use std::os::unix::fs::MetadataExt;
3837 let meta = fs::symlink_metadata(path)?;
3838 Ok(meta.uid())
3839}
3840
3841#[cfg(unix)]
3844fn ensure_valid_ownership(
3845 environment: &Environment,
3846 gitfile: Option<&Path>,
3847 worktree: Option<&Path>,
3848 gitdir: &Path,
3849) -> Result<()> {
3850 const ROOT_UID: u32 = 0;
3851 let sudo_euid = environment
3852 .sudo_uid
3853 .as_deref()
3854 .and_then(|s| s.parse::<u32>().ok());
3855
3856 fn owned_by_effective_user(path: &Path, sudo_euid: Option<u32>) -> std::io::Result<bool> {
3857 let st_uid = path_lstat_uid(path)?;
3858 let mut euid = nix::unistd::geteuid().as_raw();
3859 if euid == ROOT_UID {
3860 if st_uid == ROOT_UID {
3861 return Ok(true);
3862 }
3863 if let Some(sudo_uid) = sudo_euid {
3864 euid = sudo_uid;
3865 }
3866 }
3867 Ok(st_uid == euid)
3868 }
3869
3870 let assume_different = environment.test_assume_different_owner();
3871 if !assume_different {
3872 let gitfile_ok = gitfile
3873 .map(|p| owned_by_effective_user(p, sudo_euid))
3874 .transpose()?
3875 .unwrap_or(true);
3876 let wt_ok = match worktree {
3879 None => true,
3880 Some(wt) => match owned_by_effective_user(wt, sudo_euid) {
3881 Ok(ok) => ok,
3882 Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
3883 Err(e) => return Err(Error::Io(e)),
3884 },
3885 };
3886 let gd_ok = owned_by_effective_user(gitdir, sudo_euid)?;
3887 if gitfile_ok && wt_ok && gd_ok {
3888 return Ok(());
3889 }
3890 }
3891
3892 let data_path = if let Some(wt) = worktree {
3893 wt.canonicalize_simplified()
3894 .unwrap_or_else(|_| wt.to_path_buf())
3895 } else {
3896 gitdir
3897 .canonicalize_simplified()
3898 .unwrap_or_else(|_| gitdir.to_path_buf())
3899 };
3900 ensure_safe_directory_allows(gitdir, &data_path, environment)
3901}
3902
3903#[cfg(not(unix))]
3904fn ensure_valid_ownership(
3905 _environment: &Environment,
3906 _gitfile: Option<&Path>,
3907 _worktree: Option<&Path>,
3908 _gitdir: &Path,
3909) -> Result<()> {
3910 Ok(())
3911}
3912
3913impl Repository {
3914 pub fn enforce_safe_directory(&self) -> Result<()> {
3920 let assume_different =
3921 self.test_assume_different_owner || self.environment.test_assume_different_owner();
3922 if !assume_different {
3923 return Ok(());
3924 }
3925
3926 if self.explicit_git_dir {
3927 return Ok(());
3928 }
3929
3930 let checked = if let Some(wt) = &self.work_tree {
3934 let cwd = Some(self.environment.discovery_cwd());
3935 if let Some(cwd) = cwd {
3936 if cwd
3937 .canonicalize_simplified()
3938 .ok()
3939 .is_some_and(|c| c.starts_with(&self.git_dir))
3940 {
3941 self.git_dir
3942 .canonicalize_simplified()
3943 .unwrap_or_else(|_| self.git_dir.clone())
3944 } else {
3945 wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone())
3946 }
3947 } else {
3948 wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone())
3949 }
3950 } else {
3951 self.git_dir
3952 .canonicalize_simplified()
3953 .unwrap_or_else(|_| self.git_dir.clone())
3954 };
3955
3956 let _ = self.environment.grit_debug_safe_dir;
3957 self.enforce_safe_directory_checked(&checked)
3958 }
3959
3960 pub(crate) fn enforce_safe_directory_git_dir(&self) -> Result<()> {
3965 let assume_different =
3966 self.test_assume_different_owner || self.environment.test_assume_different_owner();
3967 if !assume_different {
3968 return Ok(());
3969 }
3970 let checked = self
3971 .git_dir
3972 .canonicalize_simplified()
3973 .unwrap_or_else(|_| self.git_dir.clone());
3974 let _ = self.environment.grit_debug_safe_dir;
3975 self.enforce_safe_directory_checked(&checked)
3976 }
3977
3978 fn enforce_safe_directory_checked(&self, checked: &Path) -> Result<()> {
3979 ensure_safe_directory_allows(&self.git_dir, checked, self.environment.as_ref())
3980 }
3981
3982 pub fn verify_safe_for_clone_source(&self) -> Result<()> {
3988 let assume_different =
3989 self.test_assume_different_owner || self.environment.test_assume_different_owner();
3990 if assume_different {
3991 self.enforce_safe_directory_git_dir()
3992 } else {
3993 #[cfg(unix)]
3994 {
3995 ensure_valid_ownership(self.environment.as_ref(), None, None, &self.git_dir)
3996 }
3997 #[cfg(not(unix))]
3998 {
3999 Ok(())
4000 }
4001 }
4002 }
4003}
4004
4005fn normalize_fs_path(raw: &str) -> String {
4006 use std::path::Component;
4007 let p = std::path::Path::new(raw);
4008 let mut parts: Vec<String> = Vec::new();
4009 let mut absolute = false;
4010 for c in p.components() {
4011 match c {
4012 Component::RootDir => {
4013 absolute = true;
4014 parts.clear();
4015 }
4016 Component::CurDir => {}
4017 Component::ParentDir => {
4018 if !parts.is_empty() {
4019 parts.pop();
4020 }
4021 }
4022 Component::Normal(s) => parts.push(s.to_string_lossy().to_string()),
4023 Component::Prefix(_) => {}
4024 }
4025 }
4026 let mut out = if absolute {
4027 String::from("/")
4028 } else {
4029 String::new()
4030 };
4031 out.push_str(&parts.join("/"));
4032 out
4033}
4034
4035fn safe_directory_matches(config_value: &str, checked: &str, cwd: &Path) -> bool {
4036 if config_value == "*" {
4037 return true;
4038 }
4039 if config_value == "." {
4040 let cwd_s = normalize_fs_path(&cwd.to_string_lossy());
4042 let checked_s = normalize_fs_path(checked);
4043 return cwd_s == checked_s;
4044 }
4045
4046 let canonicalize_or_normalize = |raw: &str| -> String {
4047 let p = std::path::Path::new(raw);
4048 if p.exists() {
4049 p.canonicalize_simplified()
4050 .map(|c| c.to_string_lossy().to_string())
4051 .map(|s| normalize_fs_path(&s))
4052 .unwrap_or_else(|_| normalize_fs_path(raw))
4053 } else {
4054 normalize_fs_path(raw)
4055 }
4056 };
4057
4058 let config_norm = canonicalize_or_normalize(config_value);
4059 let checked_norm = normalize_fs_path(checked);
4060
4061 if config_norm.ends_with("/*") {
4062 let prefix_raw = &config_norm[..config_norm.len() - 2];
4063 let prefix_norm = canonicalize_or_normalize(prefix_raw);
4064 let mut prefix = prefix_norm;
4065 if !prefix.ends_with('/') {
4066 prefix.push('/');
4067 }
4068 return checked_norm.starts_with(&prefix);
4069 }
4070
4071 config_norm == checked_norm
4072}
4073
4074fn warn_core_bare_worktree_conflict(options: &RepositoryOptions, repo: &Repository) {
4078 if options
4079 .environment
4080 .git_work_tree
4081 .as_deref()
4082 .is_some_and(|s| !s.trim().is_empty())
4083 {
4084 return;
4085 }
4086 let Ok(cfg) = repo.ensure_config_arc_fast() else {
4087 return;
4088 };
4089 if let (true, Some(_)) = read_core_bare_and_worktree_from_config(&cfg) {
4090 options
4091 .diagnostics
4092 .warn(diagnostics::Warning::CoreBareWithWorktree);
4093 }
4094}
4095
4096fn read_core_bare_and_worktree_from_config(cfg: &ConfigSet) -> (bool, Option<String>) {
4097 let bare = cfg
4098 .get_bool("core.bare")
4099 .and_then(|r| r.ok())
4100 .unwrap_or(false);
4101 let worktree = cfg.get("core.worktree").map(|s| s.to_owned());
4102 (bare, worktree)
4103}
4104
4105fn read_core_bare_and_worktree(git_dir: &Path) -> Result<(bool, Option<String>)> {
4106 let cfg = ConfigSet::load(
4107 &crate::environment::Environment::empty(),
4108 Some(git_dir),
4109 true,
4110 )
4111 .unwrap_or_default();
4112 Ok(read_core_bare_and_worktree_from_config(&cfg))
4113}
4114
4115fn validate_git_work_tree_path(path: &Path) -> Result<()> {
4118 if !path.is_absolute() {
4119 return Ok(());
4120 }
4121 let comps: Vec<Component<'_>> = path.components().collect();
4122 let Some(last_normal_idx) = comps
4123 .iter()
4124 .enumerate()
4125 .rev()
4126 .find_map(|(i, c)| matches!(c, Component::Normal(_)).then_some(i))
4127 else {
4128 return Ok(());
4129 };
4130 let mut cur = PathBuf::new();
4131 for (i, comp) in comps.iter().enumerate() {
4132 match comp {
4133 Component::Prefix(p) => cur.push(p.as_os_str()),
4134 Component::RootDir => cur.push(comp.as_os_str()),
4135 Component::CurDir => {}
4136 Component::ParentDir => {
4137 let _ = cur.pop();
4138 }
4139 Component::Normal(seg) => {
4140 cur.push(seg);
4141 if i != last_normal_idx && !cur.exists() {
4142 return Err(Error::PathError(format!(
4143 "Invalid path '{}': No such file or directory",
4144 cur.display()
4145 )));
4146 }
4147 }
4148 }
4149 }
4150 Ok(())
4151}
4152
4153fn resolve_core_worktree_path(git_dir: &Path, raw: &str) -> Result<PathBuf> {
4154 let p = Path::new(raw);
4155 if p.is_absolute() {
4156 return Ok(p
4157 .canonicalize_simplified()
4158 .unwrap_or_else(|_| p.to_path_buf()));
4159 }
4160 let joined = git_dir.join(p);
4161 Ok(joined.canonicalize_simplified().unwrap_or(joined))
4162}
4163
4164fn resolve_git_dir_env_path(git_dir: &Path) -> Result<PathBuf> {
4166 if git_dir.is_file() {
4167 return resolve_gitfile(git_dir);
4168 }
4169 Ok(git_dir.to_path_buf())
4170}
4171
4172pub fn resolve_git_directory_arg(git_dir: &Path) -> Result<PathBuf> {
4178 resolve_git_dir_env_path(git_dir)
4179}
4180
4181pub fn resolve_dot_git(dot_git: &Path) -> Result<PathBuf> {
4187 if dot_git.is_dir() {
4188 return dot_git
4189 .canonicalize_simplified()
4190 .map_err(|_| not_a_repository_at(dot_git));
4191 }
4192 if dot_git.is_file() {
4193 return resolve_gitfile(dot_git);
4194 }
4195 Err(not_a_repository_at(dot_git))
4196}
4197
4198fn not_a_repository_at(path: &Path) -> Error {
4199 Error::NotARepository(NotARepositoryReason::NoRepositoryAt {
4200 path: path.to_path_buf(),
4201 })
4202}
4203
4204fn canonicalize_existing_prefix(path: &Path) -> Result<PathBuf> {
4208 let absolute = std::path::absolute(path)?;
4209 let mut missing = Vec::new();
4210 let mut existing = absolute.as_path();
4211 loop {
4212 if let Ok(canonical) = existing.canonicalize_simplified() {
4213 return Ok(missing
4214 .iter()
4215 .rev()
4216 .fold(canonical, |acc: PathBuf, part| acc.join(part)));
4217 }
4218 match (existing.parent(), existing.file_name()) {
4219 (Some(parent), Some(name)) => {
4220 missing.push(name.to_owned());
4221 existing = parent;
4222 }
4223 _ => return Ok(absolute),
4224 }
4225 }
4226}
4227
4228fn resolve_objects_dir(
4238 environment: &Environment,
4239 git_dir: &Path,
4240 common_git_dir: &Path,
4241) -> Option<PathBuf> {
4242 if let Some(dir) = environment.git_object_directory.as_deref() {
4243 return (!dir.is_empty()).then(|| environment.discovery_cwd().join(dir));
4244 }
4245 let own = git_dir.join("objects");
4246 Some(if own.exists() {
4247 own
4248 } else {
4249 common_git_dir.join("objects")
4250 })
4251}
4252
4253fn head_file_present(git_dir: &Path) -> bool {
4254 let head_path = git_dir.join("HEAD");
4255 head_path.exists() || head_path.is_symlink()
4256}
4257
4258fn open_or_install_ref_store(
4261 caches: &RepoCaches,
4262 git_dir: &Path,
4263 environment: &Environment,
4264 injected: Option<Arc<dyn crate::refs::store::RefStore>>,
4265) -> Result<Arc<dyn crate::refs::store::RefStore>> {
4266 match injected {
4267 Some(store) => {
4268 let store = linked_worktree_view(git_dir, store);
4269 caches.install_ref_store(git_dir, Arc::clone(&store));
4270 Ok(store)
4271 }
4272 None => caches.open_ref_store(git_dir, environment),
4273 }
4274}
4275
4276fn linked_worktree_view(
4283 git_dir: &Path,
4284 store: Arc<dyn crate::refs::store::RefStore>,
4285) -> Arc<dyn crate::refs::store::RefStore> {
4286 use crate::refs::store::{LinkedWorktreeRefStore, RefStorageFormat};
4287 let on_disk = matches!(
4288 store.format(),
4289 RefStorageFormat::Files | RefStorageFormat::Reftable
4290 );
4291 if on_disk || !git_dir.join("commondir").is_file() {
4292 return store;
4293 }
4294 Arc::new(LinkedWorktreeRefStore::new(git_dir.to_path_buf(), store))
4295}
4296
4297fn resolve_gitfile(gitfile: &Path) -> Result<PathBuf> {
4299 let content = fs::read_to_string(gitfile).map_err(|error| {
4300 Error::NotARepository(NotARepositoryReason::GitfileUnreadable {
4301 path: gitfile.to_path_buf(),
4302 error,
4303 })
4304 })?;
4305 parse_gitfile(&content, gitfile)
4306}
4307
4308fn parse_gitfile(content: &str, gitfile: &Path) -> Result<PathBuf> {
4311 let Some(target) = content
4312 .lines()
4313 .find_map(|line| line.strip_prefix("gitdir:"))
4314 else {
4315 return Err(Error::NotARepository(
4316 NotARepositoryReason::GitfileMissingGitdir {
4317 path: gitfile.to_path_buf(),
4318 },
4319 ));
4320 };
4321 let path = gitfile
4322 .parent()
4323 .unwrap_or_else(|| Path::new(""))
4324 .join(target.trim());
4325 if !path.exists() {
4326 return Err(not_a_repository_at(&path));
4327 }
4328 Ok(path)
4329}
4330
4331pub fn resolve_initial_branch(env: &Environment, explicit: Option<&str>) -> Result<String> {
4341 let name = match explicit {
4342 Some(name) => name.to_owned(),
4343 None => ConfigSet::load(env, None, true)?
4344 .get_last_entry("init.defaultBranch")
4345 .and_then(|e| e.value.clone())
4346 .filter(|s| !s.is_empty())
4347 .unwrap_or_else(|| "main".to_owned()),
4348 };
4349 validate_branch_short_name(&name).map_err(|e| Error::InvalidRef(e.to_string()))?;
4350 Ok(name)
4351}
4352
4353fn resolve_git_dir_for_init(path: &Path, options: &InitOptions) -> Result<PathBuf> {
4354 if options.bare_with_worktree_at_path || options.bare {
4355 return Ok(path.to_path_buf());
4356 }
4357 if let Some(separate) = &options.separate_git_dir {
4358 return Ok(separate.clone());
4359 }
4360 let dot_git = path.join(".git");
4361 if dot_git.is_file() {
4362 let content = fs::read_to_string(&dot_git)?;
4363 return parse_gitfile(&content, &dot_git);
4364 }
4365 Ok(dot_git)
4366}
4367
4368fn head_branch_name_for_outcome(git_dir: &Path) -> Result<String> {
4370 if let Some(target) = crate::refs::read_symbolic_ref(git_dir, "HEAD")? {
4371 if let Some(short) = target.strip_prefix("refs/heads/") {
4372 return Ok(short.to_owned());
4373 }
4374 return Ok(target
4375 .rsplit('/')
4376 .next()
4377 .unwrap_or(target.as_str())
4378 .to_owned());
4379 }
4380 let head_text = fs::read_to_string(git_dir.join("HEAD"))?;
4381 let oid = head_text.trim();
4382 if crate::objects::ObjectId::from_hex(oid).is_ok() {
4383 let end = oid.len().min(7);
4384 return Ok(oid[..end].to_owned());
4385 }
4386 Ok("HEAD".to_owned())
4387}
4388
4389fn hash_algo_from_git_dir(git_dir: &Path) -> Result<HashAlgo> {
4390 let config_path = git_dir.join("config");
4391 if !config_path.is_file() {
4392 return Ok(HashAlgo::Sha1);
4393 }
4394 let content = fs::read_to_string(&config_path)?;
4395 Ok(parse_repository_format(&content, &config_path)?.hash_algo())
4396}
4397
4398fn ensure_git_directory_layout(
4400 git_dir: &Path,
4401 bare: bool,
4402 ref_storage: crate::ref_storage::RefStorageFormat,
4403 skip_hooks_and_info: bool,
4404) -> Result<()> {
4405 let mut subs = vec!["objects", "objects/info", "objects/pack", "refs"];
4406 if !ref_storage.is_reftable() {
4407 subs.extend(["refs/heads", "refs/tags"]);
4408 }
4409 if !bare && !skip_hooks_and_info {
4410 subs.push("info");
4411 subs.push("hooks");
4412 }
4413 for sub in subs {
4414 fs::create_dir_all(git_dir.join(sub))?;
4415 }
4416
4417 if ref_storage.is_reftable() {
4418 let reftable_dir = git_dir.join("reftable");
4419 fs::create_dir_all(&reftable_dir)?;
4420 let tables_list = reftable_dir.join("tables.list");
4421 if !tables_list.exists() {
4422 fs::write(&tables_list, "")?;
4423 }
4424 }
4425 Ok(())
4426}
4427
4428fn write_fresh_git_directory(
4429 git_dir: &Path,
4430 bare: bool,
4431 initial_branch: &str,
4432 template_dir: Option<&Path>,
4433 ref_storage: crate::ref_storage::RefStorageFormat,
4434 object_format: HashAlgo,
4435 skip_hooks_and_info: bool,
4436) -> Result<()> {
4437 ensure_git_directory_layout(git_dir, bare, ref_storage, skip_hooks_and_info)?;
4438
4439 if let Some(tmpl) = template_dir {
4440 if tmpl.is_dir() {
4441 copy_template(tmpl, git_dir)?;
4442 }
4443 }
4444
4445 let needs_extensions = ref_storage.is_reftable() || object_format != HashAlgo::Sha1;
4446 let repo_version = if needs_extensions { 1 } else { 0 };
4447
4448 let mut config_content = String::from("[core]\n");
4449 config_content.push_str(&format!("\trepositoryformatversion = {repo_version}\n"));
4450 config_content.push_str("\tfilemode = true\n");
4451 if bare {
4452 config_content.push_str("\tbare = true\n");
4453 } else {
4454 config_content.push_str("\tbare = false\n");
4455 config_content.push_str("\tlogallrefupdates = true\n");
4456 }
4457 if needs_extensions {
4458 config_content.push_str("[extensions]\n");
4459 if ref_storage.is_reftable() {
4460 config_content.push_str("\trefStorage = reftable\n");
4461 }
4462 if object_format != HashAlgo::Sha1 {
4463 config_content.push_str(&format!("\tobjectformat = {}\n", object_format.name()));
4464 }
4465 }
4466 fs::write(git_dir.join("config"), config_content)?;
4467
4468 if let Some(tmpl) = template_dir {
4470 if tmpl.is_dir() {
4471 let tmpl_config = tmpl.join("config");
4472 if tmpl_config.is_file() {
4473 let tmpl_text = fs::read_to_string(&tmpl_config)?;
4474 let tmpl_parsed = ConfigFile::parse(&tmpl_config, &tmpl_text, ConfigScope::Local)?;
4475 let dest_path = git_dir.join("config");
4476 let dest_text = fs::read_to_string(&dest_path)?;
4477 let mut dest_parsed =
4478 ConfigFile::parse(&dest_path, &dest_text, ConfigScope::Local)?;
4479 for e in &tmpl_parsed.entries {
4480 if e.key == "core.bare" {
4482 continue;
4483 }
4484 if let Some(v) = &e.value {
4485 let _ = dest_parsed.set(&e.key, v);
4486 } else {
4487 let _ = dest_parsed.set(&e.key, "true");
4488 }
4489 }
4490 dest_parsed.write()?;
4491 }
4492 }
4493 }
4494
4495 crate::refs::store::initialize_head(
4496 git_dir,
4497 crate::refs::store::RawRef::Symbolic(format!("refs/heads/{initial_branch}")),
4498 )?;
4499
4500 apply_init_filesystem_config(
4501 git_dir,
4502 InitFilesystemConfigOptions::default(),
4503 &Environment::empty(),
4504 )?;
4505
4506 fs::write(
4507 git_dir.join("description"),
4508 "Unnamed repository; edit this file 'description' to name the repository.\n",
4509 )?;
4510 Ok(())
4511}
4512
4513pub(crate) fn init_repository_separate_git_dir(
4522 work_tree: &Path,
4523 git_dir: &Path,
4524 initial_branch: &str,
4525 template_dir: Option<&Path>,
4526 ref_storage: crate::ref_storage::RefStorageFormat,
4527 object_format: HashAlgo,
4528) -> Result<Repository> {
4529 let skip_hooks_info = template_dir.is_some_and(|p| p.as_os_str().is_empty());
4530 fs::create_dir_all(work_tree)?;
4531 fs::create_dir_all(git_dir)?;
4532 let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
4533 if existing {
4534 ensure_git_directory_layout(git_dir, false, ref_storage, skip_hooks_info)?;
4535 } else {
4536 write_fresh_git_directory(
4537 git_dir,
4538 false,
4539 initial_branch,
4540 template_dir,
4541 ref_storage,
4542 object_format,
4543 skip_hooks_info,
4544 )?;
4545 }
4546
4547 let gitfile = work_tree.join(".git");
4553 let abs_git_dir = crate::git_path::absolute_path_for_gitfile_stored_path(git_dir);
4554 fs::write(gitfile, format!("gitdir: {abs_git_dir}\n"))?;
4555
4556 Repository::open(
4557 git_dir,
4558 &RepositoryOptions::empty().with_work_tree(work_tree),
4559 )
4560}
4561
4562pub fn ensure_core_bare(git_dir: &Path) -> Result<()> {
4577 let path = git_dir.join("config");
4578 let text = fs::read_to_string(&path).unwrap_or_default();
4579 if text.lines().any(|l| {
4580 let t = l.trim();
4581 t == "bare = true" || t == "bare=true"
4582 }) {
4583 return Ok(());
4584 }
4585 let mut out = text;
4586 if !out.ends_with('\n') && !out.is_empty() {
4587 out.push('\n');
4588 }
4589 if !out.contains("[core]") {
4590 out.push_str("[core]\n");
4591 }
4592 out.push_str("\tbare = true\n");
4593 fs::write(path, out).map_err(Error::Io)
4594}
4595
4596pub(crate) fn init_repository(
4597 path: &Path,
4598 bare: bool,
4599 initial_branch: &str,
4600 template_dir: Option<&Path>,
4601 ref_storage: crate::ref_storage::RefStorageFormat,
4602) -> Result<Repository> {
4603 init_repository_with_object_format(
4604 path,
4605 bare,
4606 initial_branch,
4607 template_dir,
4608 ref_storage,
4609 HashAlgo::Sha1,
4610 )
4611}
4612
4613pub(crate) fn init_repository_with_object_format(
4614 path: &Path,
4615 bare: bool,
4616 initial_branch: &str,
4617 template_dir: Option<&Path>,
4618 ref_storage: crate::ref_storage::RefStorageFormat,
4619 object_format: HashAlgo,
4620) -> Result<Repository> {
4621 let skip_hooks_info = !bare && template_dir.is_some_and(|p| p.as_os_str().is_empty());
4622 let git_dir = if bare {
4623 path.to_path_buf()
4624 } else {
4625 path.join(".git")
4626 };
4627
4628 if !bare {
4629 fs::create_dir_all(path)?;
4630 }
4631 fs::create_dir_all(&git_dir)?;
4632 let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
4633 if existing {
4634 ensure_git_directory_layout(&git_dir, bare, ref_storage, skip_hooks_info)?;
4635 } else {
4636 write_fresh_git_directory(
4637 &git_dir,
4638 bare,
4639 initial_branch,
4640 template_dir,
4641 ref_storage,
4642 object_format,
4643 skip_hooks_info,
4644 )?;
4645 }
4646
4647 if bare {
4648 Repository::open(&git_dir, &RepositoryOptions::empty())
4649 } else {
4650 Repository::open(
4651 &git_dir,
4652 &RepositoryOptions::empty().with_work_tree(path.to_path_buf()),
4653 )
4654 }
4655}
4656
4657pub(crate) fn init_bare_with_env_worktree(
4666 git_dir: &Path,
4667 work_tree: &Path,
4668 initial_branch: &str,
4669 template_dir: Option<&Path>,
4670 ref_storage: crate::ref_storage::RefStorageFormat,
4671 object_format: HashAlgo,
4672) -> Result<Repository> {
4673 fs::create_dir_all(git_dir)?;
4674 fs::create_dir_all(work_tree)?;
4675 let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
4676 if existing {
4677 ensure_git_directory_layout(git_dir, true, ref_storage, false)?;
4678 } else {
4679 write_fresh_git_directory(
4680 git_dir,
4681 true,
4682 initial_branch,
4683 template_dir,
4684 ref_storage,
4685 object_format,
4686 false,
4687 )?;
4688 }
4689 let work_tree_abs = work_tree
4690 .canonicalize_simplified()
4691 .unwrap_or_else(|_| work_tree.to_path_buf());
4692 let config_path = git_dir.join("config");
4693 let mut config = match ConfigFile::from_path(&config_path, ConfigScope::Local)? {
4694 Some(c) => c,
4695 None => ConfigFile::parse(&config_path, "", ConfigScope::Local)?,
4696 };
4697 config.set("core.worktree", &work_tree_abs.to_string_lossy())?;
4698 config.write()?;
4699 Repository::open(
4700 git_dir,
4701 &RepositoryOptions::empty().with_work_tree(work_tree),
4702 )
4703}
4704
4705fn copy_template(src: &Path, dst: &Path) -> Result<()> {
4707 for entry in fs::read_dir(src)? {
4708 let entry = entry?;
4709 let src_path = entry.path();
4710 let dst_path = dst.join(entry.file_name());
4711 if src_path.is_dir() {
4712 fs::create_dir_all(&dst_path)?;
4713 copy_template(&src_path, &dst_path)?;
4714 } else {
4715 fs::copy(&src_path, &dst_path)?;
4716 }
4717 }
4718 Ok(())
4719}
4720
4721fn parse_ceiling_directories(env: &Environment) -> (Vec<PathBuf>, bool) {
4730 let Some(raw) = env.git_ceiling_directories.as_deref() else {
4731 return (Vec::new(), false);
4732 };
4733 if raw.is_empty() {
4734 return (Vec::new(), false);
4735 }
4736 let (no_resolve, effective) = match raw.strip_prefix(':') {
4738 Some(rest) => (true, rest),
4739 None => (false, raw),
4740 };
4741 let paths = effective
4742 .split(':')
4743 .filter(|s| !s.is_empty())
4744 .filter_map(|s| {
4745 let p = PathBuf::from(s);
4746 if !p.is_absolute() {
4747 return None;
4748 }
4749 if no_resolve {
4750 let s = s.trim_end_matches('/');
4752 Some(PathBuf::from(s))
4753 } else {
4754 Some(p.canonicalize_simplified().unwrap_or_else(|_| {
4757 let s = s.trim_end_matches('/');
4758 PathBuf::from(s)
4759 }))
4760 }
4761 })
4762 .collect();
4763 (paths, no_resolve)
4764}
4765
4766pub fn validate_repo_config(config_text: &str) -> std::result::Result<(), String> {
4769 let mut version: u32 = 0;
4770 let mut in_core = false;
4771 for line in config_text.lines() {
4772 let trimmed = line.trim();
4773 if trimmed.starts_with('[') {
4774 in_core = trimmed.to_lowercase().starts_with("[core");
4775 continue;
4776 }
4777 if in_core {
4778 if let Some(rest) = trimmed.strip_prefix("repositoryformatversion") {
4779 let val = rest.trim_start_matches([' ', '=']).trim();
4780 if let Ok(v) = val.parse::<u32>() {
4781 version = v;
4782 }
4783 }
4784 }
4785 }
4786 if version >= 2 {
4787 return Err(format!("unknown repository format version: {version}"));
4788 }
4789 Ok(())
4790}
4791
4792pub mod caches {
4794 pub use crate::repo_caches::*;
4795}
4796
4797mod index_write_fail_inject {
4798 use std::cell::RefCell;
4799
4800 thread_local! {
4802 pub static INJECT: RefCell<bool> = const { RefCell::new(false) };
4803 pub static INJECT_REMAINING: RefCell<Option<usize>> = const { RefCell::new(None) };
4804 }
4805}
4806
4807#[doc(hidden)]
4809pub fn set_test_inject_index_write_fail(enabled: bool) {
4810 index_write_fail_inject::INJECT.with(|c| *c.borrow_mut() = enabled);
4811 if !enabled {
4812 index_write_fail_inject::INJECT_REMAINING.with(|c| *c.borrow_mut() = None);
4813 }
4814}
4815
4816#[doc(hidden)]
4818pub fn set_test_inject_index_write_fail_remaining(remaining: usize) {
4819 index_write_fail_inject::INJECT.with(|c| *c.borrow_mut() = true);
4820 index_write_fail_inject::INJECT_REMAINING.with(|c| *c.borrow_mut() = Some(remaining));
4821}
4822
4823mod rewrite_apply_inject {
4824 use std::cell::RefCell;
4825
4826 thread_local! {
4828 pub static ABORT_AFTER_REFS: RefCell<bool> = const { RefCell::new(false) };
4829 }
4830}
4831
4832#[doc(hidden)]
4834pub fn set_test_inject_rewrite_apply_abort_after_refs(enabled: bool) {
4835 rewrite_apply_inject::ABORT_AFTER_REFS.with(|c| *c.borrow_mut() = enabled);
4836}
4837
4838pub(crate) fn test_inject_rewrite_apply_abort_after_refs() -> bool {
4839 rewrite_apply_inject::ABORT_AFTER_REFS.with(|c| *c.borrow())
4840}
4841
4842fn test_inject_index_write_fail() -> Result<()> {
4843 index_write_fail_inject::INJECT.with(|enabled| {
4844 if !*enabled.borrow() {
4845 return Ok(());
4846 }
4847 index_write_fail_inject::INJECT_REMAINING.with(|remaining| {
4848 if let Some(left) = remaining.borrow_mut().as_mut() {
4849 if *left == 0 {
4850 return Ok(());
4851 }
4852 *left -= 1;
4853 }
4854 Err(Error::Io(std::io::Error::other(
4855 "injected index write failure",
4856 )))
4857 })
4858 })
4859}
4860
4861#[cfg(test)]
4862#[path = "repo_line_coverage_tests.rs"]
4863mod line_coverage_tests;