1use std::collections::{BTreeSet, HashSet};
19use std::env;
20use std::fs;
21use std::fs::OpenOptions;
22use std::io::Write;
23use std::path::{Component, Path, PathBuf};
24use std::sync::Mutex;
25
26use crate::config::{ConfigFile, ConfigScope, ConfigSet};
27use crate::error::{Error, Result};
28use crate::hooks::run_hook;
29use crate::index::Index;
30use crate::objects::parse_commit;
31use crate::odb::Odb;
32use crate::rev_parse::is_inside_work_tree;
33use crate::sparse_checkout::effective_cone_mode_for_sparse_file;
34use crate::split_index::{write_index_file_split, WriteSplitIndexRequest};
35use crate::state::resolve_head;
36use crate::worktree_cwd::cwd_relative_under_work_tree;
37
38const GIT_PREFIX_ENV: &str = "GIT_PREFIX";
39
40fn export_git_prefix_env(repo: &Repository) {
46 let Some(wt) = repo.work_tree.as_ref() else {
47 return;
48 };
49 let Ok(cwd) = env::current_dir() else {
50 return;
51 };
52 let new_s = cwd_relative_under_work_tree(wt, &cwd).unwrap_or_default();
53 if new_s.is_empty() {
54 if let Ok(existing) = env::var(GIT_PREFIX_ENV) {
55 if !existing.trim().is_empty() {
56 return;
57 }
58 }
59 }
60 env::set_var(GIT_PREFIX_ENV, new_s);
61}
62
63fn read_sparse_checkout_patterns(git_dir: &Path) -> Vec<String> {
64 let path = git_dir.join("info").join("sparse-checkout");
65 let Ok(content) = fs::read_to_string(&path) else {
66 return Vec::new();
67 };
68 content
69 .lines()
70 .map(|l| l.trim())
71 .filter(|l| !l.is_empty() && !l.starts_with('#'))
72 .map(String::from)
73 .collect()
74}
75
76#[derive(Debug)]
78pub struct Repository {
79 pub git_dir: PathBuf,
81 pub work_tree: Option<PathBuf>,
83 pub odb: Odb,
85 pub explicit_git_dir: bool,
89 pub discovery_root: Option<PathBuf>,
92 pub work_tree_from_env: bool,
94 pub discovery_via_gitfile: bool,
96 cached_settings: std::sync::Arc<std::sync::OnceLock<RepoCachedSettings>>,
102}
103
104#[derive(Debug, Clone)]
106struct RepoCachedSettings {
107 use_replace_refs: bool,
109 replace_ref_base: String,
111}
112
113impl Repository {
114 fn from_canonical_git_dir(git_dir: PathBuf, work_tree: Option<&Path>) -> Result<Self> {
115 let head_path = git_dir.join("HEAD");
117 if !head_path.exists() && !head_path.is_symlink() {
118 return Err(Error::NotARepository(git_dir.display().to_string()));
119 }
120
121 let objects_dir = if git_dir.join("objects").exists() {
124 git_dir.join("objects")
125 } else if let Some(common_dir) = resolve_common_dir(&git_dir) {
126 common_dir.join("objects")
127 } else {
128 return Err(Error::NotARepository(git_dir.display().to_string()));
129 };
130
131 if !objects_dir.exists() {
132 return Err(Error::NotARepository(git_dir.display().to_string()));
133 }
134
135 let work_tree = match work_tree {
136 Some(p) => {
137 let cwd = env::current_dir().map_err(Error::Io)?;
138 let mut resolved = if p.is_absolute() {
139 p.to_path_buf()
140 } else {
141 cwd.join(p)
142 };
143 if resolved.exists() {
144 resolved = resolved
145 .canonicalize()
146 .map_err(|_| Error::PathError(p.display().to_string()))?;
147 }
148 Some(resolved)
149 }
150 None => None,
151 };
152
153 let odb = if let Some(ref wt) = work_tree {
154 Odb::with_work_tree(&objects_dir, wt).with_config_git_dir(git_dir.clone())
155 } else {
156 Odb::new(&objects_dir).with_config_git_dir(git_dir.clone())
157 };
158
159 Ok(Self {
160 git_dir,
161 work_tree,
162 odb,
163 explicit_git_dir: false,
164 discovery_root: None,
165 work_tree_from_env: false,
166 discovery_via_gitfile: false,
167 cached_settings: std::sync::Arc::new(std::sync::OnceLock::new()),
168 })
169 }
170
171 fn cached_settings(&self) -> &RepoCachedSettings {
177 self.cached_settings.get_or_init(|| {
178 let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
179 let use_replace_refs = cfg
180 .get_bool("core.useReplaceRefs")
181 .and_then(|r| r.ok())
182 .unwrap_or(true);
183 let replace_ref_base = std::env::var("GIT_REPLACE_REF_BASE")
184 .ok()
185 .filter(|s| !s.is_empty())
186 .unwrap_or_else(|| "refs/replace/".to_owned());
187 let replace_ref_base = if replace_ref_base.ends_with('/') {
188 replace_ref_base
189 } else {
190 format!("{replace_ref_base}/")
191 };
192 RepoCachedSettings {
193 use_replace_refs,
194 replace_ref_base,
195 }
196 })
197 }
198
199 pub fn open(git_dir: &Path, work_tree: Option<&Path>) -> Result<Self> {
206 let git_dir = git_dir
207 .canonicalize()
208 .map_err(|_| Error::NotARepository(git_dir.display().to_string()))?;
209
210 validate_repository_format(&git_dir)?;
211
212 Self::from_canonical_git_dir(git_dir, work_tree)
213 }
214
215 pub fn open_skipping_format_validation(
220 git_dir: &Path,
221 work_tree: Option<&Path>,
222 ) -> Result<Self> {
223 let git_dir = git_dir
224 .canonicalize()
225 .map_err(|_| Error::NotARepository(git_dir.display().to_string()))?;
226 Self::from_canonical_git_dir(git_dir, work_tree)
227 }
228
229 pub fn discover(start: Option<&Path>) -> Result<Self> {
238 if let Ok(dir) = env::var("GIT_DIR") {
240 let cwd = env::current_dir()?;
241 let mut git_dir = PathBuf::from(&dir);
242 if git_dir.is_relative() {
243 git_dir = cwd.join(git_dir);
244 }
245 git_dir = resolve_git_dir_env_path(&git_dir)?;
247 let work_tree = env::var("GIT_WORK_TREE").ok().map(|wt| {
248 let p = PathBuf::from(wt);
249 if p.is_absolute() {
250 p
251 } else {
252 cwd.join(p)
253 }
254 });
255 if let Some(ref wt_path) = work_tree {
256 if env::var("GIT_WORK_TREE")
257 .ok()
258 .is_some_and(|raw| Path::new(&raw).is_absolute())
259 {
260 validate_git_work_tree_path(wt_path)?;
261 }
262 }
263 if work_tree.is_some() {
264 let mut repo = Self::open(&git_dir, work_tree.as_deref())?;
265 repo.explicit_git_dir = true;
266 repo.discovery_root = None;
267 repo.work_tree_from_env = false;
268 repo.discovery_via_gitfile = false;
269 export_git_prefix_env(&repo);
270 return Ok(repo);
271 }
272 let (is_bare, core_wt) = read_core_bare_and_worktree(&git_dir)?;
274 if is_bare && core_wt.is_some() {
275 warn_core_bare_worktree_conflict(&git_dir);
276 }
277 let resolved_wt = if is_bare {
278 None
279 } else if let Some(raw) = core_wt {
280 Some(resolve_core_worktree_path(&git_dir, &raw)?)
281 } else {
282 Some(cwd.canonicalize().unwrap_or_else(|_| cwd.clone()))
288 };
289 let mut repo = Self::open(&git_dir, resolved_wt.as_deref())?;
290 repo.explicit_git_dir = true;
291 repo.discovery_root = None;
292 repo.work_tree_from_env = false;
293 repo.discovery_via_gitfile = false;
294 export_git_prefix_env(&repo);
295 return Ok(repo);
296 }
297
298 let cwd = env::current_dir()?;
299
300 let env_work_tree = env::var("GIT_WORK_TREE").ok().map(|wt| {
303 let p = PathBuf::from(wt);
304 if p.is_absolute() {
305 p
306 } else {
307 cwd.join(p)
308 }
309 });
310 if let Some(ref p) = env_work_tree {
311 if env::var("GIT_WORK_TREE")
312 .ok()
313 .is_some_and(|raw| Path::new(&raw).is_absolute())
314 {
315 validate_git_work_tree_path(p)?;
316 }
317 }
318 let start = start.unwrap_or(&cwd);
319 let start = if start.is_absolute() {
320 start.to_path_buf()
321 } else {
322 cwd.join(start)
323 };
324
325 let (ceiling_paths, no_resolve_ceilings) = parse_ceiling_directories();
329 let ceiling_dirs: Vec<String> = ceiling_paths
330 .into_iter()
331 .map(|p| path_for_ceiling_compare(&p))
332 .collect();
333
334 let start_canon = start.canonicalize().unwrap_or_else(|_| start.clone());
335 let ceil_cmp_buf = if no_resolve_ceilings {
337 path_for_ceiling_compare(&start)
338 } else {
339 path_for_ceiling_compare(&start_canon)
340 };
341 let mut dir_buf = path_for_ceiling_compare(&start_canon);
342 let min_offset = offset_1st_component(&dir_buf);
343 let mut ceil_offset: isize = longest_ancestor_length(&ceil_cmp_buf, &ceiling_dirs)
344 .map(|n| n as isize)
345 .unwrap_or(-1);
346 if ceil_offset < 0 {
347 ceil_offset = min_offset as isize - 2;
348 }
349
350 loop {
351 let current = Path::new(&dir_buf);
352 if let Some(DiscoveredAt { mut repo, gitfile }) = try_open_at(current)? {
353 validate_repository_format(&repo.git_dir)?;
359 if let Some(ref wt) = env_work_tree {
360 repo.work_tree = Some(wt.canonicalize().unwrap_or_else(|_| wt.clone()));
361 repo.work_tree_from_env = true;
362 } else {
363 repo.work_tree_from_env = false;
364 let linked_gitfile =
369 repo.discovery_via_gitfile && resolve_common_dir(&repo.git_dir).is_some();
370 if !linked_gitfile {
371 let (is_bare, core_wt) = read_core_bare_and_worktree(&repo.git_dir)?;
372 if is_bare {
373 repo.work_tree = None;
374 } else if let Some(raw) = core_wt {
375 repo.work_tree = Some(resolve_core_worktree_path(&repo.git_dir, &raw)?);
376 }
377 }
378 }
379 let assume_different = env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
380 .ok()
381 .map(|v| {
382 let lower = v.to_ascii_lowercase();
383 v == "1" || lower == "true" || lower == "yes" || lower == "on"
384 })
385 .unwrap_or(false);
386 if assume_different {
387 repo.enforce_safe_directory()?;
388 } else {
389 ensure_valid_ownership(
390 gitfile.as_deref(),
391 repo.work_tree.as_deref(),
392 &repo.git_dir,
393 )?;
394 }
395 export_git_prefix_env(&repo);
396 return Ok(repo);
397 }
398
399 let mut offset: isize = dir_buf.len() as isize;
400 if offset <= min_offset as isize {
401 break;
402 }
403 loop {
404 offset -= 1;
405 if offset <= ceil_offset {
406 break;
407 }
408 if dir_buf
409 .as_bytes()
410 .get(offset as usize)
411 .is_some_and(|b| *b == b'/')
412 {
413 break;
414 }
415 }
416 if offset <= ceil_offset {
417 break;
418 }
419 let off_u = offset as usize;
420 let new_len = if off_u > min_offset {
421 off_u
422 } else {
423 min_offset
424 };
425 dir_buf.truncate(new_len);
426 }
427
428 Err(Error::NotARepository(start.display().to_string()))
429 }
430
431 #[must_use]
437 pub fn effective_pathspec_cwd(&self) -> PathBuf {
438 let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
439 let Some(wt) = self.work_tree.as_ref() else {
440 return cwd;
441 };
442 let inside_lexical = cwd.strip_prefix(wt).is_ok();
443 let inside_canon = cwd
444 .canonicalize()
445 .ok()
446 .zip(wt.canonicalize().ok())
447 .is_some_and(|(c, w)| c.starts_with(&w));
448 if inside_lexical || inside_canon {
449 cwd
450 } else {
451 wt.clone()
452 }
453 }
454
455 #[must_use]
457 pub fn index_path(&self) -> PathBuf {
458 self.git_dir.join("index")
459 }
460
461 pub fn index_path_for_env(&self) -> Result<PathBuf> {
465 if let Ok(raw) = env::var("GIT_INDEX_FILE") {
466 if !raw.is_empty() {
467 let p = PathBuf::from(raw);
468 return Ok(if p.is_absolute() {
469 p
470 } else {
471 env::current_dir().map_err(Error::Io)?.join(p)
472 });
473 }
474 }
475 Ok(self.index_path())
476 }
477
478 pub fn load_index(&self) -> Result<Index> {
482 let path = self.index_path_for_env()?;
483 self.load_index_at(&path)
484 }
485
486 pub fn load_index_at(&self, path: &std::path::Path) -> Result<Index> {
489 let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
490 if let Some(res) = cfg.get_bool("index.sparse") {
491 res.map_err(Error::ConfigError)?;
492 }
493 let mut idx = Index::load_expand_sparse_optional(path, &self.odb)?;
494 crate::split_index::resolve_split_index_if_needed(&mut idx, &self.git_dir, path)?;
495 if let Some(ref wt) = self.work_tree {
496 crate::sparse_checkout::clear_skip_worktree_from_present_files(
497 &self.git_dir,
498 wt,
499 &mut idx,
500 );
501 }
502 Ok(idx)
503 }
504
505 pub fn write_index(&self, index: &mut Index) -> Result<()> {
508 self.write_index_at(&self.index_path(), index)
509 }
510
511 pub fn write_index_with_post_index_change(
522 &self,
523 index: &mut Index,
524 updated_workdir: bool,
525 updated_skipworktree: bool,
526 ) -> Result<()> {
527 self.write_index_at_with_post_index_change(
528 &self.index_path(),
529 index,
530 updated_workdir,
531 updated_skipworktree,
532 )
533 }
534
535 pub fn write_index_at(&self, path: &std::path::Path, index: &mut Index) -> Result<()> {
537 self.write_index_at_split(path, index, WriteSplitIndexRequest::default())
538 }
539
540 #[must_use]
552 pub fn split_index_would_force_write(&self, index: &Index) -> bool {
553 if index.split_index_base_oid().is_some() {
554 return false;
555 }
556 let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
557 matches!(
558 crate::split_index::split_index_config(&cfg),
559 crate::split_index::SplitIndexConfig::Enabled
560 ) || crate::split_index::git_test_split_index_env()
561 }
562
563 pub fn write_index_at_with_post_index_change(
575 &self,
576 path: &std::path::Path,
577 index: &mut Index,
578 updated_workdir: bool,
579 updated_skipworktree: bool,
580 ) -> Result<()> {
581 self.write_index_at_split_with_post_index_change(
582 path,
583 index,
584 WriteSplitIndexRequest::default(),
585 updated_workdir,
586 updated_skipworktree,
587 )
588 }
589
590 pub fn write_index_at_split(
592 &self,
593 path: &std::path::Path,
594 index: &mut Index,
595 split: WriteSplitIndexRequest,
596 ) -> Result<()> {
597 self.write_index_at_split_with_post_index_change(path, index, split, false, false)
598 }
599
600 pub fn write_index_at_split_with_post_index_change(
613 &self,
614 path: &std::path::Path,
615 index: &mut Index,
616 split: WriteSplitIndexRequest,
617 updated_workdir: bool,
618 updated_skipworktree: bool,
619 ) -> Result<()> {
620 index.hash_algo = self.odb.hash_algo();
625 self.finalize_sparse_index_if_needed(index)?;
626 let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
627 let skip_hash = crate::index::index_skip_hash_for_write(Some(&cfg));
628 write_index_file_split(path, &self.git_dir, index, &cfg, split, skip_hash)?;
629 let updated_workdir_arg = if updated_workdir { "1" } else { "0" };
631 let updated_skipworktree_arg = if updated_skipworktree { "1" } else { "0" };
632 let _ = run_hook(
633 self,
634 "post-index-change",
635 &[updated_workdir_arg, updated_skipworktree_arg],
636 None,
637 );
638 Ok(())
639 }
640
641 fn finalize_sparse_index_if_needed(&self, index: &mut Index) -> Result<()> {
642 let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
643 let sparse_enabled = cfg
644 .get("core.sparseCheckout")
645 .map(|v| v == "true")
646 .unwrap_or(false);
647 if !sparse_enabled {
648 index.sparse_directories = false;
649 return Ok(());
650 }
651 let cone_cfg = cfg
652 .get("core.sparseCheckoutCone")
653 .and_then(|v| v.parse::<bool>().ok())
654 .unwrap_or(true);
655 let sparse_ix = cfg
656 .get("index.sparse")
657 .map(|v| v == "true")
658 .unwrap_or(false);
659 let patterns = read_sparse_checkout_patterns(&self.git_dir);
660 let cone = effective_cone_mode_for_sparse_file(cone_cfg, &patterns);
661 let head = resolve_head(&self.git_dir)?;
662 let tree_oid = if let Some(oid) = head.oid() {
663 let obj = self.odb.read(oid)?;
664 let commit = parse_commit(&obj.data)?;
665 Some(commit.tree)
666 } else {
667 None
668 };
669 if let Some(t) = tree_oid {
670 index.try_collapse_sparse_directories(&self.odb, &t, &patterns, cone, sparse_ix)?;
671 } else {
672 index.sparse_directories = false;
673 }
674 Ok(())
675 }
676
677 #[must_use]
679 pub fn refs_dir(&self) -> PathBuf {
680 self.git_dir.join("refs")
681 }
682
683 #[must_use]
685 pub fn head_path(&self) -> PathBuf {
686 self.git_dir.join("HEAD")
687 }
688
689 #[must_use]
694 pub fn bloom_pathspec_cwd(&self) -> Option<String> {
695 let wt = self.work_tree.as_ref()?;
696 let cwd = env::current_dir().ok()?;
697 let wt = wt.canonicalize().ok()?;
698 let cwd = cwd.canonicalize().ok()?;
699 let rel = cwd.strip_prefix(&wt).ok()?;
700 let s = rel.to_string_lossy().replace('\\', "/");
701 let s = s.trim_start_matches('/').to_string();
702 Some(s)
703 }
704
705 #[must_use]
707 pub fn is_bare(&self) -> bool {
708 if let Ok(cfg) = ConfigSet::load(Some(&self.git_dir), true) {
709 if let Some(Ok(bare)) = cfg.get_bool("core.bare") {
710 return bare;
711 }
712 }
713 self.work_tree.is_none()
714 }
715
716 pub fn read_replaced(&self, oid: &crate::objects::ObjectId) -> Result<crate::objects::Object> {
723 if std::env::var_os("GIT_NO_REPLACE_OBJECTS").is_some() {
724 return self.odb.read(oid);
725 }
726 let settings = self.cached_settings();
727 if !settings.use_replace_refs {
728 return self.odb.read(oid);
729 }
730 let replace_ref =
731 self.git_dir
732 .join(format!("{}{}", settings.replace_ref_base, oid.to_hex()));
733 if replace_ref.is_file() {
734 if let Ok(content) = std::fs::read_to_string(&replace_ref) {
735 let hex = content.trim();
736 if let Ok(replacement_oid) = hex.parse::<crate::objects::ObjectId>() {
737 if let Ok(obj) = self.odb.read(&replacement_oid) {
738 return Ok(obj);
739 }
740 }
741 }
742 }
743 self.odb.read(oid)
744 }
745}
746
747pub fn trace_repo_setup_if_requested(repo: &Repository) -> std::io::Result<()> {
752 let Ok(path) = env::var("GIT_TRACE_SETUP") else {
753 return Ok(());
754 };
755 if path.is_empty() || path == "0" {
756 return Ok(());
757 }
758 let trace_path = Path::new(&path);
759 if !trace_path.is_absolute() {
760 return Ok(());
761 }
762
763 let actual_cwd = env::current_dir()?;
764 let actual_cwd = actual_cwd
765 .canonicalize()
766 .unwrap_or_else(|_| actual_cwd.clone());
767
768 let (trace_cwd, prefix) = if let Some(ref wt) = repo.work_tree {
771 let wt_canon = wt.canonicalize().unwrap_or_else(|_| wt.clone());
772 if actual_cwd.starts_with(&wt_canon) {
773 let rel = actual_cwd
774 .strip_prefix(&wt_canon)
775 .map(|p| p.to_path_buf())
776 .unwrap_or_default();
777 let prefix = if rel.as_os_str().is_empty() {
778 "(null)".to_owned()
779 } else {
780 let mut s = rel.to_string_lossy().replace('\\', "/");
781 if !s.ends_with('/') {
782 s.push('/');
783 }
784 s
785 };
786 (wt_canon, prefix)
787 } else {
788 (actual_cwd.clone(), "(null)".to_owned())
789 }
790 } else {
791 (actual_cwd.clone(), "(null)".to_owned())
792 };
793
794 let git_dir_display =
795 display_git_dir_for_setup_trace(repo, &trace_cwd, &actual_cwd, prefix.as_str());
796 let common_display = display_common_dir_for_setup_trace(
797 repo,
798 &trace_cwd,
799 &actual_cwd,
800 prefix.as_str(),
801 &git_dir_display,
802 );
803 let worktree_display = repo
804 .work_tree
805 .as_ref()
806 .map(|p| {
807 p.canonicalize()
808 .unwrap_or_else(|_| lexical_normalize_path(p))
809 .display()
810 .to_string()
811 })
812 .unwrap_or_else(|| "(null)".to_owned());
813
814 let mut f = OpenOptions::new()
815 .create(true)
816 .append(true)
817 .open(trace_path)?;
818 writeln!(f, "setup: git_dir: {git_dir_display}")?;
819 writeln!(f, "setup: git_common_dir: {common_display}")?;
820 writeln!(f, "setup: worktree: {worktree_display}")?;
821 writeln!(f, "setup: cwd: {}", trace_cwd.display())?;
822 writeln!(f, "setup: prefix: {prefix}")?;
823 Ok(())
824}
825
826fn lexical_normalize_path(path: &Path) -> PathBuf {
828 let mut out = PathBuf::new();
829 let mut absolute = false;
830 for c in path.components() {
831 match c {
832 Component::Prefix(p) => {
833 out.push(p.as_os_str());
834 }
835 Component::RootDir => {
836 absolute = true;
837 out.push(c.as_os_str());
838 }
839 Component::CurDir => {}
840 Component::ParentDir => {
841 if absolute {
842 let _ = out.pop();
843 } else if !out.pop() {
844 out.push("..");
845 }
846 }
847 Component::Normal(s) => out.push(s),
848 }
849 }
850 if out.as_os_str().is_empty() {
851 PathBuf::from(".")
852 } else {
853 out
854 }
855}
856
857fn path_relative_to(target: &Path, base: &Path) -> Option<PathBuf> {
859 let t = target.canonicalize().ok()?;
860 let b = base.canonicalize().ok()?;
861 let tc: Vec<_> = t.components().collect();
862 let bc: Vec<_> = b.components().collect();
863 let mut i = 0usize;
864 while i < tc.len() && i < bc.len() && tc[i] == bc[i] {
865 i += 1;
866 }
867 let up = bc.len().saturating_sub(i);
868 let mut out = PathBuf::new();
869 for _ in 0..up {
870 out.push("..");
871 }
872 for comp in &tc[i..] {
873 out.push(comp.as_os_str());
874 }
875 Some(out)
876}
877
878fn rel_path_for_setup_trace(target: &Path, trace_cwd: &Path) -> String {
879 let t = target
880 .canonicalize()
881 .unwrap_or_else(|_| target.to_path_buf());
882 let tc = trace_cwd
883 .canonicalize()
884 .unwrap_or_else(|_| trace_cwd.to_path_buf());
885 if let Some(rel) = path_relative_to(&t, &tc) {
886 let s = rel.to_string_lossy().replace('\\', "/");
887 return if s.is_empty() || s == "." {
888 ".".to_owned()
889 } else {
890 s
891 };
892 }
893 t.display().to_string()
894}
895
896fn trace_cwd_strictly_inside_git_parent(trace_cwd: &Path, git_dir: &Path) -> bool {
897 let tc = trace_cwd
898 .canonicalize()
899 .unwrap_or_else(|_| trace_cwd.to_path_buf());
900 let gd = git_dir
901 .canonicalize()
902 .unwrap_or_else(|_| git_dir.to_path_buf());
903 let Some(parent) = gd.parent() else {
904 return false;
905 };
906 let parent = parent.to_path_buf();
907 if tc == parent {
908 return false;
909 }
910 tc.starts_with(&parent) && tc != parent
911}
912
913fn display_git_dir_for_setup_trace(
914 repo: &Repository,
915 trace_cwd: &Path,
916 actual_cwd: &Path,
917 setup_prefix: &str,
918) -> String {
919 let gd = repo
920 .git_dir
921 .canonicalize()
922 .unwrap_or_else(|_| repo.git_dir.clone());
923 let tc = trace_cwd
924 .canonicalize()
925 .unwrap_or_else(|_| trace_cwd.to_path_buf());
926 let ac = actual_cwd
927 .canonicalize()
928 .unwrap_or_else(|_| actual_cwd.to_path_buf());
929
930 if repo.work_tree.is_none() && !repo.explicit_git_dir {
933 if ac == gd {
934 return ".".to_owned();
935 }
936 if ac.starts_with(&gd) && ac != gd {
937 return gd.display().to_string();
938 }
939 }
940
941 if !repo.explicit_git_dir {
943 if let Some(wt) = &repo.work_tree {
944 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
945 if ac.starts_with(&gd) && ac != wt {
946 return gd.display().to_string();
947 }
948 }
949 }
950
951 if repo.explicit_git_dir {
955 if repo.work_tree.is_none() {
956 if let Ok(raw) = env::var("GIT_DIR") {
957 let p = Path::new(raw.trim());
958 if p.is_absolute() {
959 return gd.display().to_string();
960 }
961 let joined = ac.join(p);
962 if joined.is_file() {
963 return gd.display().to_string();
964 }
965 if let Some(rel) = path_relative_to(&gd, &tc) {
966 let s = rel.to_string_lossy().replace('\\', "/");
967 return if s.is_empty() || s == "." {
968 ".".to_owned()
969 } else {
970 s
971 };
972 }
973 }
974 return gd.display().to_string();
975 }
976 if let Some(wt) = &repo.work_tree {
977 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
978 let strictly_inside_wt = ac.starts_with(&wt) && ac != wt;
979 if strictly_inside_wt {
980 return gd.display().to_string();
981 }
982 if let Ok(raw) = env::var("GIT_DIR") {
983 let p = Path::new(raw.trim());
984 if p.is_relative() {
985 let joined = ac.join(p);
986 if joined.is_file() {
987 return gd.display().to_string();
989 }
990 if let Some(rel) = path_relative_to(&gd, &tc) {
991 let s = rel.to_string_lossy().replace('\\', "/");
992 return if s.is_empty() || s == "." {
993 ".".to_owned()
994 } else {
995 s
996 };
997 }
998 }
999 return gd.display().to_string();
1000 }
1001 }
1002 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
1003 return rel_path_for_setup_trace(&gd, trace_cwd);
1004 }
1005 return gd.display().to_string();
1006 }
1007
1008 let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
1009 (Some(root), Some(wt)) if !repo.work_tree_from_env => {
1010 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
1011 let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
1012 r != w
1013 }
1014 _ => false,
1015 };
1016
1017 if repo.work_tree_from_env {
1018 if !repo.discovery_via_gitfile {
1019 if setup_prefix == "(null)" {
1020 if let (Some(root), Some(wt)) = (&repo.discovery_root, &repo.work_tree) {
1021 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
1022 let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
1023 if r == w {
1024 let dot_git = r.join(".git");
1025 let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
1026 if gd == dot_git {
1027 return ".git".to_owned();
1028 }
1029 }
1030 }
1031 }
1032 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
1033 return rel_path_for_setup_trace(&gd, trace_cwd);
1034 }
1035 }
1036 return gd.display().to_string();
1037 }
1038
1039 if work_relocated {
1040 if let Some(wt) = &repo.work_tree {
1041 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
1042 if ac == wt {
1043 return gd.display().to_string();
1044 }
1045 let inside_wt = ac.starts_with(&wt) && ac != wt;
1046 if inside_wt {
1047 if let Some(rel) = path_relative_to(&gd, &ac) {
1048 let s = rel.to_string_lossy().replace('\\', "/");
1049 return if s.is_empty() || s == "." {
1050 ".".to_owned()
1051 } else {
1052 s
1053 };
1054 }
1055 }
1056 }
1057 }
1058 if repo.work_tree.is_some() {
1059 if let Some(root) = &repo.discovery_root {
1060 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
1061 let dot_git = r.join(".git");
1062 let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
1063 if gd == dot_git {
1064 return ".git".to_owned();
1065 }
1066 } else if let Some(wt) = &repo.work_tree {
1067 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
1068 let dot_git = wt.join(".git");
1069 let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
1070 if gd == dot_git {
1071 return ".git".to_owned();
1072 }
1073 }
1074 }
1075
1076 if repo.discovery_via_gitfile && !repo.explicit_git_dir {
1077 return gd.display().to_string();
1078 }
1079
1080 if repo.work_tree.is_none() && !repo.explicit_git_dir {
1084 if let Some(gp) = gd.parent() {
1085 let gp = gp.canonicalize().unwrap_or_else(|_| gp.to_path_buf());
1086 let gdc = gd.canonicalize().unwrap_or_else(|_| gd.clone());
1087 if tc.starts_with(&gp) && tc != gp && !tc.starts_with(&gdc) {
1088 return gdc.display().to_string();
1089 }
1090 if tc == gp {
1091 return rel_path_for_setup_trace(&gd, trace_cwd);
1092 }
1093 }
1094 }
1095
1096 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
1097 rel_path_for_setup_trace(&gd, trace_cwd)
1098 } else {
1099 gd.display().to_string()
1100 }
1101}
1102
1103fn display_common_dir_for_setup_trace(
1104 repo: &Repository,
1105 trace_cwd: &Path,
1106 actual_cwd: &Path,
1107 _setup_prefix: &str,
1108 git_dir_display: &str,
1109) -> String {
1110 let gd = repo
1111 .git_dir
1112 .canonicalize()
1113 .unwrap_or_else(|_| repo.git_dir.clone());
1114 let Some(common) = resolve_common_dir(&gd) else {
1115 return git_dir_display.to_owned();
1116 };
1117 let common = common.canonicalize().unwrap_or(common);
1118 if common == gd {
1119 return git_dir_display.to_owned();
1120 }
1121
1122 let ac = actual_cwd
1123 .canonicalize()
1124 .unwrap_or_else(|_| actual_cwd.to_path_buf());
1125 if repo.work_tree.is_none() && !repo.explicit_git_dir {
1126 if ac == common {
1127 return ".".to_owned();
1128 }
1129 if ac.starts_with(&common) && ac != common {
1130 return common.display().to_string();
1131 }
1132 }
1133
1134 let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
1135 (Some(root), Some(wt)) if !repo.work_tree_from_env => {
1136 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
1137 let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
1138 r != w
1139 }
1140 _ => false,
1141 };
1142 if work_relocated {
1143 if let Some(wt) = &repo.work_tree {
1144 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
1145 if ac == wt {
1146 return common.display().to_string();
1147 }
1148 let inside_wt = ac.starts_with(&wt) && ac != wt;
1149 if inside_wt {
1150 if let Some(rel) = path_relative_to(&common, &ac) {
1151 let s = rel.to_string_lossy().replace('\\', "/");
1152 return if s.is_empty() || s == "." {
1153 ".".to_owned()
1154 } else {
1155 s
1156 };
1157 }
1158 }
1159 }
1160 }
1161
1162 if repo.discovery_via_gitfile && !repo.explicit_git_dir {
1163 return common.display().to_string();
1164 }
1165
1166 if repo.work_tree.is_none() && !repo.explicit_git_dir {
1167 let tc = trace_cwd
1168 .canonicalize()
1169 .unwrap_or_else(|_| trace_cwd.to_path_buf());
1170 if let Some(cp) = common.parent() {
1171 let cp = cp.canonicalize().unwrap_or_else(|_| cp.to_path_buf());
1172 let comc = common.canonicalize().unwrap_or_else(|_| common.clone());
1173 if tc.starts_with(&cp) && tc != cp && !tc.starts_with(&comc) {
1174 return comc.display().to_string();
1175 }
1176 if tc == cp {
1177 return rel_path_for_setup_trace(&common, trace_cwd);
1178 }
1179 }
1180 }
1181
1182 if trace_cwd_strictly_inside_git_parent(trace_cwd, &common) {
1183 rel_path_for_setup_trace(&common, trace_cwd)
1184 } else {
1185 common.display().to_string()
1186 }
1187}
1188
1189fn resolve_common_dir(git_dir: &Path) -> Option<PathBuf> {
1191 let common_raw = fs::read_to_string(git_dir.join("commondir")).ok()?;
1192 let common_rel = common_raw.trim();
1193 if common_rel.is_empty() {
1194 return None;
1195 }
1196 let common_dir = if Path::new(common_rel).is_absolute() {
1197 PathBuf::from(common_rel)
1198 } else {
1199 git_dir.join(common_rel)
1200 };
1201 Some(common_dir.canonicalize().unwrap_or(common_dir))
1202}
1203
1204#[must_use]
1206pub fn common_git_dir_for_config(git_dir: &Path) -> PathBuf {
1207 resolve_common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf())
1208}
1209
1210pub fn worktree_config_enabled(common_dir: &Path) -> bool {
1212 let path = common_dir.join("config");
1213 let Ok(content) = fs::read_to_string(&path) else {
1214 return false;
1215 };
1216 let mut in_extensions = false;
1217 for raw_line in content.lines() {
1218 let mut line = raw_line.trim();
1219 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
1220 continue;
1221 }
1222 if line.starts_with('[') {
1223 let Some(end_idx) = line.find(']') else {
1224 continue;
1225 };
1226 let section = line[1..end_idx].trim();
1227 let section_name = section
1228 .split_whitespace()
1229 .next()
1230 .unwrap_or_default()
1231 .to_ascii_lowercase();
1232 in_extensions = section_name == "extensions";
1233 let remainder = line[end_idx + 1..].trim();
1234 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
1235 continue;
1236 }
1237 line = remainder;
1238 }
1239 if in_extensions {
1240 let Some((key, value)) = line.split_once('=') else {
1241 continue;
1242 };
1243 if key.trim().eq_ignore_ascii_case("worktreeconfig") {
1244 let v = value.trim();
1245 return v.eq_ignore_ascii_case("true")
1246 || v.eq_ignore_ascii_case("yes")
1247 || v.eq_ignore_ascii_case("on")
1248 || v == "1";
1249 }
1250 }
1251 }
1252 false
1253}
1254
1255fn open_or_create_config_file(path: &Path, scope: ConfigScope) -> Result<ConfigFile> {
1256 match ConfigFile::from_path(path, scope)? {
1257 Some(f) => Ok(f),
1258 None => {
1259 if let Some(parent) = path.parent() {
1260 fs::create_dir_all(parent).map_err(Error::Io)?;
1261 }
1262 ConfigFile::parse(path, "", scope)
1263 }
1264 }
1265}
1266
1267fn config_file_bool_true(cfg: &ConfigFile, key: &str) -> bool {
1268 cfg.get(key).is_some_and(|v| {
1269 matches!(
1270 v.trim().to_ascii_lowercase().as_str(),
1271 "true" | "yes" | "on" | "1"
1272 )
1273 })
1274}
1275
1276pub fn init_worktree_config(git_dir: &Path) -> Result<()> {
1286 let common_dir = common_git_dir_for_config(git_dir);
1287 let common_config_path = common_dir.join("config");
1288 let worktree_config_path = git_dir.join("config.worktree");
1289
1290 if worktree_config_enabled(&common_dir) {
1291 if !worktree_config_path.exists() {
1292 if let Some(parent) = worktree_config_path.parent() {
1293 fs::create_dir_all(parent).map_err(Error::Io)?;
1294 }
1295 fs::write(&worktree_config_path, "").map_err(Error::Io)?;
1296 }
1297 return Ok(());
1298 }
1299
1300 let mut common_cfg = open_or_create_config_file(&common_config_path, ConfigScope::Local)?;
1301 common_cfg.set("extensions.worktreeConfig", "true")?;
1302
1303 let mut wt_cfg = open_or_create_config_file(&worktree_config_path, ConfigScope::Worktree)?;
1304
1305 if config_file_bool_true(&common_cfg, "core.bare") {
1306 wt_cfg.set("core.bare", "true")?;
1307 common_cfg.unset("core.bare")?;
1308 }
1309 if let Some(worktree) = common_cfg.get("core.worktree") {
1310 wt_cfg.set("core.worktree", &worktree)?;
1311 common_cfg.unset("core.worktree")?;
1312 }
1313
1314 common_cfg.write()?;
1315 wt_cfg.write()?;
1316 Ok(())
1317}
1318
1319pub fn early_config_ignore_repo_reason(common_dir: &Path) -> Option<String> {
1323 const GIT_REPO_VERSION_READ: u32 = 1;
1324 let path = common_dir.join("config");
1325 let content = fs::read_to_string(&path).ok()?;
1326 let mut version = 0u32;
1327 let mut in_core = false;
1328 for raw_line in content.lines() {
1329 let mut line = raw_line.trim();
1330 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
1331 continue;
1332 }
1333 if line.starts_with('[') {
1334 let Some(end_idx) = line.find(']') else {
1335 continue;
1336 };
1337 let section = line[1..end_idx].trim();
1338 let section_name = section
1339 .split_whitespace()
1340 .next()
1341 .unwrap_or_default()
1342 .to_ascii_lowercase();
1343 in_core = section_name == "core";
1344 let remainder = line[end_idx + 1..].trim();
1345 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
1346 continue;
1347 }
1348 line = remainder;
1349 }
1350 if in_core {
1351 if let Some((key, value)) = line.split_once('=') {
1352 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
1353 if let Ok(v) = value.trim().parse::<u32>() {
1354 version = v;
1355 }
1356 }
1357 }
1358 }
1359 }
1360 if version > GIT_REPO_VERSION_READ {
1361 Some(format!(
1362 "Expected git repo version <= {GIT_REPO_VERSION_READ}, found {version}"
1363 ))
1364 } else {
1365 None
1366 }
1367}
1368
1369fn path_for_ceiling_compare(path: &Path) -> String {
1370 let path = path.to_string_lossy();
1371 #[cfg(windows)]
1372 {
1373 path.replace('\\', "/")
1374 }
1375 #[cfg(not(windows))]
1376 {
1377 path.into_owned()
1378 }
1379}
1380
1381fn offset_1st_component(path: &str) -> usize {
1382 if path.starts_with('/') {
1383 1
1384 } else {
1385 0
1386 }
1387}
1388
1389fn longest_ancestor_length(path: &str, ceilings: &[String]) -> Option<usize> {
1391 if path == "/" {
1392 return None;
1393 }
1394 let mut max_len: Option<usize> = None;
1395 for ceil in ceilings {
1396 let mut len = ceil.len();
1397 while len > 0 && ceil.as_bytes().get(len - 1) == Some(&b'/') {
1398 len -= 1;
1399 }
1400 if len == 0 {
1401 continue;
1402 }
1403 if path.len() <= len + 1 {
1404 continue;
1405 }
1406 if !path.starts_with(&ceil[..len]) {
1407 continue;
1408 }
1409 if path.as_bytes().get(len) != Some(&b'/') {
1410 continue;
1411 }
1412 if path.as_bytes().get(len + 1).is_none() {
1413 continue;
1414 }
1415 max_len = Some(max_len.map_or(len, |m| m.max(len)));
1416 }
1417 max_len
1418}
1419
1420fn repository_config_path(git_dir: &Path) -> Option<PathBuf> {
1422 let local = git_dir.join("config");
1423 if local.exists() {
1424 return Some(local);
1425 }
1426 let common = resolve_common_dir(git_dir)?;
1427 let shared = common.join("config");
1428 if shared.exists() {
1429 Some(shared)
1430 } else {
1431 None
1432 }
1433}
1434
1435pub fn validate_repo_format(git_dir: &Path) -> Result<()> {
1441 validate_repository_format(git_dir)
1442}
1443
1444fn validate_repository_format(git_dir: &Path) -> Result<()> {
1445 let Some(config_path) = repository_config_path(git_dir) else {
1446 return Ok(());
1447 };
1448
1449 let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
1450 let parsed = parse_repository_format(&content, &config_path)?;
1451
1452 if parsed.repo_version > 1 {
1453 return Err(Error::UnsupportedRepositoryFormatVersion(
1454 parsed.repo_version,
1455 ));
1456 }
1457
1458 if let Some(raw) = parsed.ref_storage.as_deref() {
1459 let lower = raw.to_ascii_lowercase();
1460 let name = lower
1461 .split_once(':')
1462 .map(|(prefix, _)| prefix)
1463 .unwrap_or(lower.as_str());
1464 if !matches!(name, "files" | "reftable") {
1465 return Err(Error::Message(format!(
1466 "error: invalid value for 'extensions.refstorage': '{raw}'"
1467 )));
1468 }
1469 }
1470
1471 if let Some(msg) = parsed.format_error_message() {
1472 return Err(Error::Message(msg));
1473 }
1474
1475 Ok(())
1476}
1477
1478struct RepositoryFormat {
1481 repo_version: u32,
1483 extensions: BTreeSet<String>,
1485 ref_storage: Option<String>,
1487}
1488
1489impl RepositoryFormat {
1490 fn format_error_message(&self) -> Option<String> {
1497 let mut v1_only_found: Vec<&str> = Vec::new();
1504 let mut unknown_found: Vec<&str> = Vec::new();
1505 for extension in &self.extensions {
1506 match extension.as_str() {
1507 "noop" | "preciousobjects" | "partialclone" | "worktreeconfig" => {}
1509 "noop-v1"
1511 | "objectformat"
1512 | "compatobjectformat"
1513 | "refstorage"
1514 | "relativeworktrees"
1515 | "submodulepathconfig" => {
1516 if self.repo_version == 0 {
1517 v1_only_found.push(extension);
1518 }
1519 }
1520 _ => {
1522 if self.repo_version >= 1 {
1523 unknown_found.push(extension);
1524 }
1525 }
1526 }
1527 }
1528
1529 if !unknown_found.is_empty() {
1530 let mut msg = if unknown_found.len() == 1 {
1531 "unknown repository extension found:".to_owned()
1532 } else {
1533 "unknown repository extensions found:".to_owned()
1534 };
1535 for ext in &unknown_found {
1536 msg.push_str(&format!("\n\t{ext}"));
1537 }
1538 return Some(msg);
1539 }
1540
1541 if !v1_only_found.is_empty() {
1542 let mut msg = if v1_only_found.len() == 1 {
1543 "repo version is 0, but v1-only extension found:".to_owned()
1544 } else {
1545 "repo version is 0, but v1-only extensions found:".to_owned()
1546 };
1547 for ext in &v1_only_found {
1548 msg.push_str(&format!("\n\t{ext}"));
1549 }
1550 return Some(msg);
1551 }
1552
1553 None
1554 }
1555}
1556
1557fn parse_repository_format(content: &str, config_path: &Path) -> Result<RepositoryFormat> {
1564 let mut in_core = false;
1565 let mut in_extensions = false;
1566 let mut repo_version = 0u32;
1567 let mut extensions = BTreeSet::new();
1568 let mut ref_storage: Option<String> = None;
1569
1570 for raw_line in content.lines() {
1571 let mut line = raw_line.trim();
1572 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
1573 continue;
1574 }
1575
1576 if line.starts_with('[') {
1577 let Some(end_idx) = line.find(']') else {
1578 return Err(Error::ConfigError(format!(
1579 "invalid config in {}",
1580 config_path.display()
1581 )));
1582 };
1583
1584 let section = line[1..end_idx].trim();
1585 let section_name = section
1586 .split_whitespace()
1587 .next()
1588 .unwrap_or_default()
1589 .to_ascii_lowercase();
1590 in_core = section_name == "core";
1591 in_extensions = section_name == "extensions";
1592
1593 let remainder = line[end_idx + 1..].trim();
1594 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
1595 continue;
1596 }
1597 line = remainder;
1598 }
1599
1600 if in_core {
1601 if let Some((key, value)) = line.split_once('=') {
1602 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
1603 if let Ok(v) = value.trim().parse::<u32>() {
1605 repo_version = v;
1606 }
1607 }
1608 }
1609 }
1610
1611 if in_extensions {
1612 let (key, value) = if let Some((key, value)) = line.split_once('=') {
1613 (key.trim(), Some(value.trim()))
1614 } else {
1615 (line, None)
1616 };
1617 if key.eq_ignore_ascii_case("refstorage") {
1618 ref_storage = value.map(str::to_owned);
1619 }
1620 if !key.is_empty() {
1621 extensions.insert(key.to_ascii_lowercase());
1622 }
1623 }
1624 }
1625
1626 Ok(RepositoryFormat {
1627 repo_version,
1628 extensions,
1629 ref_storage,
1630 })
1631}
1632
1633pub fn repository_format_warning(git_dir: &Path) -> Result<Option<String>> {
1650 const GIT_REPO_VERSION_READ: u32 = 1;
1651 let Some(config_path) = repository_config_path(git_dir) else {
1652 return Ok(None);
1653 };
1654 let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
1655 let parsed = parse_repository_format(&content, &config_path)?;
1656
1657 if parsed.repo_version > GIT_REPO_VERSION_READ {
1658 return Ok(Some(format!(
1659 "Expected git repo version <= {GIT_REPO_VERSION_READ}, found {}",
1660 parsed.repo_version
1661 )));
1662 }
1663
1664 Ok(parsed.format_error_message())
1665}
1666
1667struct DiscoveredAt {
1673 repo: Repository,
1674 gitfile: Option<PathBuf>,
1676}
1677
1678fn try_open_at(dir: &Path) -> Result<Option<DiscoveredAt>> {
1679 let dot_git = dir.join(".git");
1680
1681 #[cfg(unix)]
1684 {
1685 use std::os::unix::fs::FileTypeExt;
1686 if let Ok(meta) = fs::symlink_metadata(&dot_git) {
1687 let ft = meta.file_type();
1688 if ft.is_fifo() || ft.is_socket() || ft.is_block_device() || ft.is_char_device() {
1689 return Err(Error::NotARepository(format!(
1690 "invalid gitfile format: {} is not a regular file",
1691 dot_git.display()
1692 )));
1693 }
1694 if ft.is_symlink() {
1695 if let Ok(target_meta) = fs::metadata(&dot_git) {
1696 let tft = target_meta.file_type();
1697 if tft.is_fifo()
1698 || tft.is_socket()
1699 || tft.is_block_device()
1700 || tft.is_char_device()
1701 {
1702 return Err(Error::NotARepository(format!(
1703 "invalid gitfile format: {} is not a regular file",
1704 dot_git.display()
1705 )));
1706 }
1707 }
1708 }
1709 }
1710 }
1711
1712 if dot_git.is_file() {
1713 let content =
1715 fs::read_to_string(&dot_git).map_err(|e| Error::NotARepository(e.to_string()))?;
1716 let git_dir = parse_gitfile(&content, dir)?;
1717 let mut repo = Repository::open_skipping_format_validation(&git_dir, Some(dir))?;
1718 if resolve_common_dir(&git_dir).is_some() {
1722 let cwd = env::current_dir().map_err(Error::Io)?;
1723 if repo.work_tree.is_some() && !is_inside_work_tree(&repo, &cwd) {
1724 let root = if dir.is_absolute() {
1725 dir.to_path_buf()
1726 } else {
1727 cwd.join(dir)
1728 };
1729 repo.work_tree = Some(root.canonicalize().unwrap_or(root));
1730 }
1731 }
1732 let root = if dir.is_absolute() {
1733 dir.to_path_buf()
1734 } else {
1735 env::current_dir().map_err(Error::Io)?.join(dir)
1736 };
1737 repo.discovery_root = Some(root.canonicalize().unwrap_or(root));
1738 repo.discovery_via_gitfile = true;
1739 warn_core_bare_worktree_conflict(&git_dir);
1740 return Ok(Some(DiscoveredAt {
1741 repo,
1742 gitfile: Some(dot_git.clone()),
1743 }));
1744 }
1745
1746 if dot_git.is_dir() {
1747 let open_path = if dot_git.is_symlink() {
1751 dot_git.read_link().unwrap_or_else(|_| dot_git.clone())
1753 } else {
1754 dot_git.clone()
1755 };
1756 match Repository::open_skipping_format_validation(&open_path, Some(dir)) {
1759 Ok(mut repo) => {
1760 if dot_git.is_symlink() {
1763 let abs_dot_git = if dot_git.is_absolute() {
1764 dot_git
1765 } else {
1766 dir.join(".git")
1767 };
1768 repo.git_dir = abs_dot_git;
1769 }
1770 let root = if dir.is_absolute() {
1771 dir.to_path_buf()
1772 } else {
1773 env::current_dir().map_err(Error::Io)?.join(dir)
1774 };
1775 repo.discovery_root = Some(root.canonicalize().unwrap_or(root));
1776 repo.discovery_via_gitfile = false;
1777 return Ok(Some(DiscoveredAt {
1778 repo,
1779 gitfile: None,
1780 }));
1781 }
1782 Err(Error::NotARepository(_)) | Err(Error::ConfigError(_)) => return Ok(None),
1783 Err(Error::Message(ref msg)) if msg.contains("bad config") => return Ok(None),
1784 Err(e) => return Err(e),
1785 }
1786 }
1787
1788 if dir.join("HEAD").is_file() && dir.join("commondir").is_file() {
1791 maybe_trace_implicit_bare_repository(dir);
1792 let repo = Repository::open(dir, None)?;
1793 warn_core_bare_worktree_conflict(dir);
1794 return Ok(Some(DiscoveredAt {
1795 repo,
1796 gitfile: None,
1797 }));
1798 }
1799
1800 if dir.join("objects").is_dir() && dir.join("HEAD").is_file() {
1802 maybe_trace_implicit_bare_repository(dir);
1803 if !is_inside_dot_git(dir) {
1807 if let Ok(cfg) = crate::config::ConfigSet::load(None, true) {
1808 if let Some(val) = cfg.get("safe.bareRepository") {
1809 if val.eq_ignore_ascii_case("explicit") {
1810 return Err(Error::ForbiddenBareRepository(dir.display().to_string()));
1811 }
1812 }
1813 }
1814 }
1815 let repo = Repository::open(dir, None)?;
1816 warn_core_bare_worktree_conflict(dir);
1817 return Ok(Some(DiscoveredAt {
1818 repo,
1819 gitfile: None,
1820 }));
1821 }
1822
1823 Ok(None)
1824}
1825
1826fn is_inside_dot_git(path: &Path) -> bool {
1827 path.components().any(|c| c.as_os_str() == ".git")
1828}
1829
1830fn maybe_trace_implicit_bare_repository(dir: &Path) {
1831 let path = match std::env::var("GIT_TRACE2_PERF") {
1832 Ok(p) if !p.is_empty() => p,
1833 _ => return,
1834 };
1835
1836 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
1837 let _ = writeln!(file, "setup: implicit-bare-repository:{}", dir.display());
1838 }
1839}
1840
1841fn safe_directory_effective_values(git_dir: &Path) -> Vec<String> {
1844 let cfg = crate::config::ConfigSet::load(Some(git_dir), true)
1845 .unwrap_or_else(|_| crate::config::ConfigSet::new());
1846 let mut values: Vec<String> = Vec::new();
1847 for e in cfg.entries() {
1848 if e.key == "safe.directory"
1849 && e.scope != crate::config::ConfigScope::Local
1850 && e.scope != crate::config::ConfigScope::Worktree
1851 {
1852 values.push(e.value.clone().unwrap_or_else(|| "true".to_owned()));
1853 }
1854 }
1855 let mut effective: Vec<String> = Vec::new();
1856 for v in values {
1857 if v.is_empty() {
1858 effective.clear();
1859 } else {
1860 effective.push(v);
1861 }
1862 }
1863 effective
1864}
1865
1866fn ensure_safe_directory_allows(git_dir: &Path, checked: &Path) -> Result<()> {
1867 let effective = safe_directory_effective_values(git_dir);
1868 let checked_s = checked.to_string_lossy().to_string();
1869 if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
1870 eprintln!("debug-safe-directory values={:?}", effective);
1871 }
1872 if effective
1873 .iter()
1874 .any(|v| safe_directory_matches(v, &checked_s))
1875 {
1876 return Ok(());
1877 }
1878 Err(Error::DubiousOwnership(checked_s))
1879}
1880
1881#[cfg(unix)]
1882fn path_lstat_uid(path: &Path) -> std::io::Result<u32> {
1883 use std::os::unix::fs::MetadataExt;
1884 let meta = fs::symlink_metadata(path)?;
1885 Ok(meta.uid())
1886}
1887
1888#[cfg(unix)]
1889fn extract_uid_from_env(name: &str) -> Option<u32> {
1890 let raw = std::env::var(name).ok()?;
1891 if raw.is_empty() {
1892 return None;
1893 }
1894 raw.parse::<u32>().ok()
1895}
1896
1897#[cfg(unix)]
1900fn ensure_valid_ownership(
1901 gitfile: Option<&Path>,
1902 worktree: Option<&Path>,
1903 gitdir: &Path,
1904) -> Result<()> {
1905 const ROOT_UID: u32 = 0;
1906
1907 fn owned_by_effective_user(path: &Path) -> std::io::Result<bool> {
1908 let st_uid = path_lstat_uid(path)?;
1909 let mut euid = nix::unistd::geteuid().as_raw();
1910 if euid == ROOT_UID {
1911 if st_uid == ROOT_UID {
1912 return Ok(true);
1913 }
1914 if let Some(sudo_uid) = extract_uid_from_env("SUDO_UID") {
1915 euid = sudo_uid;
1916 }
1917 }
1918 Ok(st_uid == euid)
1919 }
1920
1921 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
1922 .ok()
1923 .map(|v| {
1924 let lower = v.to_ascii_lowercase();
1925 v == "1" || lower == "true" || lower == "yes" || lower == "on"
1926 })
1927 .unwrap_or(false);
1928 if !assume_different {
1929 let gitfile_ok = gitfile
1930 .map(owned_by_effective_user)
1931 .transpose()?
1932 .unwrap_or(true);
1933 let wt_ok = match worktree {
1936 None => true,
1937 Some(wt) => match owned_by_effective_user(wt) {
1938 Ok(ok) => ok,
1939 Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
1940 Err(e) => return Err(Error::Io(e)),
1941 },
1942 };
1943 let gd_ok = owned_by_effective_user(gitdir)?;
1944 if gitfile_ok && wt_ok && gd_ok {
1945 return Ok(());
1946 }
1947 }
1948
1949 let data_path = if let Some(wt) = worktree {
1950 wt.canonicalize().unwrap_or_else(|_| wt.to_path_buf())
1951 } else {
1952 gitdir
1953 .canonicalize()
1954 .unwrap_or_else(|_| gitdir.to_path_buf())
1955 };
1956 ensure_safe_directory_allows(gitdir, &data_path)
1957}
1958
1959#[cfg(not(unix))]
1960fn ensure_valid_ownership(
1961 _gitfile: Option<&Path>,
1962 _worktree: Option<&Path>,
1963 _gitdir: &Path,
1964) -> Result<()> {
1965 Ok(())
1966}
1967
1968impl Repository {
1969 pub fn enforce_safe_directory(&self) -> Result<()> {
1975 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
1976 .ok()
1977 .map(|v| {
1978 let lower = v.to_ascii_lowercase();
1979 v == "1" || lower == "true" || lower == "yes" || lower == "on"
1980 })
1981 .unwrap_or(false);
1982 if !assume_different {
1983 return Ok(());
1984 }
1985
1986 if self.explicit_git_dir {
1987 return Ok(());
1988 }
1989
1990 let checked = if let Some(wt) = &self.work_tree {
1994 let cwd = std::env::current_dir().ok();
1995 if let Some(cwd) = cwd {
1996 if cwd
1997 .canonicalize()
1998 .ok()
1999 .is_some_and(|c| c.starts_with(&self.git_dir))
2000 {
2001 self.git_dir
2002 .canonicalize()
2003 .unwrap_or_else(|_| self.git_dir.clone())
2004 } else {
2005 wt.canonicalize().unwrap_or_else(|_| wt.clone())
2006 }
2007 } else {
2008 wt.canonicalize().unwrap_or_else(|_| wt.clone())
2009 }
2010 } else {
2011 self.git_dir
2012 .canonicalize()
2013 .unwrap_or_else(|_| self.git_dir.clone())
2014 };
2015
2016 if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
2017 eprintln!(
2018 "debug-safe-directory checked={} git_dir={} work_tree={:?} cwd={:?}",
2019 checked.display(),
2020 self.git_dir.display(),
2021 self.work_tree,
2022 std::env::current_dir().ok()
2023 );
2024 }
2025 self.enforce_safe_directory_checked(&checked)
2026 }
2027
2028 pub fn enforce_safe_directory_git_dir(&self) -> Result<()> {
2033 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
2034 .ok()
2035 .map(|v| {
2036 let lower = v.to_ascii_lowercase();
2037 v == "1" || lower == "true" || lower == "yes" || lower == "on"
2038 })
2039 .unwrap_or(false);
2040 if !assume_different {
2041 return Ok(());
2042 }
2043 let checked = self
2044 .git_dir
2045 .canonicalize()
2046 .unwrap_or_else(|_| self.git_dir.clone());
2047 if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
2048 eprintln!(
2049 "debug-safe-directory(gitdir) checked={} git_dir={} work_tree={:?}",
2050 checked.display(),
2051 self.git_dir.display(),
2052 self.work_tree
2053 );
2054 }
2055 self.enforce_safe_directory_checked(&checked)
2056 }
2057
2058 pub fn enforce_safe_directory_git_dir_with_path(&self, checked: &Path) -> Result<()> {
2060 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
2061 .ok()
2062 .map(|v| {
2063 let lower = v.to_ascii_lowercase();
2064 v == "1" || lower == "true" || lower == "yes" || lower == "on"
2065 })
2066 .unwrap_or(false);
2067 if !assume_different {
2068 return Ok(());
2069 }
2070 self.enforce_safe_directory_checked(checked)
2071 }
2072
2073 fn enforce_safe_directory_checked(&self, checked: &Path) -> Result<()> {
2074 ensure_safe_directory_allows(&self.git_dir, checked)
2075 }
2076
2077 pub fn verify_safe_for_clone_source(&self) -> Result<()> {
2083 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
2084 .ok()
2085 .map(|v| {
2086 let lower = v.to_ascii_lowercase();
2087 v == "1" || lower == "true" || lower == "yes" || lower == "on"
2088 })
2089 .unwrap_or(false);
2090 if assume_different {
2091 self.enforce_safe_directory_git_dir()
2092 } else {
2093 #[cfg(unix)]
2094 {
2095 ensure_valid_ownership(None, None, &self.git_dir)
2096 }
2097 #[cfg(not(unix))]
2098 {
2099 Ok(())
2100 }
2101 }
2102 }
2103}
2104
2105fn normalize_fs_path(raw: &str) -> String {
2106 use std::path::Component;
2107 let p = std::path::Path::new(raw);
2108 let mut parts: Vec<String> = Vec::new();
2109 let mut absolute = false;
2110 for c in p.components() {
2111 match c {
2112 Component::RootDir => {
2113 absolute = true;
2114 parts.clear();
2115 }
2116 Component::CurDir => {}
2117 Component::ParentDir => {
2118 if !parts.is_empty() {
2119 parts.pop();
2120 }
2121 }
2122 Component::Normal(s) => parts.push(s.to_string_lossy().to_string()),
2123 Component::Prefix(_) => {}
2124 }
2125 }
2126 let mut out = if absolute {
2127 String::from("/")
2128 } else {
2129 String::new()
2130 };
2131 out.push_str(&parts.join("/"));
2132 out
2133}
2134
2135fn safe_directory_matches(config_value: &str, checked: &str) -> bool {
2136 if config_value == "*" {
2137 return true;
2138 }
2139 if config_value == "." {
2140 if let Ok(cwd) = std::env::current_dir() {
2142 let cwd_s = normalize_fs_path(&cwd.to_string_lossy());
2143 let checked_s = normalize_fs_path(checked);
2144 return cwd_s == checked_s;
2145 }
2146 return false;
2147 }
2148
2149 let canonicalize_or_normalize = |raw: &str| -> String {
2150 let p = std::path::Path::new(raw);
2151 if p.exists() {
2152 p.canonicalize()
2153 .map(|c| c.to_string_lossy().to_string())
2154 .map(|s| normalize_fs_path(&s))
2155 .unwrap_or_else(|_| normalize_fs_path(raw))
2156 } else {
2157 normalize_fs_path(raw)
2158 }
2159 };
2160
2161 let config_norm = canonicalize_or_normalize(config_value);
2162 let checked_norm = normalize_fs_path(checked);
2163
2164 if config_norm.ends_with("/*") {
2165 let prefix_raw = &config_norm[..config_norm.len() - 2];
2166 let prefix_norm = canonicalize_or_normalize(prefix_raw);
2167 let mut prefix = prefix_norm;
2168 if !prefix.ends_with('/') {
2169 prefix.push('/');
2170 }
2171 return checked_norm.starts_with(&prefix);
2172 }
2173
2174 config_norm == checked_norm
2175}
2176
2177fn warn_core_bare_worktree_conflict(git_dir: &Path) {
2178 if env::var("GIT_WORK_TREE")
2179 .ok()
2180 .filter(|s| !s.trim().is_empty())
2181 .is_some()
2182 {
2183 return;
2184 }
2185 static WARNED_DIRS: Mutex<Option<HashSet<String>>> = Mutex::new(None);
2186 if let Ok((bare, wt)) = read_core_bare_and_worktree(git_dir) {
2187 if bare && wt.is_some() {
2188 let key = git_dir
2189 .canonicalize()
2190 .unwrap_or_else(|_| git_dir.to_path_buf())
2191 .to_string_lossy()
2192 .to_string();
2193 let mut guard = WARNED_DIRS.lock().unwrap_or_else(|e| e.into_inner());
2194 let set = guard.get_or_insert_with(HashSet::new);
2195 if set.insert(key) {
2196 eprintln!("warning: core.bare and core.worktree do not make sense");
2197 }
2198 }
2199 }
2200}
2201
2202fn read_core_bare_and_worktree(git_dir: &Path) -> Result<(bool, Option<String>)> {
2203 let Some(config_path) = repository_config_path(git_dir) else {
2204 return Ok((false, None));
2205 };
2206 let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
2207 let mut in_core = false;
2208 let mut bare = false;
2209 let mut worktree: Option<String> = None;
2210 for raw_line in content.lines() {
2211 let line = raw_line.trim();
2212 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
2213 continue;
2214 }
2215 if line.starts_with('[') {
2216 in_core = line.eq_ignore_ascii_case("[core]");
2217 continue;
2218 }
2219 if !in_core {
2220 continue;
2221 }
2222 if let Some((k, v)) = line.split_once('=') {
2223 let key = k.trim();
2224 let val = v.trim();
2225 if key.eq_ignore_ascii_case("bare") {
2226 bare = val.eq_ignore_ascii_case("true");
2227 } else if key.eq_ignore_ascii_case("worktree") {
2228 worktree = Some(val.to_owned());
2229 }
2230 }
2231 }
2232 Ok((bare, worktree))
2233}
2234
2235fn validate_git_work_tree_path(path: &Path) -> Result<()> {
2238 if !path.is_absolute() {
2239 return Ok(());
2240 }
2241 let comps: Vec<Component<'_>> = path.components().collect();
2242 let Some(last_normal_idx) = comps
2243 .iter()
2244 .enumerate()
2245 .rev()
2246 .find_map(|(i, c)| matches!(c, Component::Normal(_)).then_some(i))
2247 else {
2248 return Ok(());
2249 };
2250 let mut cur = PathBuf::new();
2251 for (i, comp) in comps.iter().enumerate() {
2252 match comp {
2253 Component::Prefix(p) => cur.push(p.as_os_str()),
2254 Component::RootDir => cur.push(comp.as_os_str()),
2255 Component::CurDir => {}
2256 Component::ParentDir => {
2257 let _ = cur.pop();
2258 }
2259 Component::Normal(seg) => {
2260 cur.push(seg);
2261 if i != last_normal_idx && !cur.exists() {
2262 return Err(Error::PathError(format!(
2263 "Invalid path '{}': No such file or directory",
2264 cur.display()
2265 )));
2266 }
2267 }
2268 }
2269 }
2270 Ok(())
2271}
2272
2273fn resolve_core_worktree_path(git_dir: &Path, raw: &str) -> Result<PathBuf> {
2274 let p = Path::new(raw);
2275 if p.is_absolute() {
2276 return Ok(p.canonicalize().unwrap_or_else(|_| p.to_path_buf()));
2277 }
2278 let old = env::current_dir().map_err(Error::Io)?;
2279 env::set_current_dir(git_dir).map_err(Error::Io)?;
2280 env::set_current_dir(raw).map_err(Error::Io)?;
2281 let resolved = env::current_dir().map_err(Error::Io)?;
2282 env::set_current_dir(&old).map_err(Error::Io)?;
2283 Ok(resolved.canonicalize().unwrap_or(resolved))
2284}
2285
2286fn resolve_git_dir_env_path(git_dir: &Path) -> Result<PathBuf> {
2288 if git_dir.is_file() {
2289 let content =
2290 fs::read_to_string(git_dir).map_err(|e| Error::NotARepository(e.to_string()))?;
2291 let base = git_dir
2292 .parent()
2293 .ok_or_else(|| Error::NotARepository(git_dir.display().to_string()))?;
2294 return parse_gitfile(&content, base);
2295 }
2296 Ok(git_dir.to_path_buf())
2297}
2298
2299pub fn resolve_git_directory_arg(git_dir: &Path) -> Result<PathBuf> {
2305 resolve_git_dir_env_path(git_dir)
2306}
2307
2308pub fn resolve_dot_git(dot_git: &Path) -> Result<PathBuf> {
2314 if dot_git.is_dir() {
2315 return dot_git
2316 .canonicalize()
2317 .map_err(|_| Error::NotARepository(dot_git.display().to_string()));
2318 }
2319 if dot_git.is_file() {
2320 let content =
2321 fs::read_to_string(dot_git).map_err(|e| Error::NotARepository(e.to_string()))?;
2322 let base = dot_git
2323 .parent()
2324 .ok_or_else(|| Error::NotARepository(dot_git.display().to_string()))?;
2325 return parse_gitfile(&content, base);
2326 }
2327 Err(Error::NotARepository(dot_git.display().to_string()))
2328}
2329
2330fn parse_gitfile(content: &str, base: &Path) -> Result<PathBuf> {
2332 for line in content.lines() {
2333 if let Some(rest) = line.strip_prefix("gitdir:") {
2334 let rel = rest.trim();
2335 let path = if Path::new(rel).is_absolute() {
2336 PathBuf::from(rel)
2337 } else {
2338 base.join(rel)
2339 };
2340 if !path.exists() {
2341 return Err(Error::NotARepository(path.display().to_string()));
2342 }
2343 return Ok(path);
2344 }
2345 }
2346 Err(Error::NotARepository("invalid gitfile format".to_owned()))
2347}
2348
2349fn write_fresh_git_directory(
2366 git_dir: &Path,
2367 bare: bool,
2368 initial_branch: &str,
2369 template_dir: Option<&Path>,
2370 ref_storage: &str,
2371 skip_hooks_and_info: bool,
2372) -> Result<()> {
2373 let mut subs = vec![
2374 "objects",
2375 "objects/info",
2376 "objects/pack",
2377 "refs",
2378 "refs/heads",
2379 "refs/tags",
2380 ];
2381 if !bare && !skip_hooks_and_info {
2382 subs.push("info");
2383 subs.push("hooks");
2384 }
2385 for sub in subs {
2386 fs::create_dir_all(git_dir.join(sub))?;
2387 }
2388
2389 if ref_storage == "reftable" {
2390 let reftable_dir = git_dir.join("reftable");
2391 fs::create_dir_all(&reftable_dir)?;
2392 let tables_list = reftable_dir.join("tables.list");
2393 if !tables_list.exists() {
2394 fs::write(&tables_list, "")?;
2395 }
2396 }
2397
2398 if let Some(tmpl) = template_dir {
2399 if tmpl.is_dir() {
2400 copy_template(tmpl, git_dir)?;
2401 }
2402 }
2403
2404 let head_content = format!("ref: refs/heads/{initial_branch}\n");
2405 fs::write(git_dir.join("HEAD"), head_content)?;
2406
2407 let needs_extensions = ref_storage == "reftable";
2408 let repo_version = if needs_extensions { 1 } else { 0 };
2409
2410 let mut config_content = String::from("[core]\n");
2411 config_content.push_str(&format!("\trepositoryformatversion = {repo_version}\n"));
2412 config_content.push_str("\tfilemode = true\n");
2413 if bare {
2414 config_content.push_str("\tbare = true\n");
2415 } else {
2416 config_content.push_str("\tbare = false\n");
2417 config_content.push_str("\tlogallrefupdates = true\n");
2418 }
2419 if needs_extensions {
2420 config_content.push_str("[extensions]\n");
2421 config_content.push_str("\trefStorage = reftable\n");
2422 }
2423 fs::write(git_dir.join("config"), config_content)?;
2424
2425 if let Some(tmpl) = template_dir {
2427 if tmpl.is_dir() {
2428 let tmpl_config = tmpl.join("config");
2429 if tmpl_config.is_file() {
2430 let tmpl_text = fs::read_to_string(&tmpl_config)?;
2431 let tmpl_parsed = ConfigFile::parse(&tmpl_config, &tmpl_text, ConfigScope::Local)?;
2432 let dest_path = git_dir.join("config");
2433 let dest_text = fs::read_to_string(&dest_path)?;
2434 let mut dest_parsed =
2435 ConfigFile::parse(&dest_path, &dest_text, ConfigScope::Local)?;
2436 for e in &tmpl_parsed.entries {
2437 if e.key == "core.bare" {
2439 continue;
2440 }
2441 if let Some(v) = &e.value {
2442 let _ = dest_parsed.set(&e.key, v);
2443 } else {
2444 let _ = dest_parsed.set(&e.key, "true");
2445 }
2446 }
2447 dest_parsed.write()?;
2448 }
2449 }
2450 }
2451
2452 fs::write(
2453 git_dir.join("description"),
2454 "Unnamed repository; edit this file 'description' to name the repository.\n",
2455 )?;
2456 Ok(())
2457}
2458
2459pub fn init_repository_separate_git_dir(
2468 work_tree: &Path,
2469 git_dir: &Path,
2470 initial_branch: &str,
2471 template_dir: Option<&Path>,
2472 ref_storage: &str,
2473) -> Result<Repository> {
2474 let skip_hooks_info = template_dir.is_some_and(|p| p.as_os_str().is_empty());
2475 fs::create_dir_all(work_tree)?;
2476 fs::create_dir_all(git_dir)?;
2477 write_fresh_git_directory(
2478 git_dir,
2479 false,
2480 initial_branch,
2481 template_dir,
2482 ref_storage,
2483 skip_hooks_info,
2484 )?;
2485
2486 let gitfile = work_tree.join(".git");
2492 let abs_git_dir = fs::canonicalize(git_dir).unwrap_or_else(|_| git_dir.to_path_buf());
2493 let abs_git_dir = abs_git_dir.to_string_lossy().replace('\\', "/");
2494 fs::write(gitfile, format!("gitdir: {abs_git_dir}\n"))?;
2495
2496 Repository::open(git_dir, Some(work_tree))
2497}
2498
2499pub fn ensure_core_bare(git_dir: &Path) -> Result<()> {
2514 let path = git_dir.join("config");
2515 let text = fs::read_to_string(&path).unwrap_or_default();
2516 if text.lines().any(|l| {
2517 let t = l.trim();
2518 t == "bare = true" || t == "bare=true"
2519 }) {
2520 return Ok(());
2521 }
2522 let mut out = text;
2523 if !out.ends_with('\n') && !out.is_empty() {
2524 out.push('\n');
2525 }
2526 if !out.contains("[core]") {
2527 out.push_str("[core]\n");
2528 }
2529 out.push_str("\tbare = true\n");
2530 fs::write(path, out).map_err(Error::Io)
2531}
2532
2533pub fn init_bare_clone_minimal(
2534 git_dir: &Path,
2535 initial_branch: &str,
2536 ref_storage: &str,
2537) -> Result<()> {
2538 for sub in &[
2539 "objects",
2540 "objects/info",
2541 "objects/pack",
2542 "refs",
2543 "refs/heads",
2544 "refs/tags",
2545 ] {
2546 fs::create_dir_all(git_dir.join(sub))?;
2547 }
2548
2549 if ref_storage == "reftable" {
2550 let reftable_dir = git_dir.join("reftable");
2551 fs::create_dir_all(&reftable_dir)?;
2552 let tables_list = reftable_dir.join("tables.list");
2553 if !tables_list.exists() {
2554 fs::write(&tables_list, "")?;
2555 }
2556 }
2557
2558 let head_content = format!("ref: refs/heads/{initial_branch}\n");
2559 fs::write(git_dir.join("HEAD"), head_content)?;
2560
2561 let needs_extensions = ref_storage == "reftable";
2562 let repo_version = if needs_extensions { 1 } else { 0 };
2563 let mut config_content = String::from("[core]\n");
2564 config_content.push_str(&format!("\trepositoryformatversion = {repo_version}\n"));
2565 config_content.push_str("\tfilemode = true\n");
2566 config_content.push_str("\tbare = true\n");
2567 if needs_extensions {
2568 config_content.push_str("[extensions]\n");
2569 config_content.push_str("\trefStorage = reftable\n");
2570 }
2571 fs::write(git_dir.join("config"), config_content)?;
2572
2573 fs::write(
2574 git_dir.join("packed-refs"),
2575 "# pack-refs with: peeled fully-peeled sorted\n",
2576 )?;
2577 Ok(())
2578}
2579
2580pub fn init_repository(
2581 path: &Path,
2582 bare: bool,
2583 initial_branch: &str,
2584 template_dir: Option<&Path>,
2585 ref_storage: &str,
2586) -> Result<Repository> {
2587 let skip_hooks_info = !bare && template_dir.is_some_and(|p| p.as_os_str().is_empty());
2588 let git_dir = if bare {
2589 path.to_path_buf()
2590 } else {
2591 path.join(".git")
2592 };
2593
2594 if !bare {
2595 fs::create_dir_all(path)?;
2596 }
2597 fs::create_dir_all(&git_dir)?;
2598 write_fresh_git_directory(
2599 &git_dir,
2600 bare,
2601 initial_branch,
2602 template_dir,
2603 ref_storage,
2604 skip_hooks_info,
2605 )?;
2606
2607 let work_tree = if bare { None } else { Some(path) };
2608 Repository::open(&git_dir, work_tree)
2609}
2610
2611pub fn init_bare_with_env_worktree(
2620 git_dir: &Path,
2621 work_tree: &Path,
2622 initial_branch: &str,
2623 template_dir: Option<&Path>,
2624 ref_storage: &str,
2625) -> Result<Repository> {
2626 fs::create_dir_all(git_dir)?;
2627 fs::create_dir_all(work_tree)?;
2628 write_fresh_git_directory(
2629 git_dir,
2630 true,
2631 initial_branch,
2632 template_dir,
2633 ref_storage,
2634 false,
2635 )?;
2636 let work_tree_abs = fs::canonicalize(work_tree).unwrap_or_else(|_| work_tree.to_path_buf());
2637 let config_path = git_dir.join("config");
2638 let mut config = match ConfigFile::from_path(&config_path, ConfigScope::Local)? {
2639 Some(c) => c,
2640 None => ConfigFile::parse(&config_path, "", ConfigScope::Local)?,
2641 };
2642 config.set("core.worktree", &work_tree_abs.to_string_lossy())?;
2643 config.write()?;
2644 Repository::open(git_dir, Some(work_tree))
2645}
2646
2647pub fn init_repository_separate(
2652 work_tree: &Path,
2653 git_dir: &Path,
2654 initial_branch: &str,
2655 template_dir: Option<&Path>,
2656) -> Result<Repository> {
2657 fs::create_dir_all(work_tree)?;
2658 if git_dir.exists() {
2659 return Err(Error::PathError(format!(
2660 "git directory '{}' already exists",
2661 git_dir.display()
2662 )));
2663 }
2664
2665 for sub in &[
2666 "objects",
2667 "objects/info",
2668 "objects/pack",
2669 "refs",
2670 "refs/heads",
2671 "refs/tags",
2672 "info",
2673 "hooks",
2674 ] {
2675 fs::create_dir_all(git_dir.join(sub))?;
2676 }
2677
2678 if let Some(tmpl) = template_dir {
2679 if tmpl.is_dir() {
2680 copy_template(tmpl, git_dir)?;
2681 }
2682 }
2683
2684 fs::write(
2685 git_dir.join("HEAD"),
2686 format!("ref: refs/heads/{initial_branch}\n"),
2687 )?;
2688
2689 let work_tree_abs = fs::canonicalize(work_tree).unwrap_or_else(|_| work_tree.to_path_buf());
2690 let git_dir_abs = fs::canonicalize(git_dir).unwrap_or_else(|_| git_dir.to_path_buf());
2691 let config_content = format!(
2692 "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n\tlogallrefupdates = true\n\tworktree = {}\n",
2693 work_tree_abs.display()
2694 );
2695 fs::write(git_dir.join("config"), config_content)?;
2696 fs::write(
2697 git_dir.join("description"),
2698 "Unnamed repository; edit this file 'description' to name the repository.\n",
2699 )?;
2700
2701 let gitfile = work_tree.join(".git");
2702 fs::write(&gitfile, format!("gitdir: {}\n", git_dir_abs.display()))?;
2703
2704 Repository::open(git_dir, Some(work_tree))
2705}
2706
2707fn copy_template(src: &Path, dst: &Path) -> Result<()> {
2709 for entry in fs::read_dir(src)? {
2710 let entry = entry?;
2711 let src_path = entry.path();
2712 let dst_path = dst.join(entry.file_name());
2713 if src_path.is_dir() {
2714 fs::create_dir_all(&dst_path)?;
2715 copy_template(&src_path, &dst_path)?;
2716 } else {
2717 fs::copy(&src_path, &dst_path)?;
2718 }
2719 }
2720 Ok(())
2721}
2722
2723fn parse_ceiling_directories() -> (Vec<PathBuf>, bool) {
2732 let raw = match env::var("GIT_CEILING_DIRECTORIES") {
2733 Ok(val) => val,
2734 Err(_) => return (Vec::new(), false),
2735 };
2736 if raw.is_empty() {
2737 return (Vec::new(), false);
2738 }
2739 let (no_resolve, effective) = if raw.starts_with(':') {
2741 (true, &raw[1..])
2742 } else {
2743 (false, raw.as_str())
2744 };
2745 let paths = effective
2746 .split(':')
2747 .filter(|s| !s.is_empty())
2748 .filter_map(|s| {
2749 let p = PathBuf::from(s);
2750 if !p.is_absolute() {
2751 return None;
2752 }
2753 if no_resolve {
2754 let s = s.trim_end_matches('/');
2756 Some(PathBuf::from(s))
2757 } else {
2758 Some(p.canonicalize().unwrap_or_else(|_| {
2761 let s = s.trim_end_matches('/');
2762 PathBuf::from(s)
2763 }))
2764 }
2765 })
2766 .collect();
2767 (paths, no_resolve)
2768}
2769
2770pub fn validate_repo_config(config_text: &str) -> std::result::Result<(), String> {
2773 let mut version: u32 = 0;
2774 let mut in_core = false;
2775 for line in config_text.lines() {
2776 let trimmed = line.trim();
2777 if trimmed.starts_with('[') {
2778 in_core = trimmed.to_lowercase().starts_with("[core");
2779 continue;
2780 }
2781 if in_core {
2782 if let Some(rest) = trimmed.strip_prefix("repositoryformatversion") {
2783 let val = rest.trim_start_matches([' ', '=']).trim();
2784 if let Ok(v) = val.parse::<u32>() {
2785 version = v;
2786 }
2787 }
2788 }
2789 }
2790 if version >= 2 {
2791 return Err(format!("unknown repository format version: {version}"));
2792 }
2793 Ok(())
2794}