1use std::collections::BTreeSet;
30use std::fs;
31use std::fs::OpenOptions;
32use std::io::Write;
33use std::path::{Component, Path, PathBuf};
34use std::sync::{Arc, Mutex};
35
36use crate::change_id::ChangeIdGenerator;
37use crate::check_ref_format::validate_branch_short_name;
38use crate::command_runner::CommandRunner;
39use crate::config::{
40 config_file_stamps, stamp_for_path, ConfigCacheKey, ConfigFile, ConfigScope, ConfigSet,
41 LoadConfigOptions,
42};
43use crate::diagnostics::{self, DiagnosticsHandle};
44use crate::environment::{Environment, RepositoryOptions};
45use crate::error::{Error, Result};
46use crate::git_dir::io_error::map_index_write_error;
47use crate::git_path::path_for_error_message;
48use crate::hooks::run_hook;
49use crate::index::Index;
50use crate::init_filesystem::{apply_init_filesystem_config, InitFilesystemConfigOptions};
51use crate::objects::{parse_commit, HashAlgo, ObjectId};
52use crate::odb::Odb;
53use crate::repo_caches::RepoCaches;
54use crate::rev_parse::is_inside_work_tree;
55use crate::sparse_checkout::effective_cone_mode_for_sparse_file;
56use crate::split_index::{write_index_file_split, WriteSplitIndexRequest};
57use crate::state::resolve_head;
58use crate::worktree_cwd::cwd_relative_under_work_tree;
59
60fn compute_git_prefix(env: &Environment, work_tree: &Path) -> Option<String> {
62 if let Some(prefix) = env.git_prefix.as_deref() {
63 let p = prefix.trim();
64 if !p.is_empty() {
65 return Some(p.to_owned());
66 }
67 }
68 let rel = cwd_relative_under_work_tree(work_tree, &env.cwd)?;
69 if rel.is_empty() {
70 None
71 } else {
72 Some(rel)
73 }
74}
75
76fn read_sparse_checkout_patterns(git_dir: &Path) -> Vec<String> {
77 let path = git_dir.join("info").join("sparse-checkout");
78 let Ok(content) = fs::read_to_string(&path) else {
79 return Vec::new();
80 };
81 content
82 .lines()
83 .map(|l| l.trim())
84 .filter(|l| !l.is_empty() && !l.starts_with('#'))
85 .map(String::from)
86 .collect()
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct DiscoveryInfo {
92 pub explicit_git_dir: bool,
94 pub discovery_root: Option<PathBuf>,
96 pub work_tree_from_env: bool,
98 pub discovery_via_gitfile: bool,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct InitOutcome {
105 pub reinitialized: bool,
107 pub initial_branch: String,
109}
110
111#[derive(Debug, Clone)]
113#[non_exhaustive]
114pub struct InitOptions {
115 pub bare: bool,
117 pub initial_branch: Option<String>,
123 pub template_dir: Option<PathBuf>,
125 pub ref_storage: crate::ref_storage::RefStorageFormat,
127 pub separate_git_dir: Option<PathBuf>,
129 pub bare_with_worktree_at_path: bool,
131 pub object_format: Option<HashAlgo>,
135 pub environment: Option<Environment>,
137}
138
139impl Default for InitOptions {
140 fn default() -> Self {
141 Self {
142 bare: false,
143 initial_branch: None,
144 template_dir: None,
145 ref_storage: crate::ref_storage::RefStorageFormat::Files,
146 separate_git_dir: None,
147 bare_with_worktree_at_path: false,
148 object_format: None,
149 environment: None,
150 }
151 }
152}
153
154impl InitOptions {
155 #[must_use]
157 pub fn new() -> Self {
158 Self::default()
159 }
160
161 #[must_use]
163 pub fn with_bare(mut self, bare: bool) -> Self {
164 self.bare = bare;
165 self
166 }
167
168 #[must_use]
170 pub fn with_initial_branch(mut self, initial_branch: impl Into<String>) -> Self {
171 self.initial_branch = Some(initial_branch.into());
172 self
173 }
174
175 #[must_use]
177 pub fn with_object_format(mut self, object_format: HashAlgo) -> Self {
178 self.object_format = Some(object_format);
179 self
180 }
181
182 #[must_use]
184 pub fn with_environment(mut self, environment: Environment) -> Self {
185 self.environment = Some(environment);
186 self
187 }
188
189 #[must_use]
191 pub fn with_ref_storage(mut self, ref_storage: crate::ref_storage::RefStorageFormat) -> Self {
192 self.ref_storage = ref_storage;
193 self
194 }
195
196 #[must_use]
198 pub fn with_template_dir(mut self, template_dir: Option<PathBuf>) -> Self {
199 self.template_dir = template_dir;
200 self
201 }
202
203 #[must_use]
205 pub fn with_separate_git_dir(mut self, separate_git_dir: Option<PathBuf>) -> Self {
206 self.separate_git_dir = separate_git_dir;
207 self
208 }
209
210 #[must_use]
212 pub fn with_bare_with_worktree_at_path(mut self, bare_with_worktree_at_path: bool) -> Self {
213 self.bare_with_worktree_at_path = bare_with_worktree_at_path;
214 self
215 }
216}
217
218#[must_use]
262pub struct RepositoryBuilder {
263 options: RepositoryOptions,
264 odb_builder: Option<crate::odb::OdbBuilder>,
265 ref_store: Option<Arc<dyn crate::refs::store::RefStore>>,
266 initial_branch: Option<String>,
267}
268
269impl RepositoryBuilder {
270 pub fn new(options: RepositoryOptions) -> Self {
272 Self {
273 options,
274 odb_builder: None,
275 ref_store: None,
276 initial_branch: None,
277 }
278 }
279
280 pub fn odb_builder(mut self, builder: crate::odb::OdbBuilder) -> Self {
289 self.odb_builder = Some(builder);
290 self
291 }
292
293 pub fn ref_store(mut self, store: Arc<dyn crate::refs::store::RefStore>) -> Self {
298 self.ref_store = Some(store);
299 self
300 }
301
302 pub fn initial_branch(mut self, branch: impl Into<String>) -> Self {
307 self.initial_branch = Some(branch.into());
308 self
309 }
310
311 pub fn open(self, git_dir: &Path, work_tree: Option<&Path>) -> Result<Repository> {
327 let mut options = self.options;
328 if let Some(wt) = work_tree {
329 options.work_tree = Some(wt.to_path_buf());
330 }
331 let repo = if let Some(builder) = self.odb_builder {
332 Repository::open_with_odb_internal(&options, git_dir, builder, self.ref_store)?
333 } else {
334 Repository::open_internal_with_refs(&options, git_dir, self.ref_store)?
335 };
336 if let Some(branch) = self.initial_branch {
337 seed_unborn_head(repo.refs(), &branch)?;
338 }
339 Ok(repo)
340 }
341}
342
343fn seed_unborn_head(store: &dyn crate::refs::store::RefStore, branch: &str) -> Result<()> {
345 use crate::refs::store::{Expected, RawRef, RefStoreError, RefTransaction, RefUpdate};
346
347 validate_branch_short_name(branch).map_err(|e| Error::InvalidRef(e.to_string()))?;
348 if store.read_raw("HEAD")?.is_some() {
349 return Ok(());
350 }
351 let txn = RefTransaction::new().update(RefUpdate {
352 name: "HEAD".to_owned(),
353 new_value: Some(RawRef::Symbolic(format!("refs/heads/{branch}"))),
354 expected: Expected::Missing,
355 reflog: None,
356 flags: crate::refs::store::RefUpdateFlags {
357 no_deref: true,
358 ..Default::default()
359 },
360 })?;
361 match store.prepare(txn).and_then(|prepared| prepared.commit()) {
362 Ok(()) | Err(RefStoreError::ExpectedMismatch { .. }) => Ok(()),
363 Err(err) => Err(err.into()),
364 }
365}
366
367pub struct Repository {
405 git_dir: PathBuf,
406 common_git_dir: PathBuf,
408 work_tree: Option<PathBuf>,
409 odb: Odb,
410 explicit_git_dir: bool,
411 discovery_root: Option<PathBuf>,
412 work_tree_from_env: bool,
413 discovery_via_gitfile: bool,
414 config_session: Mutex<Option<ConfigSession>>,
416 caches: Arc<RepoCaches>,
418 ref_store: Arc<dyn crate::refs::store::RefStore>,
420 environment: Arc<Environment>,
422 git_prefix: Option<String>,
424 command_runner: Arc<dyn CommandRunner>,
426 change_id_generator: Arc<dyn ChangeIdGenerator>,
428 diagnostics: DiagnosticsHandle,
429 network_trace: bool,
430 reference_unix_time: Option<i64>,
432 test_assume_different_owner: bool,
433 thread_policy: crate::parallel::ThreadPolicy,
434 hook_stdio: crate::hooks::HookStdio,
435}
436
437const _: () = {
438 fn assert_send_sync<T: Send + Sync>() {}
439 let _ = assert_send_sync::<Repository>;
440};
441
442impl std::fmt::Debug for Repository {
443 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
444 f.debug_struct("Repository")
445 .field("git_dir", &self.git_dir())
446 .field("work_tree", &self.work_tree())
447 .field("explicit_git_dir", &self.discovery_info().explicit_git_dir)
448 .field("network_trace", &self.network_trace)
449 .finish_non_exhaustive()
450 }
451}
452
453pub(crate) type RepositoryConfigSnapshot = Arc<RepoCaches>;
455
456#[derive(Debug, Clone)]
458struct RepoCachedSettings {
459 use_replace_refs: bool,
461 replace_ref_base: String,
463}
464
465#[derive(Clone)]
467struct ConfigSessionValidation {
468 key: ConfigCacheKey,
469 env_fingerprint: Vec<(String, Option<String>)>,
470 base_stamps: Vec<(PathBuf, Option<(std::time::SystemTime, u64)>)>,
471 extra_stamps: Vec<(PathBuf, Option<(std::time::SystemTime, u64)>)>,
472}
473
474struct ConfigSession {
476 config: Arc<ConfigSet>,
477 validation: ConfigSessionValidation,
478 cached_settings: RepoCachedSettings,
479 replace_lookup_inactive: bool,
481}
482
483impl ConfigSessionValidation {
484 fn still_valid(&self, env: &Environment, git_dir: &Path, opts: &LoadConfigOptions) -> bool {
485 let Some(env_fp) = env.config_fingerprint() else {
486 return false;
487 };
488 if self.env_fingerprint.as_slice() != env_fp.as_slice() {
489 return false;
490 }
491 let key = ConfigCacheKey::new(Some(git_dir), opts);
492 if key != self.key {
493 return false;
494 }
495 let base_stamps = config_file_stamps(env, Some(git_dir), opts);
496 if self.base_stamps.as_slice() != base_stamps.as_slice() {
497 return false;
498 }
499 for (path, stamp) in &self.extra_stamps {
500 if stamp_for_path(path) != *stamp {
501 return false;
502 }
503 }
504 true
505 }
506}
507
508impl Repository {
509 fn config_load_options(&self, include_system: bool) -> LoadConfigOptions {
510 LoadConfigOptions {
511 include_system,
512 include_ctx: crate::config::IncludeContext {
513 git_dir: Some(self.git_dir.clone()),
514 cwd: self.environment.cwd.clone(),
515 pwd: self.environment.pwd.clone(),
516 ..Default::default()
517 },
518 diagnostics: Some(self.diagnostics()),
519 repo_caches: Some(Arc::clone(&self.caches)),
520 ..Default::default()
521 }
522 }
523
524 fn from_canonical_git_dir(
525 options: &RepositoryOptions,
526 git_dir: PathBuf,
527 work_tree: Option<&Path>,
528 injected_refs: Option<Arc<dyn crate::refs::store::RefStore>>,
529 ) -> Result<Self> {
530 let environment = Arc::new(options.environment.clone());
531 let command_runner = Arc::clone(&options.command_runner);
532 let change_id_generator = Arc::clone(&options.change_id_generator);
533 if injected_refs.is_none() && !head_file_present(&git_dir) {
536 return Err(not_a_repository_at(&git_dir));
537 }
538
539 let common_git_dir = resolve_common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
540 let objects_dir = resolve_objects_dir(&environment, &git_dir, &common_git_dir)
541 .filter(|dir| dir.exists())
542 .ok_or_else(|| not_a_repository_at(&git_dir))?;
543
544 let work_tree = match work_tree {
545 Some(p) => {
546 let cwd = environment.discovery_cwd();
547 let mut resolved = if p.is_absolute() {
548 p.to_path_buf()
549 } else {
550 cwd.join(p)
551 };
552 if resolved.exists() {
553 resolved = resolved
554 .canonicalize()
555 .map_err(|_| Error::PathError(p.display().to_string()))?;
556 }
557 Some(resolved)
558 }
559 None => None,
560 };
561
562 let caches = RepoCaches::with_diagnostics(
563 Arc::clone(&command_runner),
564 Arc::clone(&options.diagnostics),
565 );
566 caches.set_promisor_fetcher(options.promisor_fetcher.clone());
567 let odb = if let Some(ref wt) = work_tree {
568 Odb::with_work_tree(&objects_dir, wt)
569 .with_config_git_dir(git_dir.clone())
570 .with_shared_config_state(caches.clone(), environment.clone())
571 } else {
572 Odb::new(&objects_dir)
573 .with_config_git_dir(git_dir.clone())
574 .with_shared_config_state(caches.clone(), environment.clone())
575 };
576
577 let git_prefix = work_tree
578 .as_ref()
579 .and_then(|wt| compute_git_prefix(environment.as_ref(), wt));
580
581 let ref_store = open_or_install_ref_store(&caches, &git_dir, &environment, injected_refs)?;
582 caches.configure_filter_subprocess(environment.as_ref(), &git_dir, work_tree.as_deref());
583
584 Ok(Self {
585 git_dir,
586 common_git_dir,
587 work_tree,
588 odb,
589 explicit_git_dir: false,
590 discovery_root: None,
591 work_tree_from_env: false,
592 discovery_via_gitfile: false,
593 config_session: Mutex::new(None),
594 caches,
595 ref_store,
596 environment,
597 git_prefix,
598 command_runner,
599 change_id_generator,
600 diagnostics: Arc::clone(&options.diagnostics),
601 network_trace: options.network_trace,
602 reference_unix_time: options.reference_unix_time,
603 test_assume_different_owner: options.test_assume_different_owner,
604 thread_policy: options.thread_policy,
605 hook_stdio: options.hook_stdio,
606 })
607 }
608
609 #[must_use]
611 pub fn thread_policy(&self) -> crate::parallel::ThreadPolicy {
612 self.thread_policy
613 }
614
615 #[must_use]
617 pub fn with_thread_policy(mut self, policy: crate::parallel::ThreadPolicy) -> Self {
618 self.thread_policy = policy;
619 self
620 }
621
622 #[must_use]
624 pub(crate) fn caches(&self) -> &Arc<RepoCaches> {
625 &self.caches
626 }
627
628 #[must_use]
630 pub(crate) fn promisor_hydrate_hook(&self) -> Option<crate::blame::PromisorHydrateHook> {
631 self.caches.promisor_hydrate_hook()
632 }
633
634 #[must_use]
636 pub fn environment(&self) -> &Environment {
637 &self.environment
638 }
639
640 #[must_use]
642 pub fn reference_unix_time(&self) -> i64 {
643 self.wall_clock_epoch()
644 }
645
646 #[must_use]
648 pub fn wall_clock_epoch(&self) -> i64 {
649 if let Some(ts) = self.reference_unix_time {
650 return ts;
651 }
652 if let Some(ts) = self.environment.git_now_date_override() {
653 return ts;
654 }
655 crate::git_date::tm::process_wall_clock_sec()
656 }
657
658 #[must_use]
662 pub fn identity_now(&self) -> time::OffsetDateTime {
663 crate::environment::offset_from_epoch_and_tz(
664 self.wall_clock_epoch(),
665 self.environment.tz.as_deref(),
666 )
667 }
668
669 pub fn identity(
703 &self,
704 role: crate::config::ident_resolve::IdentRole,
705 now: time::OffsetDateTime,
706 ) -> Result<crate::ident::Signature> {
707 let config = self.config()?;
708 Ok(crate::config::ident_resolve::resolve_signature_with(
709 self.environment(),
710 &config,
711 role,
712 now,
713 )?)
714 }
715
716 #[must_use]
718 pub fn command_runner(&self) -> Arc<dyn CommandRunner> {
719 Arc::clone(&self.command_runner)
720 }
721
722 #[must_use]
725 pub fn hook_stdio(&self) -> crate::hooks::HookStdio {
726 self.hook_stdio
727 }
728
729 #[must_use]
731 pub fn change_id_generator(&self) -> Arc<dyn ChangeIdGenerator> {
732 Arc::clone(&self.change_id_generator)
733 }
734
735 #[must_use]
741 pub fn linked_worktree_discover_options(&self) -> RepositoryOptions {
742 let mut environment = (*self.environment).clone();
743 environment.git_dir = None;
744 environment.git_work_tree = None;
745 environment.git_index_file = None;
746 environment.git_prefix = None;
747 environment.git_object_directory = None;
748 RepositoryOptions {
749 environment,
750 command_runner: Arc::clone(&self.command_runner),
751 change_id_generator: Arc::clone(&self.change_id_generator),
752 diagnostics: Arc::clone(&self.diagnostics),
753 network_trace: self.network_trace,
754 test_assume_different_owner: self.test_assume_different_owner,
755 reference_unix_time: self.reference_unix_time,
756 thread_policy: self.thread_policy,
757 work_tree: None,
758 validate_repository_format: true,
759 open_for_serving: false,
760 promisor_fetcher: self.caches().promisor_fetcher(),
761 hook_stdio: self.hook_stdio,
762 }
763 }
764
765 pub(crate) fn open_linked_worktree(&self, admin_dir: &Path, work_tree: &Path) -> Result<Self> {
778 let options = self.linked_worktree_discover_options();
779 if self.refs_on_disk() && self.odb.uses_files_primary() {
780 return Self::discover(Some(work_tree), &options);
781 }
782 let git_dir = admin_dir
783 .canonicalize()
784 .map_err(|_| not_a_repository_at(admin_dir))?;
785 let work_tree = work_tree
786 .canonicalize()
787 .map_err(|_| Error::PathError(work_tree.display().to_string()))?;
788 let environment = Arc::new(options.environment);
789 let caches = RepoCaches::with_diagnostics(
790 Arc::clone(&self.command_runner),
791 Arc::clone(&self.diagnostics),
792 );
793 let ref_store = if self.refs_on_disk() {
794 caches.open_ref_store(&git_dir, &environment)?
795 } else {
796 let view: Arc<dyn crate::refs::store::RefStore> =
797 Arc::new(crate::refs::store::LinkedWorktreeRefStore::new(
798 git_dir.clone(),
799 Arc::clone(&self.ref_store),
800 ));
801 caches.install_ref_store(&git_dir, Arc::clone(&view));
802 view
803 };
804 let repo = Self {
805 git_prefix: compute_git_prefix(&environment, &work_tree),
806 git_dir,
807 common_git_dir: self.common_git_dir.clone(),
808 work_tree: Some(work_tree),
809 odb: self.odb.clone(),
810 explicit_git_dir: false,
811 discovery_root: None,
812 work_tree_from_env: false,
813 discovery_via_gitfile: true,
814 config_session: Mutex::new(None),
815 caches,
816 ref_store,
817 environment,
818 command_runner: Arc::clone(&self.command_runner),
819 change_id_generator: Arc::clone(&self.change_id_generator),
820 diagnostics: Arc::clone(&self.diagnostics),
821 network_trace: self.network_trace,
822 reference_unix_time: self.reference_unix_time,
823 test_assume_different_owner: self.test_assume_different_owner,
824 thread_policy: self.thread_policy,
825 hook_stdio: self.hook_stdio,
826 };
827 repo.caches().configure_filter_subprocess(
828 repo.environment.as_ref(),
829 repo.git_dir(),
830 repo.work_tree(),
831 );
832 repo.refresh_config_session()?;
833 Ok(repo)
834 }
835
836 fn repo_cached_settings_from_config(
837 cfg: &ConfigSet,
838 environment: &Environment,
839 ) -> RepoCachedSettings {
840 let use_replace_refs = cfg
841 .get_bool("core.useReplaceRefs")
842 .and_then(|r| r.ok())
843 .unwrap_or(true);
844 let replace_ref_base = environment
845 .git_replace_ref_base
846 .clone()
847 .filter(|s| !s.is_empty())
848 .unwrap_or_else(|| "refs/replace/".to_owned());
849 let replace_ref_base = if replace_ref_base.ends_with('/') {
850 replace_ref_base
851 } else {
852 format!("{replace_ref_base}/")
853 };
854 RepoCachedSettings {
855 use_replace_refs,
856 replace_ref_base,
857 }
858 }
859
860 fn ensure_config_arc(&self) -> Result<Arc<ConfigSet>> {
861 self.config_arc_with_validation(true)
862 }
863
864 pub(crate) fn ensure_config_arc_fast(&self) -> Result<Arc<ConfigSet>> {
866 self.config_arc_with_validation(false)
867 }
868
869 fn config_arc_with_validation(&self, validate_stamps: bool) -> Result<Arc<ConfigSet>> {
870 if validate_stamps {
871 if let Ok(guard) = self.config_session.lock() {
872 if let Some(session) = guard.as_ref() {
873 let opts = self.config_load_options(true);
874 if session.validation.still_valid(
875 self.environment.as_ref(),
876 &self.git_dir,
877 &opts,
878 ) {
879 return Ok(Arc::clone(&session.config));
880 }
881 }
882 }
883 } else if let Ok(guard) = self.config_session.lock() {
884 if let Some(session) = guard.as_ref() {
885 return Ok(Arc::clone(&session.config));
886 }
887 }
888 self.refresh_config_session()?;
889 self.config_session
890 .lock()
891 .map_err(|e| Error::Message(format!("config session lock poisoned: {e}")))?
892 .as_ref()
893 .map(|s| Arc::clone(&s.config))
894 .ok_or_else(|| Error::Message("config session missing after refresh".to_owned()))
895 }
896
897 fn refresh_config_session(&self) -> Result<()> {
898 crate::trace::trace_span!("grit.config.load");
899 let opts = self.config_load_options(true);
900 if let Ok(guard) = self.config_session.lock() {
901 if let Some(session) = guard.as_ref() {
902 if session
903 .validation
904 .still_valid(self.environment.as_ref(), &self.git_dir, &opts)
905 {
906 return Ok(());
907 }
908 let env = self.environment.as_ref();
909 let base_stamps = config_file_stamps(env, Some(&self.git_dir), &opts);
910 if session.validation.base_stamps.as_slice() != base_stamps.as_slice() {
911 self.caches.invalidate_config_file_derived();
912 }
913 }
914 }
915 self.caches.clear_session_config();
916 let env = self.environment.as_ref();
917 let cfg = self
918 .caches
919 .load_config_with_options(env, Some(&self.git_dir), &opts)?;
920 let key = ConfigCacheKey::new(Some(&self.git_dir), &opts);
921 let (env_fingerprint, base_stamps, extra_stamps) =
922 self.caches.config_entry_stamps(&key).unwrap_or_else(|| {
923 (
924 env.config_fingerprint().unwrap_or_default(),
925 config_file_stamps(env, Some(&self.git_dir), &opts),
926 Vec::new(),
927 )
928 });
929 let cached_settings =
930 Self::repo_cached_settings_from_config(cfg.as_ref(), self.environment.as_ref());
931 let replace_lookup_inactive = self.replace_lookup_inactive(cfg.as_ref());
932 self.caches
933 .set_session_config(key.clone(), Arc::clone(&cfg));
934 let session = ConfigSession {
935 config: Arc::clone(&cfg),
936 validation: ConfigSessionValidation {
937 key,
938 env_fingerprint,
939 base_stamps,
940 extra_stamps,
941 },
942 cached_settings,
943 replace_lookup_inactive,
944 };
945 *self
946 .config_session
947 .lock()
948 .map_err(|e| Error::Message(format!("config session lock poisoned: {e}")))? =
949 Some(session);
950 Ok(())
951 }
952
953 fn replace_lookup_inactive(&self, cfg: &ConfigSet) -> bool {
954 if self.environment.git_no_replace_objects {
955 return true;
956 }
957 if !cfg
958 .get_bool("core.useReplaceRefs")
959 .and_then(|r| r.ok())
960 .unwrap_or(true)
961 {
962 return true;
963 }
964 let replace_dir = self.common_git_dir.join("refs/replace");
965 !replace_dir_is_non_empty(&replace_dir)
966 }
967
968 #[must_use]
970 pub fn diagnostics(&self) -> DiagnosticsHandle {
971 Arc::clone(&self.diagnostics)
972 }
973
974 pub fn set_diagnostics(&mut self, sink: DiagnosticsHandle) {
976 self.caches.set_diagnostics(Arc::clone(&sink));
977 self.diagnostics = sink;
978 }
979
980 #[must_use]
982 pub fn network_trace_enabled(&self) -> bool {
983 self.network_trace
984 }
985
986 pub fn set_network_trace(&mut self, enabled: bool) {
988 self.network_trace = enabled;
989 }
990
991 pub(crate) fn warn(&self, warning: diagnostics::Warning) {
992 self.diagnostics.warn(warning);
993 }
994
995 pub fn config(&self) -> Result<Arc<ConfigSet>> {
1006 self.ensure_config_arc()
1007 }
1008
1009 pub fn reload_config(&self) -> Result<()> {
1018 self.caches.invalidate_config_cache();
1019 if let Ok(mut guard) = self.config_session.lock() {
1020 *guard = None;
1021 }
1022 self.refresh_config_session()?;
1023 Ok(())
1024 }
1025
1026 pub(crate) fn install_config_snapshot(&self) {
1028 let _ = self
1029 .caches
1030 .open_ref_store(&self.git_dir, self.environment.as_ref());
1031 }
1032
1033 #[must_use]
1035 pub fn common_git_dir(&self) -> &Path {
1036 &self.common_git_dir
1037 }
1038
1039 fn validate_repository_format_cached(&self) -> Result<()> {
1040 let Some(config_path) = repository_config_path(&self.git_dir) else {
1041 return Ok(());
1042 };
1043 let parsed = self.caches.repository_format(&self.git_dir, &config_path)?;
1044 validate_repository_format_parsed(&parsed)
1045 }
1046
1047 #[must_use]
1049 pub fn refs(&self) -> &dyn crate::refs::store::RefStore {
1050 self.ref_store.as_ref()
1051 }
1052
1053 #[must_use]
1057 pub(crate) fn refs_on_disk(&self) -> bool {
1058 use crate::refs::store::RefStorageFormat;
1059 matches!(
1060 self.ref_store.format(),
1061 RefStorageFormat::Files | RefStorageFormat::Reftable
1062 )
1063 }
1064
1065 #[inline]
1067 #[must_use]
1068 pub fn objects(&self) -> crate::objects::Objects<'_> {
1069 crate::objects::Objects::new(self)
1070 }
1071
1072 #[inline]
1074 #[must_use]
1075 pub fn references(&self) -> crate::refs::References<'_> {
1076 crate::refs::References::new(self)
1077 }
1078
1079 #[inline]
1081 #[must_use]
1082 pub fn index_handle(&self) -> crate::index::IndexHandle<'_> {
1083 crate::index::IndexHandle::new(self)
1084 }
1085
1086 #[inline]
1088 #[must_use]
1089 pub fn config_handle(&self) -> crate::config::ConfigHandle<'_> {
1090 crate::config::ConfigHandle::new(self)
1091 }
1092
1093 #[inline]
1095 #[must_use]
1096 pub fn rev_walk(&self) -> crate::revwalk::RevWalk<'_> {
1097 crate::revwalk::RevWalk::new(self)
1098 }
1099
1100 #[inline]
1102 #[must_use]
1103 pub fn diff(&self) -> crate::diff::Diff<'_> {
1104 crate::diff::Diff::new(self)
1105 }
1106
1107 #[must_use]
1109 pub fn remotes(&self) -> crate::repo_handles::RepositoryRemotes<'_> {
1110 crate::repo_handles::RepositoryRemotes::new(self)
1111 }
1112
1113 #[must_use]
1115 pub fn worktrees(&self) -> crate::repo_handles::RepositoryWorktrees<'_> {
1116 crate::repo_handles::RepositoryWorktrees::new(self)
1117 }
1118
1119 #[must_use]
1121 pub fn stash(&self) -> crate::repo_handles::RepositoryStash<'_> {
1122 crate::repo_handles::RepositoryStash::new(self)
1123 }
1124
1125 #[must_use]
1127 pub fn notes(&self) -> crate::repo_handles::RepositoryNotes<'_> {
1128 crate::repo_handles::RepositoryNotes::new(self)
1129 }
1130
1131 #[must_use]
1133 pub fn hooks(&self) -> crate::repo_handles::RepositoryHooks<'_> {
1134 crate::repo_handles::RepositoryHooks::new(self)
1135 }
1136
1137 pub fn fsck(
1146 &self,
1147 options: &crate::fsck::FsckOptions,
1148 progress: &mut dyn crate::progress::ProgressSink,
1149 cancel: &dyn crate::progress::Cancel,
1150 ) -> crate::error::Result<crate::fsck::FsckReport> {
1151 crate::fsck::fsck(self, options, progress, cancel)
1152 }
1153
1154 pub fn tree_sha256(
1163 &self,
1164 oid: crate::objects::ObjectId,
1165 options: &crate::tree_sha256::TreeSha256Options,
1166 ) -> crate::error::Result<crate::tree_sha256::TreeSha256Report> {
1167 crate::tree_sha256::tree_sha256(self, oid, options)
1168 }
1169
1170 pub fn verify_tag(
1179 &self,
1180 oid: crate::objects::ObjectId,
1181 options: &crate::tree_sha256::TagVerifyOptions,
1182 ) -> crate::error::Result<crate::tree_sha256::TagVerification> {
1183 crate::tree_sha256::verify_tag(self, oid, options)
1184 }
1185
1186 pub fn patch_id(
1196 &self,
1197 commit: &crate::objects::ObjectId,
1198 mode: crate::patch_ids::PatchIdMode,
1199 ) -> crate::error::Result<Option<crate::objects::ObjectId>> {
1200 crate::patch_ids::compute_patch_id(self.odb().hash_algo(), self.odb(), commit, mode)
1201 }
1202
1203 #[must_use]
1205 pub fn reflogs(&self) -> crate::repo_handles::RepositoryReflogs<'_> {
1206 crate::repo_handles::RepositoryReflogs::new(self)
1207 }
1208
1209 #[must_use]
1211 pub fn bundles(&self) -> crate::repo_handles::RepositoryBundles<'_> {
1212 crate::repo_handles::RepositoryBundles::new(self)
1213 }
1214
1215 pub fn repack(
1221 &self,
1222 opts: &crate::maintenance::RepackOptions,
1223 ) -> crate::error::Result<crate::maintenance::RepackReport> {
1224 crate::maintenance::repack(self, opts)
1225 }
1226
1227 pub(crate) fn resolve_ref_oid(&self, refname: &str) -> Result<crate::objects::ObjectId> {
1233 self.ref_store.resolve(refname)
1234 }
1235
1236 #[must_use]
1238 pub fn with_ref_store(mut self, store: Arc<dyn crate::refs::store::RefStore>) -> Self {
1239 self.caches
1240 .install_ref_store(&self.git_dir, Arc::clone(&store));
1241 self.ref_store = store;
1242 self
1243 }
1244
1245 #[must_use]
1247 pub(crate) fn pathspec_precompose_enabled(&self) -> bool {
1248 self.caches
1249 .pathspec_precompose_enabled(self.environment.as_ref(), &self.git_dir)
1250 }
1251
1252 #[cfg(test)]
1255 pub(crate) fn force_pathspec_precompose_for_test(&self) {
1256 self.caches.force_pathspec_precompose_for_test();
1257 }
1258
1259 pub fn resolve_worktree_pathspec(
1273 &self,
1274 pathspec: &str,
1275 prefix: Option<&str>,
1276 ) -> std::result::Result<String, crate::pathspec::PathspecResolveError> {
1277 let work_tree = self
1278 .work_tree()
1279 .ok_or(crate::pathspec::PathspecResolveError::NeedsWorkTree)?;
1280 Ok(
1281 crate::pathspec::resolve_pathspec_in_worktree_with_argv_precompose(
1282 pathspec,
1283 pathspec,
1284 work_tree,
1285 prefix,
1286 self.pathspec_precompose_enabled(),
1287 )?,
1288 )
1289 }
1290
1291 #[must_use]
1292 pub(crate) fn pathspec_match_context(&self) -> crate::pathspec::PathspecMatchContext {
1293 crate::pathspec::pathspec_match_context_with_precompose(self.pathspec_precompose_enabled())
1294 }
1295
1296 pub fn paths_match_pathspecs(&self, path: &str, specs: &[String]) -> Result<bool> {
1306 Ok(self.pathspec_matcher(specs)?.matches(path))
1307 }
1308
1309 pub fn pathspec_matcher<'s>(
1325 &self,
1326 specs: &'s [String],
1327 ) -> Result<crate::pathspec::PathspecMatcher<'s>> {
1328 let precompose = self.pathspec_precompose_enabled();
1329 if !specs
1330 .iter()
1331 .any(|spec| crate::pathspec::pathspec_has_attr_magic(spec))
1332 {
1333 return Ok(crate::pathspec::PathspecMatcher::new(
1334 specs, precompose, None, false,
1335 ));
1336 }
1337 crate::pathspec::validate_attr_pathspecs(specs)?;
1338 let attrs = match self.work_tree() {
1339 Some(work_tree) => self.caches().gitattributes_stack_shared(self, work_tree)?,
1340 None => self.caches().gitattributes_bare_shared(self)?,
1341 };
1342 let icase = crate::crlf::config_ignorecase(self.config()?.as_ref());
1343 Ok(crate::pathspec::PathspecMatcher::new(
1344 specs,
1345 precompose,
1346 Some(attrs),
1347 icase,
1348 ))
1349 }
1350
1351 #[must_use]
1356 pub fn pathspec_cwd_prefix(&self) -> Option<&str> {
1357 self.git_prefix.as_deref().filter(|p| !p.is_empty())
1358 }
1359
1360 pub fn pathspec_argv_prefix<'a>(&'a self, pathspec: &str) -> Option<&'a str> {
1362 if crate::pathspec::pathspec_has_top(pathspec) {
1363 None
1364 } else {
1365 self.pathspec_cwd_prefix()
1366 }
1367 }
1368
1369 pub fn resolve_rev_walk_pathspecs(&self, paths: &[String]) -> Result<Vec<String>> {
1379 if paths.is_empty() {
1380 return Ok(Vec::new());
1381 }
1382 let prefix = self.pathspec_cwd_prefix();
1383 let cwd_for_implicit = prefix.map(str::to_owned);
1384 let mut resolved = Vec::with_capacity(paths.len());
1385 if self.work_tree().is_some() {
1386 for pathspec in paths {
1387 let prefix = Self::pathspec_argv_prefix(self, pathspec);
1388 let spec = self
1389 .resolve_worktree_pathspec(pathspec, prefix)
1390 .map_err(|e| Error::PathError(e.to_string()))?;
1391 resolved.push(spec);
1392 }
1393 } else {
1394 let root = self.git_dir();
1395 for pathspec in paths {
1396 let prefix = Self::pathspec_argv_prefix(self, pathspec);
1397 resolved.push(crate::pathspec::resolve_pathspec(pathspec, root, prefix));
1398 }
1399 }
1400 Ok(crate::pathspec::extend_pathspec_list_implicit_cwd(
1401 &resolved,
1402 cwd_for_implicit.as_deref(),
1403 ))
1404 }
1405
1406 #[must_use]
1408 pub fn git_dir(&self) -> &Path {
1409 &self.git_dir
1410 }
1411
1412 #[must_use]
1414 pub fn work_tree(&self) -> Option<&Path> {
1415 self.work_tree.as_deref()
1416 }
1417
1418 #[must_use]
1420 pub fn odb(&self) -> &Odb {
1421 &self.odb
1422 }
1423
1424 pub fn prefetch_promised(&self, oids: &[ObjectId]) -> Result<()> {
1430 let Some(fetcher) = self.caches().promisor_fetcher() else {
1431 return Err(Error::Message(
1432 "promisor prefetch requires a PromisorFetcher on RepositoryOptions".into(),
1433 ));
1434 };
1435 fetcher.fetch_missing(self, oids)
1436 }
1437
1438 #[must_use]
1440 pub fn discovery_info(&self) -> DiscoveryInfo {
1441 DiscoveryInfo {
1442 explicit_git_dir: self.explicit_git_dir,
1443 discovery_root: self.discovery_root.clone(),
1444 work_tree_from_env: self.work_tree_from_env,
1445 discovery_via_gitfile: self.discovery_via_gitfile,
1446 }
1447 }
1448
1449 fn cached_settings(&self) -> RepoCachedSettings {
1450 if let Ok(guard) = self.config_session.lock() {
1451 if let Some(session) = guard.as_ref() {
1452 return session.cached_settings.clone();
1453 }
1454 }
1455 self.ensure_config_arc_fast()
1456 .map(|cfg| {
1457 Self::repo_cached_settings_from_config(cfg.as_ref(), self.environment.as_ref())
1458 })
1459 .unwrap_or_else(|_| {
1460 Self::repo_cached_settings_from_config(&ConfigSet::new(), self.environment.as_ref())
1461 })
1462 }
1463
1464 pub fn init(path: &Path, options: &InitOptions) -> Result<Self> {
1470 Self::init_with_outcome(path, options).map(|(repo, _)| repo)
1471 }
1472
1473 pub fn init_with_outcome(path: &Path, options: &InitOptions) -> Result<(Self, InitOutcome)> {
1480 if options.bare && options.separate_git_dir.is_some() {
1481 return Err(Error::InitBareWithSeparateGitDir);
1482 }
1483 let env = options
1484 .environment
1485 .clone()
1486 .unwrap_or_else(Environment::empty);
1487 let git_dir = resolve_git_dir_for_init(path, options)?;
1488 let reinitialized = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
1489 let existing_format = hash_algo_from_git_dir(&git_dir)?;
1490 let object_format = match (reinitialized, options.object_format) {
1491 (true, None) => existing_format,
1492 (true, Some(requested)) if requested != existing_format => {
1493 return Err(Error::InitObjectFormatChange);
1494 }
1495 (true, Some(requested)) => requested,
1496 (false, None) => HashAlgo::Sha1,
1497 (false, Some(requested)) => requested,
1498 };
1499 let initial_branch = if reinitialized {
1500 head_branch_name_for_outcome(&git_dir)?
1501 } else {
1502 resolve_initial_branch(&env, options.initial_branch.as_deref())?
1503 };
1504
1505 let separate_layout = options.separate_git_dir.is_some()
1506 || (!options.bare
1507 && !options.bare_with_worktree_at_path
1508 && path.join(".git").is_file());
1509
1510 let repo = if options.bare_with_worktree_at_path {
1511 init_bare_with_env_worktree(
1512 path,
1513 path,
1514 &initial_branch,
1515 options.template_dir.as_deref(),
1516 options.ref_storage,
1517 object_format,
1518 )?
1519 } else if separate_layout {
1520 init_repository_separate_git_dir(
1521 path,
1522 &git_dir,
1523 &initial_branch,
1524 options.template_dir.as_deref(),
1525 options.ref_storage,
1526 object_format,
1527 )?
1528 } else {
1529 init_repository_with_object_format(
1530 path,
1531 options.bare,
1532 &initial_branch,
1533 options.template_dir.as_deref(),
1534 options.ref_storage,
1535 object_format,
1536 )?
1537 };
1538 Ok((
1539 repo,
1540 InitOutcome {
1541 reinitialized,
1542 initial_branch,
1543 },
1544 ))
1545 }
1546
1547 pub fn open(path: &Path, options: &RepositoryOptions) -> Result<Self> {
1557 if options.open_for_serving {
1558 return Self::open_for_serving_internal(path, options);
1559 }
1560 Self::open_internal(options, path)
1561 }
1562
1563 fn open_with_work_tree(
1564 options: &RepositoryOptions,
1565 git_dir: &Path,
1566 work_tree: Option<&Path>,
1567 ) -> Result<Self> {
1568 let mut opts = options.clone();
1569 opts.work_tree = work_tree.map(|p| p.to_path_buf());
1570 Self::open_internal(&opts, git_dir)
1571 }
1572
1573 fn open_internal(options: &RepositoryOptions, git_dir: &Path) -> Result<Self> {
1574 Self::open_internal_with_refs(options, git_dir, None)
1575 }
1576
1577 fn open_internal_with_refs(
1578 options: &RepositoryOptions,
1579 git_dir: &Path,
1580 injected_refs: Option<Arc<dyn crate::refs::store::RefStore>>,
1581 ) -> Result<Self> {
1582 crate::trace::trace_span!("grit.repository.open");
1583 let work_tree = options.work_tree.as_deref();
1584 let git_dir = git_dir
1585 .canonicalize()
1586 .map_err(|_| not_a_repository_at(git_dir))?;
1587
1588 let repo = Self::from_canonical_git_dir(options, git_dir, work_tree, injected_refs)?;
1589 repo.refresh_config_session()?;
1590 if options.validate_repository_format {
1591 repo.validate_repository_format_cached()?;
1592 }
1593 repo.install_config_snapshot();
1594 warn_core_bare_worktree_conflict(options, repo.git_dir());
1595 let _ = trace_repo_setup_if_requested(&repo);
1596 Ok(repo)
1597 }
1598
1599 fn open_for_serving_internal(path: &Path, options: &RepositoryOptions) -> Result<Self> {
1600 let mut with_git_suffix = path.as_os_str().to_owned();
1601 with_git_suffix.push(".git");
1602 let candidates = [
1603 (path.join(".git"), Some(path.to_path_buf())),
1604 (path.to_path_buf(), None),
1605 (PathBuf::from(with_git_suffix), None),
1606 ];
1607 for (git_dir, work_tree) in candidates {
1608 if !git_dir.join("HEAD").is_file() {
1609 continue;
1610 }
1611 let mut opts = options.clone();
1612 opts.work_tree = work_tree;
1613 opts.open_for_serving = false;
1614 return Self::open_internal(&opts, &git_dir);
1615 }
1616 Err(not_a_repository_at(path))
1617 }
1618
1619 fn open_with_odb_internal(
1620 options: &RepositoryOptions,
1621 git_dir: &Path,
1622 mut builder: crate::odb::OdbBuilder,
1623 injected_refs: Option<Arc<dyn crate::refs::store::RefStore>>,
1624 ) -> Result<Self> {
1625 let work_tree = options.work_tree.as_deref();
1626 let environment = Arc::new(options.environment.clone());
1627 let cwd = environment.discovery_cwd();
1628 let command_runner = Arc::clone(&options.command_runner);
1629 let change_id_generator = Arc::clone(&options.change_id_generator);
1630
1631 let fully_injected = injected_refs.is_some() && builder.has_custom_primary();
1634 let requested_git_dir = cwd.join(git_dir);
1635 let git_dir = match requested_git_dir.canonicalize() {
1636 Ok(dir) => dir,
1637 Err(_) if fully_injected => canonicalize_existing_prefix(&requested_git_dir)?,
1638 Err(_) => return Err(not_a_repository_at(git_dir)),
1639 };
1640
1641 if injected_refs.is_none() && !head_file_present(&git_dir) {
1642 return Err(not_a_repository_at(&git_dir));
1643 }
1644
1645 let common_git_dir = resolve_common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
1646 let objects_dir = resolve_objects_dir(&environment, &git_dir, &common_git_dir)
1647 .ok_or_else(|| not_a_repository_at(&git_dir))?;
1648
1649 if !builder.has_custom_primary() && !objects_dir.exists() {
1651 return Err(not_a_repository_at(&git_dir));
1652 }
1653 let objects_dir = canonicalize_existing_prefix(&objects_dir)?;
1654 let builder_objects = canonicalize_existing_prefix(&cwd.join(builder.objects_dir()))?;
1655 if builder_objects != objects_dir {
1656 return Err(Error::OdbObjectsDirMismatch {
1657 builder: builder_objects,
1658 repository: objects_dir,
1659 });
1660 }
1661 builder.set_objects_dir(objects_dir);
1662
1663 let work_tree = match work_tree {
1664 Some(p) => {
1665 let mut resolved = cwd.join(p);
1666 if resolved.exists() {
1667 resolved = resolved
1668 .canonicalize()
1669 .map_err(|_| Error::PathError(p.display().to_string()))?;
1670 }
1671 Some(resolved)
1672 }
1673 None => None,
1674 };
1675
1676 let caches = RepoCaches::with_diagnostics(
1677 Arc::clone(&command_runner),
1678 Arc::clone(&options.diagnostics),
1679 );
1680 let odb = builder
1681 .build()
1682 .with_config_git_dir(git_dir.clone())
1683 .with_shared_config_state(caches.clone(), environment.clone())
1684 .with_resolved_work_tree(work_tree.clone());
1685
1686 let git_prefix = work_tree
1687 .as_ref()
1688 .and_then(|wt| compute_git_prefix(environment.as_ref(), wt));
1689
1690 let ref_store = open_or_install_ref_store(&caches, &git_dir, &environment, injected_refs)?;
1691 caches.configure_filter_subprocess(environment.as_ref(), &git_dir, work_tree.as_deref());
1692
1693 let repo = Self {
1694 git_dir,
1695 common_git_dir,
1696 work_tree,
1697 odb,
1698 explicit_git_dir: false,
1699 discovery_root: None,
1700 work_tree_from_env: false,
1701 discovery_via_gitfile: false,
1702 config_session: Mutex::new(None),
1703 caches,
1704 ref_store,
1705 environment,
1706 git_prefix,
1707 command_runner,
1708 change_id_generator,
1709 diagnostics: Arc::clone(&options.diagnostics),
1710 network_trace: options.network_trace,
1711 reference_unix_time: options.reference_unix_time,
1712 test_assume_different_owner: options.test_assume_different_owner,
1713 thread_policy: options.thread_policy,
1714 hook_stdio: options.hook_stdio,
1715 };
1716 repo.refresh_config_session()?;
1717 if options.validate_repository_format {
1718 repo.validate_repository_format_cached()?;
1719 }
1720 warn_core_bare_worktree_conflict(options, repo.git_dir());
1721 let _ = trace_repo_setup_if_requested(&repo);
1722 Ok(repo)
1723 }
1724
1725 pub fn discover(start: Option<&Path>, options: &RepositoryOptions) -> Result<Self> {
1727 let env = &options.environment;
1728 let cwd = env.discovery_cwd();
1729
1730 if let Some(dir) = env.git_dir.as_deref() {
1732 let mut git_dir = PathBuf::from(dir);
1733 if git_dir.is_relative() {
1734 git_dir = cwd.join(git_dir);
1735 }
1736 git_dir = resolve_git_dir_env_path(&git_dir)?;
1738 let work_tree = env.git_work_tree.as_deref().map(|wt| {
1739 let p = PathBuf::from(wt);
1740 if p.is_absolute() {
1741 p
1742 } else {
1743 cwd.join(p)
1744 }
1745 });
1746 if let Some(ref wt_path) = work_tree {
1747 if env
1748 .git_work_tree
1749 .as_deref()
1750 .is_some_and(|raw| Path::new(raw).is_absolute())
1751 {
1752 validate_git_work_tree_path(wt_path)?;
1753 }
1754 }
1755 if work_tree.is_some() {
1756 let mut repo = Self::open_with_work_tree(options, &git_dir, work_tree.as_deref())?;
1757 repo.explicit_git_dir = true;
1758 repo.discovery_root = None;
1759 repo.work_tree_from_env = false;
1760 repo.discovery_via_gitfile = false;
1761 return Ok(repo);
1762 }
1763 let (is_bare, core_wt) = read_core_bare_and_worktree(&git_dir)?;
1765 let bare_worktree_conflict = is_bare && core_wt.is_some();
1766 let resolved_wt = if is_bare {
1767 None
1768 } else if let Some(raw) = core_wt {
1769 Some(resolve_core_worktree_path(&git_dir, &raw)?)
1770 } else {
1771 Some(cwd.canonicalize().unwrap_or_else(|_| cwd.clone()))
1777 };
1778 let mut repo = Self::open_with_work_tree(options, &git_dir, resolved_wt.as_deref())?;
1779 repo.explicit_git_dir = true;
1780 repo.discovery_root = None;
1781 repo.work_tree_from_env = false;
1782 repo.discovery_via_gitfile = false;
1783 let _ = bare_worktree_conflict;
1784 return Ok(repo);
1785 }
1786
1787 let env_work_tree = env.git_work_tree.as_deref().map(|wt| {
1790 let p = PathBuf::from(wt);
1791 if p.is_absolute() {
1792 p
1793 } else {
1794 cwd.join(p)
1795 }
1796 });
1797 if let Some(ref p) = env_work_tree {
1798 if env
1799 .git_work_tree
1800 .as_deref()
1801 .is_some_and(|raw| Path::new(raw).is_absolute())
1802 {
1803 validate_git_work_tree_path(p)?;
1804 }
1805 }
1806 let start = start.unwrap_or(&cwd);
1807 let start = if start.is_absolute() {
1808 start.to_path_buf()
1809 } else {
1810 cwd.join(start)
1811 };
1812
1813 let (ceiling_paths, no_resolve_ceilings) = parse_ceiling_directories(env);
1817 let ceiling_dirs: Vec<String> = ceiling_paths
1818 .into_iter()
1819 .map(|p| path_for_ceiling_compare(&p))
1820 .collect();
1821
1822 let start_canon = start.canonicalize().unwrap_or_else(|_| start.clone());
1823 let ceil_cmp_buf = if no_resolve_ceilings {
1825 path_for_ceiling_compare(&start)
1826 } else {
1827 path_for_ceiling_compare(&start_canon)
1828 };
1829 let mut dir_buf = path_for_ceiling_compare(&start_canon);
1830 let min_offset = offset_1st_component(&dir_buf);
1831 let mut ceil_offset: isize = longest_ancestor_length(&ceil_cmp_buf, &ceiling_dirs)
1832 .map(|n| n as isize)
1833 .unwrap_or(-1);
1834 if ceiling_dirs.contains(&dir_buf) {
1835 ceil_offset = dir_buf.len() as isize;
1836 } else if ceil_offset < 0 {
1837 ceil_offset = min_offset as isize - 2;
1838 }
1839
1840 loop {
1841 let current = Path::new(&dir_buf);
1842 if let Some(DiscoveredAt { mut repo, gitfile }) = try_open_at(options, current)? {
1843 repo.environment = Arc::new(env.clone());
1849 repo.refresh_config_session()?;
1850 repo.validate_repository_format_cached()?;
1851 repo.install_config_snapshot();
1852 let cfg = repo.config()?;
1853 repo.set_diagnostics(Arc::clone(&options.diagnostics));
1854 if let Some(ref wt) = env_work_tree {
1855 repo.work_tree = Some(wt.canonicalize().unwrap_or_else(|_| wt.clone()));
1856 repo.work_tree_from_env = true;
1857 } else {
1858 repo.work_tree_from_env = false;
1859 let linked_gitfile =
1864 repo.discovery_via_gitfile && resolve_common_dir(&repo.git_dir).is_some();
1865 if !linked_gitfile {
1866 let (is_bare, core_wt) =
1867 read_core_bare_and_worktree_from_config(cfg.as_ref());
1868 if is_bare {
1869 repo.work_tree = None;
1870 } else if let Some(raw) = core_wt {
1871 repo.work_tree = Some(resolve_core_worktree_path(&repo.git_dir, &raw)?);
1872 }
1873 }
1874 }
1875 let assume_different =
1876 options.test_assume_different_owner || env.test_assume_different_owner();
1877 if assume_different {
1878 repo.enforce_safe_directory()?;
1879 } else {
1880 ensure_valid_ownership(
1881 env,
1882 gitfile.as_deref(),
1883 repo.work_tree.as_deref(),
1884 &repo.git_dir,
1885 )?;
1886 }
1887 if let Some(wt) = repo.work_tree.as_ref() {
1888 repo.git_prefix = compute_git_prefix(env, wt);
1889 }
1890 let _ = trace_repo_setup_if_requested(&repo);
1891 return Ok(repo);
1892 }
1893
1894 let mut offset: isize = dir_buf.len() as isize;
1895 if offset <= min_offset as isize {
1896 break;
1897 }
1898 loop {
1899 offset -= 1;
1900 if offset <= ceil_offset {
1901 break;
1902 }
1903 if dir_buf
1904 .as_bytes()
1905 .get(offset as usize)
1906 .is_some_and(|b| *b == b'/')
1907 {
1908 break;
1909 }
1910 }
1911 if offset <= ceil_offset {
1912 break;
1913 }
1914 let off_u = offset as usize;
1915 let new_len = if off_u > min_offset {
1916 off_u
1917 } else {
1918 min_offset
1919 };
1920 dir_buf.truncate(new_len);
1921 }
1922
1923 Err(not_a_repository_at(&start))
1924 }
1925
1926 #[must_use]
1928 pub(crate) fn index_path(&self) -> PathBuf {
1929 self.git_dir.join("index")
1930 }
1931
1932 pub(crate) fn index_path_for_env(&self) -> Result<PathBuf> {
1936 if let Some(raw) = self.environment.git_index_file.as_deref() {
1937 if !raw.is_empty() {
1938 let p = PathBuf::from(raw);
1939 return Ok(if p.is_absolute() {
1940 p
1941 } else {
1942 self.environment.discovery_cwd().join(p)
1943 });
1944 }
1945 }
1946 Ok(self.index_path())
1947 }
1948
1949 pub(crate) fn load_index(&self) -> Result<Index> {
1953 let path = self.index_path_for_env()?;
1954 self.load_index_at(&path)
1955 }
1956
1957 pub(crate) fn load_index_at(&self, path: &std::path::Path) -> Result<Index> {
1960 self.load_index_at_with_context(
1961 path,
1962 crate::index::IndexParseContext::from_policy(self.thread_policy()),
1963 )
1964 }
1965
1966 pub(crate) fn load_index_at_with_context(
1967 &self,
1968 path: &std::path::Path,
1969 parse_ctx: crate::index::IndexParseContext,
1970 ) -> Result<Index> {
1971 crate::trace::trace_span!("grit.index.read");
1972 let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
1973 if let Some(res) = cfg.as_ref().get_bool("index.sparse") {
1974 res.map_err(|s| Error::Config(s.into()))?;
1975 }
1976 let mut idx = Index::load_expand_sparse_optional_with_context(
1977 path,
1978 &self.odb,
1979 Some(cfg.as_ref()),
1980 parse_ctx,
1981 )?;
1982 crate::split_index::resolve_split_index_if_needed(&mut idx, &self.git_dir, path)?;
1983 if idx.source_mtime.is_none() {
1984 idx.source_mtime = crate::index::index_file_mtime(path);
1985 }
1986 if let Some(ref wt) = self.work_tree {
1987 crate::sparse_checkout::clear_skip_worktree_from_present_files(
1988 &self.git_dir,
1989 wt,
1990 &mut idx,
1991 Some(cfg.as_ref()),
1992 );
1993 }
1994 Ok(idx)
1995 }
1996
1997 pub(crate) fn write_index(&self, index: &mut Index) -> Result<()> {
2000 self.write_index_with_post_index_change(index, false, false)
2001 }
2002
2003 pub(crate) fn begin_index_update(&self) -> Result<crate::index::IndexLock> {
2012 test_inject_index_write_fail()?;
2013 let path = self.index_path();
2014 let cfg = self.config()?;
2015 crate::index::IndexLock::acquire(&path, Some(cfg.as_ref()))
2016 }
2017
2018 pub(crate) fn commit_index_update(
2024 &self,
2025 lock: &mut crate::index::IndexLock,
2026 index: &mut Index,
2027 updated_workdir: bool,
2028 ) -> Result<()> {
2029 self.flush_index_update(lock, index)?;
2030 self.finish_index_update(lock, updated_workdir)
2031 }
2032
2033 pub(crate) fn flush_index_update(
2035 &self,
2036 lock: &mut crate::index::IndexLock,
2037 index: &mut Index,
2038 ) -> Result<()> {
2039 let path = self.index_path();
2040 let (skip_hash, cfg) = self.prepare_index_for_disk_write(&path, index)?;
2041 write_index_file_split(
2042 &path,
2043 &self.git_dir,
2044 index,
2045 cfg.as_ref(),
2046 WriteSplitIndexRequest::default(),
2047 skip_hash,
2048 self.thread_policy(),
2049 Some(lock),
2050 false,
2051 )
2052 }
2053
2054 pub(crate) fn finish_index_update(
2056 &self,
2057 lock: &mut crate::index::IndexLock,
2058 updated_workdir: bool,
2059 ) -> Result<()> {
2060 lock.commit_replace_index()?;
2061 let updated_workdir_arg = if updated_workdir { "1" } else { "0" };
2062 let _ = run_hook(self, "post-index-change", &[updated_workdir_arg, "0"], None);
2063 Ok(())
2064 }
2065
2066 pub fn write_largest_pack_bitmap(
2073 &self,
2074 options: &crate::pack_bitmap::PackBitmapWriteOptions,
2075 now: std::time::SystemTime,
2076 ) -> std::result::Result<std::path::PathBuf, crate::pack_bitmap::PackBitmapWriteError> {
2077 let path = self
2078 .odb
2079 .largest_pack_index_path()
2080 .map_err(|e| crate::pack_bitmap::PackBitmapWriteError::Object(e.to_string()))?
2081 .ok_or(crate::pack_bitmap::PackBitmapWriteError::InvalidPackPath(
2082 "no pack index in object store".to_owned(),
2083 ))?;
2084 crate::pack_bitmap::PackBitmapWriter::write(self, &path, options, now)
2085 }
2086
2087 pub(crate) fn try_write_index(&self, index: &mut Index) -> Result<bool> {
2100 self.try_write_index_at(&self.index_path(), index)
2101 }
2102
2103 pub(crate) fn try_write_index_at(&self, path: &Path, index: &mut Index) -> Result<bool> {
2106 match self.write_index_at(path, index) {
2107 Ok(()) => Ok(true),
2108 Err(e) if Self::opportunistic_index_write_skippable(&e) => Ok(false),
2109 Err(e) => Err(e),
2110 }
2111 }
2112
2113 fn opportunistic_index_write_skippable(err: &crate::error::Error) -> bool {
2114 use crate::error::Error;
2115 match err {
2116 Error::LockHeld { .. } => true,
2117 Error::Io(e) => matches!(
2118 e.kind(),
2119 std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::ReadOnlyFilesystem
2120 ),
2121 _ => false,
2122 }
2123 }
2124
2125 pub(crate) fn write_index_with_post_index_change(
2136 &self,
2137 index: &mut Index,
2138 updated_workdir: bool,
2139 updated_skipworktree: bool,
2140 ) -> Result<()> {
2141 self.write_index_at_with_post_index_change(
2142 &self.index_path(),
2143 index,
2144 updated_workdir,
2145 updated_skipworktree,
2146 )
2147 }
2148
2149 pub(crate) fn write_index_at(&self, path: &std::path::Path, index: &mut Index) -> Result<()> {
2151 self.write_index_at_with_post_index_change(path, index, false, false)
2152 }
2153
2154 pub(crate) fn write_index_at_with_split(
2156 &self,
2157 path: &std::path::Path,
2158 index: &mut Index,
2159 split: WriteSplitIndexRequest,
2160 ) -> Result<()> {
2161 self.write_index_at_split(path, index, split)
2162 }
2163
2164 pub(crate) fn write_index_at_with_post_index_change(
2179 &self,
2180 path: &std::path::Path,
2181 index: &mut Index,
2182 updated_workdir: bool,
2183 updated_skipworktree: bool,
2184 ) -> Result<()> {
2185 self.write_index_at_split_with_post_index_change(
2186 path,
2187 index,
2188 WriteSplitIndexRequest::default(),
2189 updated_workdir,
2190 updated_skipworktree,
2191 )
2192 }
2193
2194 pub(crate) fn write_index_at_split(
2196 &self,
2197 path: &std::path::Path,
2198 index: &mut Index,
2199 split: WriteSplitIndexRequest,
2200 ) -> Result<()> {
2201 self.write_index_at_split_with_post_index_change(path, index, split, false, false)
2202 }
2203
2204 pub(crate) fn write_index_at_split_with_post_index_change(
2217 &self,
2218 path: &std::path::Path,
2219 index: &mut Index,
2220 split: WriteSplitIndexRequest,
2221 updated_workdir: bool,
2222 updated_skipworktree: bool,
2223 ) -> Result<()> {
2224 self.write_index_file(
2225 path,
2226 index,
2227 split,
2228 updated_workdir,
2229 updated_skipworktree,
2230 None,
2231 )
2232 }
2233
2234 pub(crate) fn write_index_with_lock(
2243 &self,
2244 lock: &mut crate::index::IndexLock,
2245 index: &mut Index,
2246 ) -> Result<()> {
2247 let path = lock.index_path.clone();
2248 self.write_index_file(
2249 &path,
2250 index,
2251 WriteSplitIndexRequest::default(),
2252 false,
2253 false,
2254 Some(lock),
2255 )
2256 }
2257
2258 pub(crate) fn lock_and_load_index(&self) -> Result<(crate::index::IndexLock, Index)> {
2273 let path = self.index_path_for_env()?;
2274 let cfg = self.config()?;
2275 let lock = crate::index::IndexLock::acquire(&path, Some(cfg.as_ref()))?;
2276 let index = self.load_index_at(&path)?;
2277 Ok((lock, index))
2278 }
2279
2280 fn prepare_index_for_disk_write(
2282 &self,
2283 path: &std::path::Path,
2284 index: &mut Index,
2285 ) -> Result<(bool, Arc<ConfigSet>)> {
2286 test_inject_index_write_fail()?;
2287 index.hash_algo = self.odb.hash_algo();
2292 self.finalize_sparse_index_if_needed(index)?;
2293 let prev_index_mtime = crate::index::index_file_mtime(path);
2294 let cfg = self.config()?;
2295 if let Some(work_tree) = self.work_tree.as_deref() {
2296 if !index.suppress_pre_write_index_housekeeping {
2297 crate::untracked_cache::ensure_untracked_cache_shell(
2298 index,
2299 work_tree,
2300 cfg.as_ref(),
2301 );
2302 if !index.skip_racy_smudge_on_write {
2303 crate::diff::smudge_racily_clean_entries(
2304 &self.odb,
2305 &self.git_dir,
2306 index,
2307 work_tree,
2308 prev_index_mtime,
2309 Some(cfg.as_ref()),
2310 Some(self.caches().filters()),
2311 );
2312 }
2313 }
2314 }
2315 index.content_verified.clear();
2316 let skip_hash = crate::index::index_skip_hash_for_write(Some(cfg.as_ref()));
2317 Ok((skip_hash, cfg))
2318 }
2319
2320 fn write_index_file(
2321 &self,
2322 path: &std::path::Path,
2323 index: &mut Index,
2324 split: WriteSplitIndexRequest,
2325 updated_workdir: bool,
2326 updated_skipworktree: bool,
2327 existing_lock: Option<&mut crate::index::IndexLock>,
2328 ) -> Result<()> {
2329 crate::trace::trace_span!("grit.index.write");
2330 let (skip_hash, cfg) = self.prepare_index_for_disk_write(path, index)?;
2331 write_index_file_split(
2332 path,
2333 &self.git_dir,
2334 index,
2335 cfg.as_ref(),
2336 split,
2337 skip_hash,
2338 self.thread_policy(),
2339 existing_lock,
2340 true,
2341 )
2342 .map_err(|err| map_index_write_error(&self.git_dir, path, err))?;
2343 let updated_workdir_arg = if updated_workdir { "1" } else { "0" };
2345 let updated_skipworktree_arg = if updated_skipworktree { "1" } else { "0" };
2346 let _ = run_hook(
2347 self,
2348 "post-index-change",
2349 &[updated_workdir_arg, updated_skipworktree_arg],
2350 None,
2351 );
2352 Ok(())
2353 }
2354
2355 fn finalize_sparse_index_if_needed(&self, index: &mut Index) -> Result<()> {
2356 let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
2357 let sparse_enabled = cfg
2358 .get("core.sparseCheckout")
2359 .map(|v| v == "true")
2360 .unwrap_or(false);
2361 if !sparse_enabled {
2362 index.sparse_directories = false;
2363 return Ok(());
2364 }
2365 let cone_cfg = cfg
2366 .get("core.sparseCheckoutCone")
2367 .and_then(|v| v.parse::<bool>().ok())
2368 .unwrap_or(true);
2369 let sparse_ix = cfg
2370 .get("index.sparse")
2371 .map(|v| v == "true")
2372 .unwrap_or(false);
2373 let patterns = read_sparse_checkout_patterns(&self.git_dir);
2374 let cone = effective_cone_mode_for_sparse_file(cone_cfg, &patterns);
2375 let tree_oid = match index.cache_tree_root {
2378 Some(tree) => Some(tree),
2379 None => match resolve_head(&self.git_dir)?.oid() {
2380 Some(oid) => Some(parse_commit(&self.odb.read(oid)?.data)?.tree),
2381 None => None,
2382 },
2383 };
2384 if let Some(t) = tree_oid {
2385 let protect = crate::path_validate::ProtectOptions::from_config(cfg.as_ref());
2386 index.try_collapse_sparse_directories(
2387 &self.odb, &t, &patterns, cone, sparse_ix, protect,
2388 )?;
2389 } else {
2390 index.sparse_directories = false;
2391 }
2392 Ok(())
2393 }
2394
2395 #[must_use]
2400 pub(crate) fn bloom_pathspec_cwd(&self) -> Option<String> {
2401 let wt = self.work_tree.as_ref()?;
2402 let cwd = self.environment.discovery_cwd();
2403 let wt = wt.canonicalize().ok()?;
2404 let cwd = cwd.canonicalize().ok()?;
2405 let rel = cwd.strip_prefix(&wt).ok()?;
2406 let s = rel.to_string_lossy().replace('\\', "/");
2407 let s = s.trim_start_matches('/').to_string();
2408 Some(s)
2409 }
2410
2411 #[must_use]
2413 pub fn is_bare(&self) -> bool {
2414 if let Ok(cfg) = self.config() {
2415 if let Some(Ok(bare)) = cfg.get_bool("core.bare") {
2416 return bare;
2417 }
2418 }
2419 self.work_tree.is_none()
2420 }
2421
2422 pub(crate) fn replace_ref_base_if_enabled(&self) -> Option<String> {
2426 if self.environment.git_no_replace_objects {
2427 return None;
2428 }
2429 let settings = self.cached_settings();
2430 settings
2431 .use_replace_refs
2432 .then_some(settings.replace_ref_base)
2433 }
2434
2435 pub(crate) fn object_id_after_replace(
2437 &self,
2438 oid: &crate::objects::ObjectId,
2439 ) -> crate::objects::ObjectId {
2440 self.resolve_replace_redirect(oid).unwrap_or(*oid)
2441 }
2442
2443 pub(crate) fn read_info_replaced(
2445 &self,
2446 oid: &crate::objects::ObjectId,
2447 ) -> Result<crate::objects::ObjectInfo> {
2448 let effective = self.object_id_after_replace(oid);
2449 self.odb.read_info(&effective)
2450 }
2451
2452 pub(crate) fn read_replaced(
2459 &self,
2460 oid: &crate::objects::ObjectId,
2461 ) -> Result<crate::objects::Object> {
2462 let effective = self.object_id_after_replace(oid);
2463 self.odb.read(&effective)
2464 }
2465
2466 fn resolve_replace_redirect(
2467 &self,
2468 oid: &crate::objects::ObjectId,
2469 ) -> Option<crate::objects::ObjectId> {
2470 if self.environment.git_no_replace_objects {
2471 return None;
2472 }
2473 if let Ok(guard) = self.config_session.lock() {
2474 if guard.as_ref().is_some_and(|s| s.replace_lookup_inactive) {
2475 return None;
2476 }
2477 }
2478 let settings = self.cached_settings();
2479 if !settings.use_replace_refs {
2480 return None;
2481 }
2482 let replace_ref =
2483 self.common_git_dir
2484 .join(format!("{}{}", settings.replace_ref_base, oid.to_hex()));
2485 if !replace_ref.is_file() {
2486 return None;
2487 }
2488 let content = std::fs::read_to_string(&replace_ref).ok()?;
2489 let hex = content.trim();
2490 let replacement_oid = hex.parse::<crate::objects::ObjectId>().ok()?;
2491 if self.odb.read_info(&replacement_oid).is_ok() {
2492 Some(replacement_oid)
2493 } else {
2494 None
2495 }
2496 }
2497}
2498
2499pub(crate) fn trace_repo_setup_if_requested(repo: &Repository) -> std::io::Result<()> {
2504 let Some(path) = repo.environment.git_trace_setup.as_deref() else {
2505 return Ok(());
2506 };
2507 if path.is_empty() || path == "0" {
2508 return Ok(());
2509 }
2510 let trace_path = Path::new(path);
2511 if !trace_path.is_absolute() {
2512 return Ok(());
2513 }
2514
2515 let actual_cwd = repo.environment.discovery_cwd();
2516 let actual_cwd = actual_cwd
2517 .canonicalize()
2518 .unwrap_or_else(|_| actual_cwd.clone());
2519
2520 let (trace_cwd, prefix) = if let Some(ref wt) = repo.work_tree {
2523 let wt_canon = wt.canonicalize().unwrap_or_else(|_| wt.clone());
2524 if actual_cwd.starts_with(&wt_canon) {
2525 let rel = actual_cwd
2526 .strip_prefix(&wt_canon)
2527 .map(|p| p.to_path_buf())
2528 .unwrap_or_default();
2529 let prefix = if rel.as_os_str().is_empty() {
2530 "(null)".to_owned()
2531 } else {
2532 let mut s = rel.to_string_lossy().replace('\\', "/");
2533 if !s.ends_with('/') {
2534 s.push('/');
2535 }
2536 s
2537 };
2538 (wt_canon, prefix)
2539 } else {
2540 (actual_cwd.clone(), "(null)".to_owned())
2541 }
2542 } else {
2543 (actual_cwd.clone(), "(null)".to_owned())
2544 };
2545
2546 let git_dir_display =
2547 display_git_dir_for_setup_trace(repo, &trace_cwd, &actual_cwd, prefix.as_str());
2548 let common_display = display_common_dir_for_setup_trace(
2549 repo,
2550 &trace_cwd,
2551 &actual_cwd,
2552 prefix.as_str(),
2553 &git_dir_display,
2554 );
2555 let worktree_display = repo
2556 .work_tree
2557 .as_ref()
2558 .map(|p| {
2559 p.canonicalize()
2560 .unwrap_or_else(|_| lexical_normalize_path(p))
2561 .display()
2562 .to_string()
2563 })
2564 .unwrap_or_else(|| "(null)".to_owned());
2565
2566 let mut f = OpenOptions::new()
2567 .create(true)
2568 .append(true)
2569 .open(trace_path)?;
2570 writeln!(f, "setup: git_dir: {git_dir_display}")?;
2571 writeln!(f, "setup: git_common_dir: {common_display}")?;
2572 writeln!(f, "setup: worktree: {worktree_display}")?;
2573 writeln!(f, "setup: cwd: {}", trace_cwd.display())?;
2574 writeln!(f, "setup: prefix: {prefix}")?;
2575 Ok(())
2576}
2577
2578fn lexical_normalize_path(path: &Path) -> PathBuf {
2580 let mut out = PathBuf::new();
2581 let mut absolute = false;
2582 for c in path.components() {
2583 match c {
2584 Component::Prefix(p) => {
2585 out.push(p.as_os_str());
2586 }
2587 Component::RootDir => {
2588 absolute = true;
2589 out.push(c.as_os_str());
2590 }
2591 Component::CurDir => {}
2592 Component::ParentDir => {
2593 if absolute {
2594 let _ = out.pop();
2595 } else if !out.pop() {
2596 out.push("..");
2597 }
2598 }
2599 Component::Normal(s) => out.push(s),
2600 }
2601 }
2602 if out.as_os_str().is_empty() {
2603 PathBuf::from(".")
2604 } else {
2605 out
2606 }
2607}
2608
2609fn path_relative_to(target: &Path, base: &Path) -> Option<PathBuf> {
2611 let t = target.canonicalize().ok()?;
2612 let b = base.canonicalize().ok()?;
2613 let tc: Vec<_> = t.components().collect();
2614 let bc: Vec<_> = b.components().collect();
2615 let mut i = 0usize;
2616 while i < tc.len() && i < bc.len() && tc[i] == bc[i] {
2617 i += 1;
2618 }
2619 let up = bc.len().saturating_sub(i);
2620 let mut out = PathBuf::new();
2621 for _ in 0..up {
2622 out.push("..");
2623 }
2624 for comp in &tc[i..] {
2625 out.push(comp.as_os_str());
2626 }
2627 Some(out)
2628}
2629
2630fn rel_path_for_setup_trace(target: &Path, trace_cwd: &Path) -> String {
2631 let t = target
2632 .canonicalize()
2633 .unwrap_or_else(|_| target.to_path_buf());
2634 let tc = trace_cwd
2635 .canonicalize()
2636 .unwrap_or_else(|_| trace_cwd.to_path_buf());
2637 if let Some(rel) = path_relative_to(&t, &tc) {
2638 let s = rel.to_string_lossy().replace('\\', "/");
2639 return if s.is_empty() || s == "." {
2640 ".".to_owned()
2641 } else {
2642 s
2643 };
2644 }
2645 t.display().to_string()
2646}
2647
2648fn trace_cwd_strictly_inside_git_parent(trace_cwd: &Path, git_dir: &Path) -> bool {
2649 let tc = trace_cwd
2650 .canonicalize()
2651 .unwrap_or_else(|_| trace_cwd.to_path_buf());
2652 let gd = git_dir
2653 .canonicalize()
2654 .unwrap_or_else(|_| git_dir.to_path_buf());
2655 let Some(parent) = gd.parent() else {
2656 return false;
2657 };
2658 let parent = parent.to_path_buf();
2659 if tc == parent {
2660 return false;
2661 }
2662 tc.starts_with(&parent) && tc != parent
2663}
2664
2665fn display_git_dir_for_setup_trace(
2666 repo: &Repository,
2667 trace_cwd: &Path,
2668 actual_cwd: &Path,
2669 setup_prefix: &str,
2670) -> String {
2671 let gd = repo
2672 .git_dir
2673 .canonicalize()
2674 .unwrap_or_else(|_| repo.git_dir.clone());
2675 let tc = trace_cwd
2676 .canonicalize()
2677 .unwrap_or_else(|_| trace_cwd.to_path_buf());
2678 let ac = actual_cwd
2679 .canonicalize()
2680 .unwrap_or_else(|_| actual_cwd.to_path_buf());
2681
2682 if repo.work_tree.is_none() && !repo.explicit_git_dir {
2685 if ac == gd {
2686 return ".".to_owned();
2687 }
2688 if ac.starts_with(&gd) && ac != gd {
2689 return gd.display().to_string();
2690 }
2691 }
2692
2693 if !repo.explicit_git_dir {
2695 if let Some(wt) = &repo.work_tree {
2696 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
2697 if ac.starts_with(&gd) && ac != wt {
2698 return gd.display().to_string();
2699 }
2700 }
2701 }
2702
2703 if repo.explicit_git_dir {
2707 if repo.work_tree.is_none() {
2708 if let Some(raw) = repo.environment.git_dir.as_deref() {
2709 let p = Path::new(raw.trim());
2710 if p.is_absolute() {
2711 return gd.display().to_string();
2712 }
2713 let joined = ac.join(p);
2714 if joined.is_file() {
2715 return gd.display().to_string();
2716 }
2717 if let Some(rel) = path_relative_to(&gd, &tc) {
2718 let s = rel.to_string_lossy().replace('\\', "/");
2719 return if s.is_empty() || s == "." {
2720 ".".to_owned()
2721 } else {
2722 s
2723 };
2724 }
2725 }
2726 return gd.display().to_string();
2727 }
2728 if let Some(wt) = &repo.work_tree {
2729 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
2730 let strictly_inside_wt = ac.starts_with(&wt) && ac != wt;
2731 if strictly_inside_wt {
2732 return gd.display().to_string();
2733 }
2734 if let Some(raw) = repo.environment.git_dir.as_deref() {
2735 let p = Path::new(raw.trim());
2736 if p.is_relative() {
2737 let joined = ac.join(p);
2738 if joined.is_file() {
2739 return gd.display().to_string();
2741 }
2742 if let Some(rel) = path_relative_to(&gd, &tc) {
2743 let s = rel.to_string_lossy().replace('\\', "/");
2744 return if s.is_empty() || s == "." {
2745 ".".to_owned()
2746 } else {
2747 s
2748 };
2749 }
2750 }
2751 return gd.display().to_string();
2752 }
2753 }
2754 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
2755 return rel_path_for_setup_trace(&gd, trace_cwd);
2756 }
2757 return gd.display().to_string();
2758 }
2759
2760 let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
2761 (Some(root), Some(wt)) if !repo.work_tree_from_env => {
2762 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
2763 let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
2764 r != w
2765 }
2766 _ => false,
2767 };
2768
2769 if repo.work_tree_from_env {
2770 if !repo.discovery_via_gitfile {
2771 if setup_prefix == "(null)" {
2772 if let (Some(root), Some(wt)) = (&repo.discovery_root, &repo.work_tree) {
2773 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
2774 let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
2775 if r == w {
2776 let dot_git = r.join(".git");
2777 let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
2778 if gd == dot_git {
2779 return ".git".to_owned();
2780 }
2781 }
2782 }
2783 }
2784 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
2785 return rel_path_for_setup_trace(&gd, trace_cwd);
2786 }
2787 }
2788 return gd.display().to_string();
2789 }
2790
2791 if work_relocated {
2792 if let Some(wt) = &repo.work_tree {
2793 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
2794 if ac == wt {
2795 return gd.display().to_string();
2796 }
2797 let inside_wt = ac.starts_with(&wt) && ac != wt;
2798 if inside_wt {
2799 if let Some(rel) = path_relative_to(&gd, &ac) {
2800 let s = rel.to_string_lossy().replace('\\', "/");
2801 return if s.is_empty() || s == "." {
2802 ".".to_owned()
2803 } else {
2804 s
2805 };
2806 }
2807 }
2808 }
2809 }
2810 if repo.work_tree.is_some() {
2811 if let Some(root) = &repo.discovery_root {
2812 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
2813 let dot_git = r.join(".git");
2814 let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
2815 if gd == dot_git {
2816 return ".git".to_owned();
2817 }
2818 } else if let Some(wt) = &repo.work_tree {
2819 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
2820 let dot_git = wt.join(".git");
2821 let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
2822 if gd == dot_git {
2823 return ".git".to_owned();
2824 }
2825 }
2826 }
2827
2828 if repo.discovery_via_gitfile && !repo.explicit_git_dir {
2829 return gd.display().to_string();
2830 }
2831
2832 if repo.work_tree.is_none() && !repo.explicit_git_dir {
2836 if let Some(gp) = gd.parent() {
2837 let gp = gp.canonicalize().unwrap_or_else(|_| gp.to_path_buf());
2838 let gdc = gd.canonicalize().unwrap_or_else(|_| gd.clone());
2839 if tc.starts_with(&gp) && tc != gp && !tc.starts_with(&gdc) {
2840 return gdc.display().to_string();
2841 }
2842 if tc == gp {
2843 return rel_path_for_setup_trace(&gd, trace_cwd);
2844 }
2845 }
2846 }
2847
2848 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
2849 rel_path_for_setup_trace(&gd, trace_cwd)
2850 } else {
2851 gd.display().to_string()
2852 }
2853}
2854
2855fn display_common_dir_for_setup_trace(
2856 repo: &Repository,
2857 trace_cwd: &Path,
2858 actual_cwd: &Path,
2859 _setup_prefix: &str,
2860 git_dir_display: &str,
2861) -> String {
2862 let gd = repo
2863 .git_dir
2864 .canonicalize()
2865 .unwrap_or_else(|_| repo.git_dir.clone());
2866 let Some(common) = resolve_common_dir(&gd) else {
2867 return git_dir_display.to_owned();
2868 };
2869 let common = common.canonicalize().unwrap_or(common);
2870 if common == gd {
2871 return git_dir_display.to_owned();
2872 }
2873
2874 let ac = actual_cwd
2875 .canonicalize()
2876 .unwrap_or_else(|_| actual_cwd.to_path_buf());
2877 if repo.work_tree.is_none() && !repo.explicit_git_dir {
2878 if ac == common {
2879 return ".".to_owned();
2880 }
2881 if ac.starts_with(&common) && ac != common {
2882 return common.display().to_string();
2883 }
2884 }
2885
2886 let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
2887 (Some(root), Some(wt)) if !repo.work_tree_from_env => {
2888 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
2889 let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
2890 r != w
2891 }
2892 _ => false,
2893 };
2894 if work_relocated {
2895 if let Some(wt) = &repo.work_tree {
2896 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
2897 if ac == wt {
2898 return common.display().to_string();
2899 }
2900 let inside_wt = ac.starts_with(&wt) && ac != wt;
2901 if inside_wt {
2902 if let Some(rel) = path_relative_to(&common, &ac) {
2903 let s = rel.to_string_lossy().replace('\\', "/");
2904 return if s.is_empty() || s == "." {
2905 ".".to_owned()
2906 } else {
2907 s
2908 };
2909 }
2910 }
2911 }
2912 }
2913
2914 if repo.discovery_via_gitfile && !repo.explicit_git_dir {
2915 return common.display().to_string();
2916 }
2917
2918 if repo.work_tree.is_none() && !repo.explicit_git_dir {
2919 let tc = trace_cwd
2920 .canonicalize()
2921 .unwrap_or_else(|_| trace_cwd.to_path_buf());
2922 if let Some(cp) = common.parent() {
2923 let cp = cp.canonicalize().unwrap_or_else(|_| cp.to_path_buf());
2924 let comc = common.canonicalize().unwrap_or_else(|_| common.clone());
2925 if tc.starts_with(&cp) && tc != cp && !tc.starts_with(&comc) {
2926 return comc.display().to_string();
2927 }
2928 if tc == cp {
2929 return rel_path_for_setup_trace(&common, trace_cwd);
2930 }
2931 }
2932 }
2933
2934 if trace_cwd_strictly_inside_git_parent(trace_cwd, &common) {
2935 rel_path_for_setup_trace(&common, trace_cwd)
2936 } else {
2937 common.display().to_string()
2938 }
2939}
2940
2941fn replace_dir_is_non_empty(replace_dir: &Path) -> bool {
2943 let Ok(entries) = fs::read_dir(replace_dir) else {
2944 return false;
2945 };
2946 entries.filter_map(|e| e.ok()).any(|e| {
2947 let Ok(ft) = e.file_type() else {
2948 return false;
2949 };
2950 ft.is_file()
2951 })
2952}
2953
2954fn resolve_common_dir(git_dir: &Path) -> Option<PathBuf> {
2956 let commondir_file = git_dir.join("commondir");
2957 if !commondir_file.is_file() {
2958 return None;
2959 }
2960 let common_raw = fs::read_to_string(commondir_file).ok()?;
2961 let common_rel = common_raw.trim();
2962 if common_rel.is_empty() {
2963 return None;
2964 }
2965 let common_dir = if Path::new(common_rel).is_absolute() {
2966 PathBuf::from(common_rel)
2967 } else {
2968 git_dir.join(common_rel)
2969 };
2970 Some(common_dir.canonicalize().unwrap_or(common_dir))
2971}
2972
2973#[must_use]
2975pub fn common_git_dir_for_config(git_dir: &Path) -> PathBuf {
2976 resolve_common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf())
2977}
2978
2979pub fn worktree_config_enabled(common_dir: &Path) -> bool {
2981 let path = common_dir.join("config");
2982 let Ok(content) = crate::repo_caches::read_local_config_file(&path) else {
2983 return false;
2984 };
2985 worktree_config_enabled_for_text(Some(content.as_str()))
2986}
2987
2988pub fn worktree_config_enabled_for_text(content: Option<&str>) -> bool {
2990 let Some(content) = content else {
2991 return false;
2992 };
2993 let mut in_extensions = false;
2994 for raw_line in content.lines() {
2995 let mut line = raw_line.trim();
2996 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
2997 continue;
2998 }
2999 if line.starts_with('[') {
3000 let Some(end_idx) = line.find(']') else {
3001 continue;
3002 };
3003 let section = line[1..end_idx].trim();
3004 let section_name = section
3005 .split_whitespace()
3006 .next()
3007 .unwrap_or_default()
3008 .to_ascii_lowercase();
3009 in_extensions = section_name == "extensions";
3010 let remainder = line[end_idx + 1..].trim();
3011 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
3012 continue;
3013 }
3014 line = remainder;
3015 }
3016 if in_extensions {
3017 let Some((key, value)) = line.split_once('=') else {
3018 continue;
3019 };
3020 if key.trim().eq_ignore_ascii_case("worktreeconfig") {
3021 let v = value.trim();
3022 return v.eq_ignore_ascii_case("true")
3023 || v.eq_ignore_ascii_case("yes")
3024 || v.eq_ignore_ascii_case("on")
3025 || v == "1";
3026 }
3027 }
3028 }
3029 false
3030}
3031
3032fn open_or_create_config_file(path: &Path, scope: ConfigScope) -> Result<ConfigFile> {
3033 match ConfigFile::from_path(path, scope)? {
3034 Some(f) => Ok(f),
3035 None => {
3036 if let Some(parent) = path.parent() {
3037 fs::create_dir_all(parent).map_err(Error::Io)?;
3038 }
3039 ConfigFile::parse(path, "", scope)
3040 }
3041 }
3042}
3043
3044fn config_file_bool_true(cfg: &ConfigFile, key: &str) -> bool {
3045 cfg.get(key).is_some_and(|v| {
3046 matches!(
3047 v.trim().to_ascii_lowercase().as_str(),
3048 "true" | "yes" | "on" | "1"
3049 )
3050 })
3051}
3052
3053pub fn init_worktree_config(git_dir: &Path) -> Result<()> {
3063 let common_dir = common_git_dir_for_config(git_dir);
3064 let common_config_path = common_dir.join("config");
3065 let worktree_config_path = git_dir.join("config.worktree");
3066
3067 if worktree_config_enabled(&common_dir) {
3068 if !worktree_config_path.exists() {
3069 if let Some(parent) = worktree_config_path.parent() {
3070 fs::create_dir_all(parent).map_err(Error::Io)?;
3071 }
3072 fs::write(&worktree_config_path, "").map_err(Error::Io)?;
3073 }
3074 return Ok(());
3075 }
3076
3077 let mut common_cfg = open_or_create_config_file(&common_config_path, ConfigScope::Local)?;
3078 common_cfg.set("extensions.worktreeConfig", "true")?;
3079
3080 let mut wt_cfg = open_or_create_config_file(&worktree_config_path, ConfigScope::Worktree)?;
3081
3082 if config_file_bool_true(&common_cfg, "core.bare") {
3083 wt_cfg.set("core.bare", "true")?;
3084 common_cfg.unset("core.bare")?;
3085 }
3086 if let Some(worktree) = common_cfg.get("core.worktree") {
3087 wt_cfg.set("core.worktree", &worktree)?;
3088 common_cfg.unset("core.worktree")?;
3089 }
3090
3091 common_cfg.write()?;
3092 wt_cfg.write()?;
3093 Ok(())
3094}
3095
3096pub fn early_config_ignore_repo_reason(common_dir: &Path) -> Option<String> {
3100 let path = common_dir.join("config");
3101 let content = crate::repo_caches::read_local_config_file(&path).ok()?;
3102 early_config_ignore_repo_reason_for_text(Some(content.as_str()))
3103}
3104
3105pub fn early_config_ignore_repo_reason_for_text(content: Option<&str>) -> Option<String> {
3107 const GIT_REPO_VERSION_READ: u32 = 1;
3108 let content = content?;
3109 let mut version = 0u32;
3110 let mut in_core = false;
3111 for raw_line in content.lines() {
3112 let mut line = raw_line.trim();
3113 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
3114 continue;
3115 }
3116 if line.starts_with('[') {
3117 let Some(end_idx) = line.find(']') else {
3118 continue;
3119 };
3120 let section = line[1..end_idx].trim();
3121 let section_name = section
3122 .split_whitespace()
3123 .next()
3124 .unwrap_or_default()
3125 .to_ascii_lowercase();
3126 in_core = section_name == "core";
3127 let remainder = line[end_idx + 1..].trim();
3128 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
3129 continue;
3130 }
3131 line = remainder;
3132 }
3133 if in_core {
3134 if let Some((key, value)) = line.split_once('=') {
3135 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
3136 if let Ok(v) = value.trim().parse::<u32>() {
3137 version = v;
3138 }
3139 }
3140 }
3141 }
3142 }
3143 if version > GIT_REPO_VERSION_READ {
3144 Some(format!(
3145 "Expected git repo version <= {GIT_REPO_VERSION_READ}, found {version}"
3146 ))
3147 } else {
3148 None
3149 }
3150}
3151
3152fn path_for_ceiling_compare(path: &Path) -> String {
3153 let path = path.to_string_lossy();
3154 #[cfg(windows)]
3155 {
3156 path.replace('\\', "/")
3157 }
3158 #[cfg(not(windows))]
3159 {
3160 path.into_owned()
3161 }
3162}
3163
3164fn offset_1st_component(path: &str) -> usize {
3165 if path.starts_with('/') {
3166 1
3167 } else {
3168 0
3169 }
3170}
3171
3172fn longest_ancestor_length(path: &str, ceilings: &[String]) -> Option<usize> {
3174 if path == "/" {
3175 return None;
3176 }
3177 let mut max_len: Option<usize> = None;
3178 for ceil in ceilings {
3179 let mut len = ceil.len();
3180 while len > 0 && ceil.as_bytes().get(len - 1) == Some(&b'/') {
3181 len -= 1;
3182 }
3183 if len == 0 {
3184 continue;
3185 }
3186 if path.len() <= len + 1 {
3187 continue;
3188 }
3189 if !path.starts_with(&ceil[..len]) {
3190 continue;
3191 }
3192 if path.as_bytes().get(len) != Some(&b'/') {
3193 continue;
3194 }
3195 if path.as_bytes().get(len + 1).is_none() {
3196 continue;
3197 }
3198 max_len = Some(max_len.map_or(len, |m| m.max(len)));
3199 }
3200 max_len
3201}
3202
3203pub fn local_config_write_path(git_dir: &Path) -> PathBuf {
3208 if let Some(path) = repository_config_path(git_dir) {
3209 return path;
3210 }
3211 if let Some(common) = resolve_common_dir(git_dir) {
3212 return common.join("config");
3213 }
3214 git_dir.join("config")
3215}
3216
3217fn repository_config_path(git_dir: &Path) -> Option<PathBuf> {
3219 repository_config_path_for_caches(git_dir)
3220}
3221
3222pub(crate) fn repository_config_path_for_caches(git_dir: &Path) -> Option<PathBuf> {
3224 let local = git_dir.join("config");
3225 if local.is_file() {
3226 return Some(local);
3227 }
3228 let commondir = git_dir.join("commondir");
3229 if !commondir.is_file() {
3230 return None;
3231 }
3232 let common = resolve_common_dir(git_dir)?;
3233 let shared = common.join("config");
3234 if shared.exists() {
3235 Some(shared)
3236 } else {
3237 None
3238 }
3239}
3240
3241pub fn validate_repo_format(git_dir: &Path) -> Result<()> {
3247 validate_repository_format(git_dir)
3248}
3249
3250pub(crate) fn validate_repository_format_parsed(parsed: &RepositoryFormat) -> Result<()> {
3252 if parsed.repo_version > 1 {
3253 return Err(Error::UnsupportedRepositoryFormatVersion(
3254 parsed.repo_version,
3255 ));
3256 }
3257
3258 if let Some(raw) = parsed.ref_storage.as_deref() {
3259 crate::ref_storage::RefStorageFormat::validate_config_extension(raw)?;
3260 }
3261
3262 if let Some(msg) = parsed.format_error_message() {
3263 return Err(Error::Message(msg));
3264 }
3265
3266 Ok(())
3267}
3268
3269fn validate_repository_format(git_dir: &Path) -> Result<()> {
3270 let Some(config_path) = repository_config_path(git_dir) else {
3271 return Ok(());
3272 };
3273 let content = crate::repo_caches::read_local_config_file(&config_path)?;
3274 let parsed = parse_repository_format(&content, &config_path)?;
3275 validate_repository_format_parsed(&parsed)
3276}
3277
3278#[derive(Clone)]
3281pub(crate) struct RepositoryFormat {
3282 pub(crate) repo_version: u32,
3284 pub(crate) extensions: BTreeSet<String>,
3286 pub(crate) ref_storage: Option<String>,
3288 pub(crate) object_format: Option<String>,
3290}
3291
3292pub(crate) fn read_repository_format_from_git_dir(git_dir: &Path) -> Result<RepositoryFormat> {
3298 let Some(config_path) = repository_config_path(git_dir) else {
3299 return Ok(RepositoryFormat {
3300 repo_version: 0,
3301 extensions: BTreeSet::new(),
3302 ref_storage: None,
3303 object_format: None,
3304 });
3305 };
3306 let content = crate::repo_caches::read_local_config_file(&config_path)?;
3307 parse_repository_format(&content, &config_path)
3308}
3309
3310impl RepositoryFormat {
3311 pub(crate) fn hash_algo(&self) -> crate::objects::HashAlgo {
3314 self.object_format
3315 .as_deref()
3316 .and_then(crate::objects::HashAlgo::from_name)
3317 .unwrap_or(crate::objects::HashAlgo::Sha1)
3318 }
3319
3320 fn format_error_message(&self) -> Option<String> {
3327 let mut v1_only_found: Vec<&str> = Vec::new();
3334 let mut unknown_found: Vec<&str> = Vec::new();
3335 for extension in &self.extensions {
3336 match extension.as_str() {
3337 "noop" | "preciousobjects" | "partialclone" | "worktreeconfig" => {}
3339 "noop-v1"
3341 | "objectformat"
3342 | "compatobjectformat"
3343 | "refstorage"
3344 | "relativeworktrees"
3345 | "submodulepathconfig" => {
3346 if self.repo_version == 0 {
3347 v1_only_found.push(extension);
3348 }
3349 }
3350 _ => {
3352 if self.repo_version >= 1 {
3353 unknown_found.push(extension);
3354 }
3355 }
3356 }
3357 }
3358
3359 if !unknown_found.is_empty() {
3360 let mut msg = if unknown_found.len() == 1 {
3361 "unknown repository extension found:".to_owned()
3362 } else {
3363 "unknown repository extensions found:".to_owned()
3364 };
3365 for ext in &unknown_found {
3366 msg.push_str(&format!("\n\t{ext}"));
3367 }
3368 return Some(msg);
3369 }
3370
3371 if !v1_only_found.is_empty() {
3372 let mut msg = if v1_only_found.len() == 1 {
3373 "repo version is 0, but v1-only extension found:".to_owned()
3374 } else {
3375 "repo version is 0, but v1-only extensions found:".to_owned()
3376 };
3377 for ext in &v1_only_found {
3378 msg.push_str(&format!("\n\t{ext}"));
3379 }
3380 return Some(msg);
3381 }
3382
3383 None
3384 }
3385}
3386
3387pub(crate) fn parse_repository_format(
3394 content: &str,
3395 config_path: &Path,
3396) -> Result<RepositoryFormat> {
3397 let mut in_core = false;
3398 let mut in_extensions = false;
3399 let mut repo_version = 0u32;
3400 let mut extensions = BTreeSet::new();
3401 let mut ref_storage: Option<String> = None;
3402 let mut object_format: Option<String> = None;
3403
3404 for raw_line in content.lines() {
3405 let mut line = raw_line.trim();
3406 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
3407 continue;
3408 }
3409
3410 if line.starts_with('[') {
3411 let Some(end_idx) = line.find(']') else {
3412 return Err(Error::Config(
3413 format!("invalid config in {}", config_path.display()).into(),
3414 ));
3415 };
3416
3417 let section = line[1..end_idx].trim();
3418 let section_name = section
3419 .split_whitespace()
3420 .next()
3421 .unwrap_or_default()
3422 .to_ascii_lowercase();
3423 in_core = section_name == "core";
3424 in_extensions = section_name == "extensions";
3425
3426 let remainder = line[end_idx + 1..].trim();
3427 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
3428 continue;
3429 }
3430 line = remainder;
3431 }
3432
3433 if in_core {
3434 if let Some((key, value)) = line.split_once('=') {
3435 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
3436 if let Ok(v) = value.trim().parse::<u32>() {
3438 repo_version = v;
3439 }
3440 }
3441 }
3442 }
3443
3444 if in_extensions {
3445 let (key, value) = if let Some((key, value)) = line.split_once('=') {
3446 (key.trim(), Some(value.trim()))
3447 } else {
3448 (line, None)
3449 };
3450 if key.eq_ignore_ascii_case("refstorage") {
3451 ref_storage = value.map(str::to_owned);
3452 }
3453 if key.eq_ignore_ascii_case("objectformat") {
3454 object_format = value.map(str::to_owned);
3455 }
3456 if !key.is_empty() {
3457 extensions.insert(key.to_ascii_lowercase());
3458 }
3459 }
3460 }
3461
3462 Ok(RepositoryFormat {
3463 repo_version,
3464 extensions,
3465 ref_storage,
3466 object_format,
3467 })
3468}
3469
3470pub fn repository_format_warning(git_dir: &Path) -> Result<Option<String>> {
3487 const GIT_REPO_VERSION_READ: u32 = 1;
3488 let Some(config_path) = repository_config_path(git_dir) else {
3489 return Ok(None);
3490 };
3491 let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
3492 let parsed = parse_repository_format(&content, &config_path)?;
3493
3494 if parsed.repo_version > GIT_REPO_VERSION_READ {
3495 return Ok(Some(format!(
3496 "Expected git repo version <= {GIT_REPO_VERSION_READ}, found {}",
3497 parsed.repo_version
3498 )));
3499 }
3500
3501 Ok(parsed.format_error_message())
3502}
3503
3504struct DiscoveredAt {
3510 repo: Repository,
3511 gitfile: Option<PathBuf>,
3513}
3514
3515fn try_open_at(options: &RepositoryOptions, dir: &Path) -> Result<Option<DiscoveredAt>> {
3516 let env = &options.environment;
3517 let dot_git = dir.join(".git");
3518
3519 #[cfg(unix)]
3522 {
3523 use std::os::unix::fs::FileTypeExt;
3524 if let Ok(meta) = fs::symlink_metadata(&dot_git) {
3525 let ft = meta.file_type();
3526 if ft.is_fifo() || ft.is_socket() || ft.is_block_device() || ft.is_char_device() {
3527 return Err(Error::NotARepository(format!(
3528 "invalid gitfile format: {} is not a regular file",
3529 path_for_error_message(&dot_git)
3530 )));
3531 }
3532 if ft.is_symlink() {
3533 if let Ok(target_meta) = fs::metadata(&dot_git) {
3534 let tft = target_meta.file_type();
3535 if tft.is_fifo()
3536 || tft.is_socket()
3537 || tft.is_block_device()
3538 || tft.is_char_device()
3539 {
3540 return Err(Error::NotARepository(format!(
3541 "invalid gitfile format: {} is not a regular file",
3542 path_for_error_message(&dot_git)
3543 )));
3544 }
3545 }
3546 }
3547 }
3548 }
3549
3550 if dot_git.is_file() {
3551 let content =
3553 fs::read_to_string(&dot_git).map_err(|e| Error::NotARepository(e.to_string()))?;
3554 let git_dir = parse_gitfile(&content, dir)?;
3555 let mut opts = options.clone();
3556 opts.validate_repository_format = false;
3557 opts.work_tree = Some(dir.to_path_buf());
3558 let mut repo = Repository::open_internal(&opts, &git_dir)?;
3559 if resolve_common_dir(&git_dir).is_some() {
3563 let cwd = env.discovery_cwd();
3564 if repo.work_tree.is_some() && !is_inside_work_tree(&repo, &cwd) {
3565 let root = if dir.is_absolute() {
3566 dir.to_path_buf()
3567 } else {
3568 cwd.join(dir)
3569 };
3570 repo.work_tree = Some(root.canonicalize().unwrap_or(root));
3571 }
3572 }
3573 let root = if dir.is_absolute() {
3574 dir.to_path_buf()
3575 } else {
3576 env.discovery_cwd().join(dir)
3577 };
3578 repo.discovery_root = Some(root.canonicalize().unwrap_or(root));
3579 repo.discovery_via_gitfile = true;
3580 return Ok(Some(DiscoveredAt {
3581 repo,
3582 gitfile: Some(dot_git.clone()),
3583 }));
3584 }
3585
3586 if dot_git.is_dir() {
3587 let open_path = if dot_git.is_symlink() {
3591 dot_git.read_link().unwrap_or_else(|_| dot_git.clone())
3593 } else {
3594 dot_git.clone()
3595 };
3596 let mut skip_opts = options.clone();
3599 skip_opts.validate_repository_format = false;
3600 skip_opts.work_tree = Some(dir.to_path_buf());
3601 match Repository::open_internal(&skip_opts, &open_path) {
3602 Ok(mut repo) => {
3603 if dot_git.is_symlink() {
3606 let abs_dot_git = if dot_git.is_absolute() {
3607 dot_git
3608 } else {
3609 dir.join(".git")
3610 };
3611 repo.git_dir = abs_dot_git;
3612 }
3613 let root = if dir.is_absolute() {
3614 dir.to_path_buf()
3615 } else {
3616 env.discovery_cwd().join(dir)
3617 };
3618 repo.discovery_root = Some(root.canonicalize().unwrap_or(root));
3619 repo.discovery_via_gitfile = false;
3620 return Ok(Some(DiscoveredAt {
3621 repo,
3622 gitfile: None,
3623 }));
3624 }
3625 Err(Error::NotARepository(_)) | Err(Error::Config(_)) => return Ok(None),
3626 Err(e) => return Err(e),
3627 }
3628 }
3629
3630 if dir.join("HEAD").is_file() && dir.join("commondir").is_file() {
3633 maybe_trace_implicit_bare_repository(dir, env);
3634 let repo = Repository::open_with_work_tree(options, dir, None)?;
3635 return Ok(Some(DiscoveredAt {
3636 repo,
3637 gitfile: None,
3638 }));
3639 }
3640
3641 if dir.join("objects").is_dir() && dir.join("HEAD").is_file() {
3643 maybe_trace_implicit_bare_repository(dir, env);
3644 if !is_inside_dot_git(dir) {
3648 if let Ok(cfg) = crate::config::ConfigSet::load(env, None, true) {
3649 if let Some(val) = cfg.get("safe.bareRepository") {
3650 if val.eq_ignore_ascii_case("explicit") {
3651 return Err(Error::ForbiddenBareRepository(path_for_error_message(dir)));
3652 }
3653 }
3654 }
3655 }
3656 let repo = Repository::open_with_work_tree(options, dir, None)?;
3657 return Ok(Some(DiscoveredAt {
3658 repo,
3659 gitfile: None,
3660 }));
3661 }
3662
3663 Ok(None)
3664}
3665
3666fn is_inside_dot_git(path: &Path) -> bool {
3667 path.components().any(|c| c.as_os_str() == ".git")
3668}
3669
3670fn maybe_trace_implicit_bare_repository(dir: &Path, environment: &Environment) {
3671 let Some(path) = environment
3672 .git_trace2_perf
3673 .as_deref()
3674 .filter(|p| !p.is_empty())
3675 else {
3676 return;
3677 };
3678
3679 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
3680 let _ = writeln!(file, "setup: implicit-bare-repository:{}", dir.display());
3681 }
3682}
3683
3684fn safe_directory_effective_values(git_dir: &Path, env: &Environment) -> Vec<String> {
3687 let cfg = crate::config::ConfigSet::load(env, Some(git_dir), true)
3688 .unwrap_or_else(|_| crate::config::ConfigSet::new());
3689 let mut values: Vec<String> = Vec::new();
3690 for e in cfg.entries() {
3691 if e.key == "safe.directory"
3692 && e.scope != crate::config::ConfigScope::Local
3693 && e.scope != crate::config::ConfigScope::Worktree
3694 {
3695 values.push(e.value.clone().unwrap_or_else(|| "true".to_owned()));
3696 }
3697 }
3698 let mut effective: Vec<String> = Vec::new();
3699 for v in values {
3700 if v.is_empty() {
3701 effective.clear();
3702 } else {
3703 effective.push(v);
3704 }
3705 }
3706 effective
3707}
3708
3709fn ensure_safe_directory_allows(
3710 git_dir: &Path,
3711 checked: &Path,
3712 environment: &Environment,
3713) -> Result<()> {
3714 let effective = safe_directory_effective_values(git_dir, environment);
3715 let checked_s = checked.to_string_lossy().to_string();
3716 let _ = environment.grit_debug_safe_dir;
3717 if effective
3718 .iter()
3719 .any(|v| safe_directory_matches(v, &checked_s, &environment.discovery_cwd()))
3720 {
3721 return Ok(());
3722 }
3723 Err(Error::DubiousOwnership(path_for_error_message(checked)))
3724}
3725
3726#[cfg(unix)]
3727fn path_lstat_uid(path: &Path) -> std::io::Result<u32> {
3728 use std::os::unix::fs::MetadataExt;
3729 let meta = fs::symlink_metadata(path)?;
3730 Ok(meta.uid())
3731}
3732
3733#[cfg(unix)]
3736fn ensure_valid_ownership(
3737 environment: &Environment,
3738 gitfile: Option<&Path>,
3739 worktree: Option<&Path>,
3740 gitdir: &Path,
3741) -> Result<()> {
3742 const ROOT_UID: u32 = 0;
3743 let sudo_euid = environment
3744 .sudo_uid
3745 .as_deref()
3746 .and_then(|s| s.parse::<u32>().ok());
3747
3748 fn owned_by_effective_user(path: &Path, sudo_euid: Option<u32>) -> std::io::Result<bool> {
3749 let st_uid = path_lstat_uid(path)?;
3750 let mut euid = nix::unistd::geteuid().as_raw();
3751 if euid == ROOT_UID {
3752 if st_uid == ROOT_UID {
3753 return Ok(true);
3754 }
3755 if let Some(sudo_uid) = sudo_euid {
3756 euid = sudo_uid;
3757 }
3758 }
3759 Ok(st_uid == euid)
3760 }
3761
3762 let assume_different = environment.test_assume_different_owner();
3763 if !assume_different {
3764 let gitfile_ok = gitfile
3765 .map(|p| owned_by_effective_user(p, sudo_euid))
3766 .transpose()?
3767 .unwrap_or(true);
3768 let wt_ok = match worktree {
3771 None => true,
3772 Some(wt) => match owned_by_effective_user(wt, sudo_euid) {
3773 Ok(ok) => ok,
3774 Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
3775 Err(e) => return Err(Error::Io(e)),
3776 },
3777 };
3778 let gd_ok = owned_by_effective_user(gitdir, sudo_euid)?;
3779 if gitfile_ok && wt_ok && gd_ok {
3780 return Ok(());
3781 }
3782 }
3783
3784 let data_path = if let Some(wt) = worktree {
3785 wt.canonicalize().unwrap_or_else(|_| wt.to_path_buf())
3786 } else {
3787 gitdir
3788 .canonicalize()
3789 .unwrap_or_else(|_| gitdir.to_path_buf())
3790 };
3791 ensure_safe_directory_allows(gitdir, &data_path, environment)
3792}
3793
3794#[cfg(not(unix))]
3795fn ensure_valid_ownership(
3796 _environment: &Environment,
3797 _gitfile: Option<&Path>,
3798 _worktree: Option<&Path>,
3799 _gitdir: &Path,
3800) -> Result<()> {
3801 Ok(())
3802}
3803
3804impl Repository {
3805 pub fn enforce_safe_directory(&self) -> Result<()> {
3811 let assume_different =
3812 self.test_assume_different_owner || self.environment.test_assume_different_owner();
3813 if !assume_different {
3814 return Ok(());
3815 }
3816
3817 if self.explicit_git_dir {
3818 return Ok(());
3819 }
3820
3821 let checked = if let Some(wt) = &self.work_tree {
3825 let cwd = Some(self.environment.discovery_cwd());
3826 if let Some(cwd) = cwd {
3827 if cwd
3828 .canonicalize()
3829 .ok()
3830 .is_some_and(|c| c.starts_with(&self.git_dir))
3831 {
3832 self.git_dir
3833 .canonicalize()
3834 .unwrap_or_else(|_| self.git_dir.clone())
3835 } else {
3836 wt.canonicalize().unwrap_or_else(|_| wt.clone())
3837 }
3838 } else {
3839 wt.canonicalize().unwrap_or_else(|_| wt.clone())
3840 }
3841 } else {
3842 self.git_dir
3843 .canonicalize()
3844 .unwrap_or_else(|_| self.git_dir.clone())
3845 };
3846
3847 let _ = self.environment.grit_debug_safe_dir;
3848 self.enforce_safe_directory_checked(&checked)
3849 }
3850
3851 pub(crate) fn enforce_safe_directory_git_dir(&self) -> Result<()> {
3856 let assume_different =
3857 self.test_assume_different_owner || self.environment.test_assume_different_owner();
3858 if !assume_different {
3859 return Ok(());
3860 }
3861 let checked = self
3862 .git_dir
3863 .canonicalize()
3864 .unwrap_or_else(|_| self.git_dir.clone());
3865 let _ = self.environment.grit_debug_safe_dir;
3866 self.enforce_safe_directory_checked(&checked)
3867 }
3868
3869 fn enforce_safe_directory_checked(&self, checked: &Path) -> Result<()> {
3870 ensure_safe_directory_allows(&self.git_dir, checked, self.environment.as_ref())
3871 }
3872
3873 pub fn verify_safe_for_clone_source(&self) -> Result<()> {
3879 let assume_different =
3880 self.test_assume_different_owner || self.environment.test_assume_different_owner();
3881 if assume_different {
3882 self.enforce_safe_directory_git_dir()
3883 } else {
3884 #[cfg(unix)]
3885 {
3886 ensure_valid_ownership(self.environment.as_ref(), None, None, &self.git_dir)
3887 }
3888 #[cfg(not(unix))]
3889 {
3890 Ok(())
3891 }
3892 }
3893 }
3894}
3895
3896fn normalize_fs_path(raw: &str) -> String {
3897 use std::path::Component;
3898 let p = std::path::Path::new(raw);
3899 let mut parts: Vec<String> = Vec::new();
3900 let mut absolute = false;
3901 for c in p.components() {
3902 match c {
3903 Component::RootDir => {
3904 absolute = true;
3905 parts.clear();
3906 }
3907 Component::CurDir => {}
3908 Component::ParentDir => {
3909 if !parts.is_empty() {
3910 parts.pop();
3911 }
3912 }
3913 Component::Normal(s) => parts.push(s.to_string_lossy().to_string()),
3914 Component::Prefix(_) => {}
3915 }
3916 }
3917 let mut out = if absolute {
3918 String::from("/")
3919 } else {
3920 String::new()
3921 };
3922 out.push_str(&parts.join("/"));
3923 out
3924}
3925
3926fn safe_directory_matches(config_value: &str, checked: &str, cwd: &Path) -> bool {
3927 if config_value == "*" {
3928 return true;
3929 }
3930 if config_value == "." {
3931 let cwd_s = normalize_fs_path(&cwd.to_string_lossy());
3933 let checked_s = normalize_fs_path(checked);
3934 return cwd_s == checked_s;
3935 }
3936
3937 let canonicalize_or_normalize = |raw: &str| -> String {
3938 let p = std::path::Path::new(raw);
3939 if p.exists() {
3940 p.canonicalize()
3941 .map(|c| c.to_string_lossy().to_string())
3942 .map(|s| normalize_fs_path(&s))
3943 .unwrap_or_else(|_| normalize_fs_path(raw))
3944 } else {
3945 normalize_fs_path(raw)
3946 }
3947 };
3948
3949 let config_norm = canonicalize_or_normalize(config_value);
3950 let checked_norm = normalize_fs_path(checked);
3951
3952 if config_norm.ends_with("/*") {
3953 let prefix_raw = &config_norm[..config_norm.len() - 2];
3954 let prefix_norm = canonicalize_or_normalize(prefix_raw);
3955 let mut prefix = prefix_norm;
3956 if !prefix.ends_with('/') {
3957 prefix.push('/');
3958 }
3959 return checked_norm.starts_with(&prefix);
3960 }
3961
3962 config_norm == checked_norm
3963}
3964
3965fn warn_core_bare_worktree_conflict(options: &RepositoryOptions, git_dir: &Path) {
3966 if options
3967 .environment
3968 .git_work_tree
3969 .as_deref()
3970 .is_some_and(|s| !s.trim().is_empty())
3971 {
3972 return;
3973 }
3974 if let Ok((bare, wt)) = read_core_bare_and_worktree(git_dir) {
3975 if bare && wt.is_some() {
3976 options
3977 .diagnostics
3978 .warn(diagnostics::Warning::CoreBareWithWorktree);
3979 }
3980 }
3981}
3982
3983fn read_core_bare_and_worktree_from_config(cfg: &ConfigSet) -> (bool, Option<String>) {
3984 let bare = cfg
3985 .get_bool("core.bare")
3986 .and_then(|r| r.ok())
3987 .unwrap_or(false);
3988 let worktree = cfg.get("core.worktree").map(|s| s.to_owned());
3989 (bare, worktree)
3990}
3991
3992fn read_core_bare_and_worktree(git_dir: &Path) -> Result<(bool, Option<String>)> {
3993 let cfg = ConfigSet::load(
3994 &crate::environment::Environment::empty(),
3995 Some(git_dir),
3996 true,
3997 )
3998 .unwrap_or_default();
3999 Ok(read_core_bare_and_worktree_from_config(&cfg))
4000}
4001
4002fn validate_git_work_tree_path(path: &Path) -> Result<()> {
4005 if !path.is_absolute() {
4006 return Ok(());
4007 }
4008 let comps: Vec<Component<'_>> = path.components().collect();
4009 let Some(last_normal_idx) = comps
4010 .iter()
4011 .enumerate()
4012 .rev()
4013 .find_map(|(i, c)| matches!(c, Component::Normal(_)).then_some(i))
4014 else {
4015 return Ok(());
4016 };
4017 let mut cur = PathBuf::new();
4018 for (i, comp) in comps.iter().enumerate() {
4019 match comp {
4020 Component::Prefix(p) => cur.push(p.as_os_str()),
4021 Component::RootDir => cur.push(comp.as_os_str()),
4022 Component::CurDir => {}
4023 Component::ParentDir => {
4024 let _ = cur.pop();
4025 }
4026 Component::Normal(seg) => {
4027 cur.push(seg);
4028 if i != last_normal_idx && !cur.exists() {
4029 return Err(Error::PathError(format!(
4030 "Invalid path '{}': No such file or directory",
4031 cur.display()
4032 )));
4033 }
4034 }
4035 }
4036 }
4037 Ok(())
4038}
4039
4040fn resolve_core_worktree_path(git_dir: &Path, raw: &str) -> Result<PathBuf> {
4041 let p = Path::new(raw);
4042 if p.is_absolute() {
4043 return Ok(p.canonicalize().unwrap_or_else(|_| p.to_path_buf()));
4044 }
4045 let joined = git_dir.join(p);
4046 Ok(joined.canonicalize().unwrap_or(joined))
4047}
4048
4049fn resolve_git_dir_env_path(git_dir: &Path) -> Result<PathBuf> {
4051 if git_dir.is_file() {
4052 let content =
4053 fs::read_to_string(git_dir).map_err(|e| Error::NotARepository(e.to_string()))?;
4054 let base = git_dir
4055 .parent()
4056 .ok_or_else(|| not_a_repository_at(git_dir))?;
4057 return parse_gitfile(&content, base);
4058 }
4059 Ok(git_dir.to_path_buf())
4060}
4061
4062pub fn resolve_git_directory_arg(git_dir: &Path) -> Result<PathBuf> {
4068 resolve_git_dir_env_path(git_dir)
4069}
4070
4071pub fn resolve_dot_git(dot_git: &Path) -> Result<PathBuf> {
4077 if dot_git.is_dir() {
4078 return dot_git
4079 .canonicalize()
4080 .map_err(|_| not_a_repository_at(dot_git));
4081 }
4082 if dot_git.is_file() {
4083 let content =
4084 fs::read_to_string(dot_git).map_err(|e| Error::NotARepository(e.to_string()))?;
4085 let base = dot_git
4086 .parent()
4087 .ok_or_else(|| not_a_repository_at(dot_git))?;
4088 return parse_gitfile(&content, base);
4089 }
4090 Err(not_a_repository_at(dot_git))
4091}
4092
4093fn not_a_repository_at(path: &Path) -> Error {
4094 Error::NotARepository(path_for_error_message(path))
4095}
4096
4097fn canonicalize_existing_prefix(path: &Path) -> Result<PathBuf> {
4101 let absolute = std::path::absolute(path)?;
4102 let mut missing = Vec::new();
4103 let mut existing = absolute.as_path();
4104 loop {
4105 if let Ok(canonical) = existing.canonicalize() {
4106 return Ok(missing
4107 .iter()
4108 .rev()
4109 .fold(canonical, |acc: PathBuf, part| acc.join(part)));
4110 }
4111 match (existing.parent(), existing.file_name()) {
4112 (Some(parent), Some(name)) => {
4113 missing.push(name.to_owned());
4114 existing = parent;
4115 }
4116 _ => return Ok(absolute),
4117 }
4118 }
4119}
4120
4121fn resolve_objects_dir(
4131 environment: &Environment,
4132 git_dir: &Path,
4133 common_git_dir: &Path,
4134) -> Option<PathBuf> {
4135 if let Some(dir) = environment.git_object_directory.as_deref() {
4136 return (!dir.is_empty()).then(|| environment.discovery_cwd().join(dir));
4137 }
4138 let own = git_dir.join("objects");
4139 Some(if own.exists() {
4140 own
4141 } else {
4142 common_git_dir.join("objects")
4143 })
4144}
4145
4146fn head_file_present(git_dir: &Path) -> bool {
4147 let head_path = git_dir.join("HEAD");
4148 head_path.exists() || head_path.is_symlink()
4149}
4150
4151fn open_or_install_ref_store(
4154 caches: &RepoCaches,
4155 git_dir: &Path,
4156 environment: &Environment,
4157 injected: Option<Arc<dyn crate::refs::store::RefStore>>,
4158) -> Result<Arc<dyn crate::refs::store::RefStore>> {
4159 match injected {
4160 Some(store) => {
4161 caches.install_ref_store(git_dir, Arc::clone(&store));
4162 Ok(store)
4163 }
4164 None => caches.open_ref_store(git_dir, environment),
4165 }
4166}
4167
4168fn parse_gitfile(content: &str, base: &Path) -> Result<PathBuf> {
4170 for line in content.lines() {
4171 if let Some(rest) = line.strip_prefix("gitdir:") {
4172 let rel = rest.trim();
4173 let path = if Path::new(rel).is_absolute() {
4174 PathBuf::from(rel)
4175 } else {
4176 base.join(rel)
4177 };
4178 if !path.exists() {
4179 return Err(not_a_repository_at(&path));
4180 }
4181 return Ok(path);
4182 }
4183 }
4184 Err(Error::NotARepository("invalid gitfile format".to_owned()))
4185}
4186
4187pub fn resolve_initial_branch(env: &Environment, explicit: Option<&str>) -> Result<String> {
4197 let name = match explicit {
4198 Some(name) => name.to_owned(),
4199 None => ConfigSet::load(env, None, true)?
4200 .get_last_entry("init.defaultBranch")
4201 .and_then(|e| e.value.clone())
4202 .filter(|s| !s.is_empty())
4203 .unwrap_or_else(|| "main".to_owned()),
4204 };
4205 validate_branch_short_name(&name).map_err(|e| Error::InvalidRef(e.to_string()))?;
4206 Ok(name)
4207}
4208
4209fn resolve_git_dir_for_init(path: &Path, options: &InitOptions) -> Result<PathBuf> {
4210 if options.bare_with_worktree_at_path || options.bare {
4211 return Ok(path.to_path_buf());
4212 }
4213 if let Some(separate) = &options.separate_git_dir {
4214 return Ok(separate.clone());
4215 }
4216 let dot_git = path.join(".git");
4217 if dot_git.is_file() {
4218 let content = fs::read_to_string(&dot_git)?;
4219 return parse_gitfile(&content, path);
4220 }
4221 Ok(dot_git)
4222}
4223
4224fn head_branch_name_for_outcome(git_dir: &Path) -> Result<String> {
4226 if let Some(target) = crate::refs::read_symbolic_ref(git_dir, "HEAD")? {
4227 if let Some(short) = target.strip_prefix("refs/heads/") {
4228 return Ok(short.to_owned());
4229 }
4230 return Ok(target
4231 .rsplit('/')
4232 .next()
4233 .unwrap_or(target.as_str())
4234 .to_owned());
4235 }
4236 let head_text = fs::read_to_string(git_dir.join("HEAD"))?;
4237 let oid = head_text.trim();
4238 if crate::objects::ObjectId::from_hex(oid).is_ok() {
4239 let end = oid.len().min(7);
4240 return Ok(oid[..end].to_owned());
4241 }
4242 Ok("HEAD".to_owned())
4243}
4244
4245fn hash_algo_from_git_dir(git_dir: &Path) -> Result<HashAlgo> {
4246 let config_path = git_dir.join("config");
4247 if !config_path.is_file() {
4248 return Ok(HashAlgo::Sha1);
4249 }
4250 let content = fs::read_to_string(&config_path)?;
4251 Ok(parse_repository_format(&content, &config_path)?.hash_algo())
4252}
4253
4254fn ensure_git_directory_layout(
4256 git_dir: &Path,
4257 bare: bool,
4258 ref_storage: crate::ref_storage::RefStorageFormat,
4259 skip_hooks_and_info: bool,
4260) -> Result<()> {
4261 let mut subs = vec!["objects", "objects/info", "objects/pack", "refs"];
4262 if !ref_storage.is_reftable() {
4263 subs.extend(["refs/heads", "refs/tags"]);
4264 }
4265 if !bare && !skip_hooks_and_info {
4266 subs.push("info");
4267 subs.push("hooks");
4268 }
4269 for sub in subs {
4270 fs::create_dir_all(git_dir.join(sub))?;
4271 }
4272
4273 if ref_storage.is_reftable() {
4274 let reftable_dir = git_dir.join("reftable");
4275 fs::create_dir_all(&reftable_dir)?;
4276 let tables_list = reftable_dir.join("tables.list");
4277 if !tables_list.exists() {
4278 fs::write(&tables_list, "")?;
4279 }
4280 }
4281 Ok(())
4282}
4283
4284fn write_fresh_git_directory(
4285 git_dir: &Path,
4286 bare: bool,
4287 initial_branch: &str,
4288 template_dir: Option<&Path>,
4289 ref_storage: crate::ref_storage::RefStorageFormat,
4290 object_format: HashAlgo,
4291 skip_hooks_and_info: bool,
4292) -> Result<()> {
4293 ensure_git_directory_layout(git_dir, bare, ref_storage, skip_hooks_and_info)?;
4294
4295 if let Some(tmpl) = template_dir {
4296 if tmpl.is_dir() {
4297 copy_template(tmpl, git_dir)?;
4298 }
4299 }
4300
4301 let needs_extensions = ref_storage.is_reftable() || object_format != HashAlgo::Sha1;
4302 let repo_version = if needs_extensions { 1 } else { 0 };
4303
4304 let mut config_content = String::from("[core]\n");
4305 config_content.push_str(&format!("\trepositoryformatversion = {repo_version}\n"));
4306 config_content.push_str("\tfilemode = true\n");
4307 if bare {
4308 config_content.push_str("\tbare = true\n");
4309 } else {
4310 config_content.push_str("\tbare = false\n");
4311 config_content.push_str("\tlogallrefupdates = true\n");
4312 }
4313 if needs_extensions {
4314 config_content.push_str("[extensions]\n");
4315 if ref_storage.is_reftable() {
4316 config_content.push_str("\trefStorage = reftable\n");
4317 }
4318 if object_format != HashAlgo::Sha1 {
4319 config_content.push_str(&format!("\tobjectformat = {}\n", object_format.name()));
4320 }
4321 }
4322 fs::write(git_dir.join("config"), config_content)?;
4323
4324 if let Some(tmpl) = template_dir {
4326 if tmpl.is_dir() {
4327 let tmpl_config = tmpl.join("config");
4328 if tmpl_config.is_file() {
4329 let tmpl_text = fs::read_to_string(&tmpl_config)?;
4330 let tmpl_parsed = ConfigFile::parse(&tmpl_config, &tmpl_text, ConfigScope::Local)?;
4331 let dest_path = git_dir.join("config");
4332 let dest_text = fs::read_to_string(&dest_path)?;
4333 let mut dest_parsed =
4334 ConfigFile::parse(&dest_path, &dest_text, ConfigScope::Local)?;
4335 for e in &tmpl_parsed.entries {
4336 if e.key == "core.bare" {
4338 continue;
4339 }
4340 if let Some(v) = &e.value {
4341 let _ = dest_parsed.set(&e.key, v);
4342 } else {
4343 let _ = dest_parsed.set(&e.key, "true");
4344 }
4345 }
4346 dest_parsed.write()?;
4347 }
4348 }
4349 }
4350
4351 crate::refs::store::initialize_head(
4352 git_dir,
4353 crate::refs::store::RawRef::Symbolic(format!("refs/heads/{initial_branch}")),
4354 )?;
4355
4356 apply_init_filesystem_config(
4357 git_dir,
4358 InitFilesystemConfigOptions::default(),
4359 &Environment::empty(),
4360 )?;
4361
4362 fs::write(
4363 git_dir.join("description"),
4364 "Unnamed repository; edit this file 'description' to name the repository.\n",
4365 )?;
4366 Ok(())
4367}
4368
4369pub(crate) fn init_repository_separate_git_dir(
4378 work_tree: &Path,
4379 git_dir: &Path,
4380 initial_branch: &str,
4381 template_dir: Option<&Path>,
4382 ref_storage: crate::ref_storage::RefStorageFormat,
4383 object_format: HashAlgo,
4384) -> Result<Repository> {
4385 let skip_hooks_info = template_dir.is_some_and(|p| p.as_os_str().is_empty());
4386 fs::create_dir_all(work_tree)?;
4387 fs::create_dir_all(git_dir)?;
4388 let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
4389 if existing {
4390 ensure_git_directory_layout(git_dir, false, ref_storage, skip_hooks_info)?;
4391 } else {
4392 write_fresh_git_directory(
4393 git_dir,
4394 false,
4395 initial_branch,
4396 template_dir,
4397 ref_storage,
4398 object_format,
4399 skip_hooks_info,
4400 )?;
4401 }
4402
4403 let gitfile = work_tree.join(".git");
4409 let abs_git_dir = fs::canonicalize(git_dir).unwrap_or_else(|_| git_dir.to_path_buf());
4410 let abs_git_dir = abs_git_dir.to_string_lossy().replace('\\', "/");
4411 fs::write(gitfile, format!("gitdir: {abs_git_dir}\n"))?;
4412
4413 Repository::open(
4414 git_dir,
4415 &RepositoryOptions::empty().with_work_tree(work_tree),
4416 )
4417}
4418
4419pub fn ensure_core_bare(git_dir: &Path) -> Result<()> {
4434 let path = git_dir.join("config");
4435 let text = fs::read_to_string(&path).unwrap_or_default();
4436 if text.lines().any(|l| {
4437 let t = l.trim();
4438 t == "bare = true" || t == "bare=true"
4439 }) {
4440 return Ok(());
4441 }
4442 let mut out = text;
4443 if !out.ends_with('\n') && !out.is_empty() {
4444 out.push('\n');
4445 }
4446 if !out.contains("[core]") {
4447 out.push_str("[core]\n");
4448 }
4449 out.push_str("\tbare = true\n");
4450 fs::write(path, out).map_err(Error::Io)
4451}
4452
4453pub(crate) fn init_repository(
4454 path: &Path,
4455 bare: bool,
4456 initial_branch: &str,
4457 template_dir: Option<&Path>,
4458 ref_storage: crate::ref_storage::RefStorageFormat,
4459) -> Result<Repository> {
4460 init_repository_with_object_format(
4461 path,
4462 bare,
4463 initial_branch,
4464 template_dir,
4465 ref_storage,
4466 HashAlgo::Sha1,
4467 )
4468}
4469
4470pub(crate) fn init_repository_with_object_format(
4471 path: &Path,
4472 bare: bool,
4473 initial_branch: &str,
4474 template_dir: Option<&Path>,
4475 ref_storage: crate::ref_storage::RefStorageFormat,
4476 object_format: HashAlgo,
4477) -> Result<Repository> {
4478 let skip_hooks_info = !bare && template_dir.is_some_and(|p| p.as_os_str().is_empty());
4479 let git_dir = if bare {
4480 path.to_path_buf()
4481 } else {
4482 path.join(".git")
4483 };
4484
4485 if !bare {
4486 fs::create_dir_all(path)?;
4487 }
4488 fs::create_dir_all(&git_dir)?;
4489 let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
4490 if existing {
4491 ensure_git_directory_layout(&git_dir, bare, ref_storage, skip_hooks_info)?;
4492 } else {
4493 write_fresh_git_directory(
4494 &git_dir,
4495 bare,
4496 initial_branch,
4497 template_dir,
4498 ref_storage,
4499 object_format,
4500 skip_hooks_info,
4501 )?;
4502 }
4503
4504 if bare {
4505 Repository::open(&git_dir, &RepositoryOptions::empty())
4506 } else {
4507 Repository::open(
4508 &git_dir,
4509 &RepositoryOptions::empty().with_work_tree(path.to_path_buf()),
4510 )
4511 }
4512}
4513
4514pub(crate) fn init_bare_with_env_worktree(
4523 git_dir: &Path,
4524 work_tree: &Path,
4525 initial_branch: &str,
4526 template_dir: Option<&Path>,
4527 ref_storage: crate::ref_storage::RefStorageFormat,
4528 object_format: HashAlgo,
4529) -> Result<Repository> {
4530 fs::create_dir_all(git_dir)?;
4531 fs::create_dir_all(work_tree)?;
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, true, ref_storage, false)?;
4535 } else {
4536 write_fresh_git_directory(
4537 git_dir,
4538 true,
4539 initial_branch,
4540 template_dir,
4541 ref_storage,
4542 object_format,
4543 false,
4544 )?;
4545 }
4546 let work_tree_abs = fs::canonicalize(work_tree).unwrap_or_else(|_| work_tree.to_path_buf());
4547 let config_path = git_dir.join("config");
4548 let mut config = match ConfigFile::from_path(&config_path, ConfigScope::Local)? {
4549 Some(c) => c,
4550 None => ConfigFile::parse(&config_path, "", ConfigScope::Local)?,
4551 };
4552 config.set("core.worktree", &work_tree_abs.to_string_lossy())?;
4553 config.write()?;
4554 Repository::open(
4555 git_dir,
4556 &RepositoryOptions::empty().with_work_tree(work_tree),
4557 )
4558}
4559
4560fn copy_template(src: &Path, dst: &Path) -> Result<()> {
4562 for entry in fs::read_dir(src)? {
4563 let entry = entry?;
4564 let src_path = entry.path();
4565 let dst_path = dst.join(entry.file_name());
4566 if src_path.is_dir() {
4567 fs::create_dir_all(&dst_path)?;
4568 copy_template(&src_path, &dst_path)?;
4569 } else {
4570 fs::copy(&src_path, &dst_path)?;
4571 }
4572 }
4573 Ok(())
4574}
4575
4576fn parse_ceiling_directories(env: &Environment) -> (Vec<PathBuf>, bool) {
4585 let Some(raw) = env.git_ceiling_directories.as_deref() else {
4586 return (Vec::new(), false);
4587 };
4588 if raw.is_empty() {
4589 return (Vec::new(), false);
4590 }
4591 let (no_resolve, effective) = match raw.strip_prefix(':') {
4593 Some(rest) => (true, rest),
4594 None => (false, raw),
4595 };
4596 let paths = effective
4597 .split(':')
4598 .filter(|s| !s.is_empty())
4599 .filter_map(|s| {
4600 let p = PathBuf::from(s);
4601 if !p.is_absolute() {
4602 return None;
4603 }
4604 if no_resolve {
4605 let s = s.trim_end_matches('/');
4607 Some(PathBuf::from(s))
4608 } else {
4609 Some(p.canonicalize().unwrap_or_else(|_| {
4612 let s = s.trim_end_matches('/');
4613 PathBuf::from(s)
4614 }))
4615 }
4616 })
4617 .collect();
4618 (paths, no_resolve)
4619}
4620
4621pub fn validate_repo_config(config_text: &str) -> std::result::Result<(), String> {
4624 let mut version: u32 = 0;
4625 let mut in_core = false;
4626 for line in config_text.lines() {
4627 let trimmed = line.trim();
4628 if trimmed.starts_with('[') {
4629 in_core = trimmed.to_lowercase().starts_with("[core");
4630 continue;
4631 }
4632 if in_core {
4633 if let Some(rest) = trimmed.strip_prefix("repositoryformatversion") {
4634 let val = rest.trim_start_matches([' ', '=']).trim();
4635 if let Ok(v) = val.parse::<u32>() {
4636 version = v;
4637 }
4638 }
4639 }
4640 }
4641 if version >= 2 {
4642 return Err(format!("unknown repository format version: {version}"));
4643 }
4644 Ok(())
4645}
4646
4647pub mod caches {
4649 pub use crate::repo_caches::*;
4650}
4651
4652mod index_write_fail_inject {
4653 use std::cell::RefCell;
4654
4655 thread_local! {
4657 pub static INJECT: RefCell<bool> = const { RefCell::new(false) };
4658 pub static INJECT_REMAINING: RefCell<Option<usize>> = const { RefCell::new(None) };
4659 }
4660}
4661
4662#[doc(hidden)]
4664pub fn set_test_inject_index_write_fail(enabled: bool) {
4665 index_write_fail_inject::INJECT.with(|c| *c.borrow_mut() = enabled);
4666 if !enabled {
4667 index_write_fail_inject::INJECT_REMAINING.with(|c| *c.borrow_mut() = None);
4668 }
4669}
4670
4671#[doc(hidden)]
4673pub fn set_test_inject_index_write_fail_remaining(remaining: usize) {
4674 index_write_fail_inject::INJECT.with(|c| *c.borrow_mut() = true);
4675 index_write_fail_inject::INJECT_REMAINING.with(|c| *c.borrow_mut() = Some(remaining));
4676}
4677
4678mod rewrite_apply_inject {
4679 use std::cell::RefCell;
4680
4681 thread_local! {
4682 pub static ABORT_AFTER_REFS: RefCell<bool> = const { RefCell::new(false) };
4683 }
4684}
4685
4686#[doc(hidden)]
4688pub fn set_test_inject_rewrite_apply_abort_after_refs(enabled: bool) {
4689 rewrite_apply_inject::ABORT_AFTER_REFS.with(|c| *c.borrow_mut() = enabled);
4690}
4691
4692pub(crate) fn test_inject_rewrite_apply_abort_after_refs() -> bool {
4693 rewrite_apply_inject::ABORT_AFTER_REFS.with(|c| *c.borrow())
4694}
4695
4696fn test_inject_index_write_fail() -> Result<()> {
4697 index_write_fail_inject::INJECT.with(|enabled| {
4698 if !*enabled.borrow() {
4699 return Ok(());
4700 }
4701 index_write_fail_inject::INJECT_REMAINING.with(|remaining| {
4702 if let Some(left) = remaining.borrow_mut().as_mut() {
4703 if *left == 0 {
4704 return Ok(());
4705 }
4706 *left -= 1;
4707 }
4708 Err(Error::Io(std::io::Error::other(
4709 "injected index write failure",
4710 )))
4711 })
4712 })
4713}
4714
4715#[cfg(test)]
4716#[path = "repo_line_coverage_tests.rs"]
4717mod line_coverage_tests;