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 #[cfg(unix)]
390 ensure_valid_ownership(
391 gitfile.as_deref(),
392 repo.work_tree.as_deref(),
393 &repo.git_dir,
394 )?;
395 }
396 export_git_prefix_env(&repo);
397 return Ok(repo);
398 }
399
400 let mut offset: isize = dir_buf.len() as isize;
401 if offset <= min_offset as isize {
402 break;
403 }
404 loop {
405 offset -= 1;
406 if offset <= ceil_offset {
407 break;
408 }
409 if dir_buf
410 .as_bytes()
411 .get(offset as usize)
412 .is_some_and(|b| *b == b'/')
413 {
414 break;
415 }
416 }
417 if offset <= ceil_offset {
418 break;
419 }
420 let off_u = offset as usize;
421 let new_len = if off_u > min_offset {
422 off_u
423 } else {
424 min_offset
425 };
426 dir_buf.truncate(new_len);
427 }
428
429 Err(Error::NotARepository(start.display().to_string()))
430 }
431
432 #[must_use]
438 pub fn effective_pathspec_cwd(&self) -> PathBuf {
439 let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
440 let Some(wt) = self.work_tree.as_ref() else {
441 return cwd;
442 };
443 let inside_lexical = cwd.strip_prefix(wt).is_ok();
444 let inside_canon = cwd
445 .canonicalize()
446 .ok()
447 .zip(wt.canonicalize().ok())
448 .is_some_and(|(c, w)| c.starts_with(&w));
449 if inside_lexical || inside_canon {
450 cwd
451 } else {
452 wt.clone()
453 }
454 }
455
456 #[must_use]
458 pub fn index_path(&self) -> PathBuf {
459 self.git_dir.join("index")
460 }
461
462 pub fn index_path_for_env(&self) -> Result<PathBuf> {
466 if let Ok(raw) = env::var("GIT_INDEX_FILE") {
467 if !raw.is_empty() {
468 let p = PathBuf::from(raw);
469 return Ok(if p.is_absolute() {
470 p
471 } else {
472 env::current_dir().map_err(Error::Io)?.join(p)
473 });
474 }
475 }
476 Ok(self.index_path())
477 }
478
479 pub fn load_index(&self) -> Result<Index> {
483 let path = self.index_path_for_env()?;
484 self.load_index_at(&path)
485 }
486
487 pub fn load_index_at(&self, path: &std::path::Path) -> Result<Index> {
490 let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
491 if let Some(res) = cfg.get_bool("index.sparse") {
492 res.map_err(Error::ConfigError)?;
493 }
494 let mut idx = Index::load_expand_sparse_optional(path, &self.odb)?;
495 crate::split_index::resolve_split_index_if_needed(&mut idx, &self.git_dir, path)?;
496 if let Some(ref wt) = self.work_tree {
497 crate::sparse_checkout::clear_skip_worktree_from_present_files(
498 &self.git_dir,
499 wt,
500 &mut idx,
501 );
502 }
503 Ok(idx)
504 }
505
506 pub fn write_index(&self, index: &mut Index) -> Result<()> {
509 self.write_index_at(&self.index_path(), index)
510 }
511
512 pub fn write_index_with_post_index_change(
523 &self,
524 index: &mut Index,
525 updated_workdir: bool,
526 updated_skipworktree: bool,
527 ) -> Result<()> {
528 self.write_index_at_with_post_index_change(
529 &self.index_path(),
530 index,
531 updated_workdir,
532 updated_skipworktree,
533 )
534 }
535
536 pub fn write_index_at(&self, path: &std::path::Path, index: &mut Index) -> Result<()> {
538 self.write_index_at_split(path, index, WriteSplitIndexRequest::default())
539 }
540
541 #[must_use]
553 pub fn split_index_would_force_write(&self, index: &Index) -> bool {
554 if index.split_index_base_oid().is_some() {
555 return false;
556 }
557 let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
558 matches!(
559 crate::split_index::split_index_config(&cfg),
560 crate::split_index::SplitIndexConfig::Enabled
561 ) || crate::split_index::git_test_split_index_env()
562 }
563
564 pub fn write_index_at_with_post_index_change(
576 &self,
577 path: &std::path::Path,
578 index: &mut Index,
579 updated_workdir: bool,
580 updated_skipworktree: bool,
581 ) -> Result<()> {
582 self.write_index_at_split_with_post_index_change(
583 path,
584 index,
585 WriteSplitIndexRequest::default(),
586 updated_workdir,
587 updated_skipworktree,
588 )
589 }
590
591 pub fn write_index_at_split(
593 &self,
594 path: &std::path::Path,
595 index: &mut Index,
596 split: WriteSplitIndexRequest,
597 ) -> Result<()> {
598 self.write_index_at_split_with_post_index_change(path, index, split, false, false)
599 }
600
601 pub fn write_index_at_split_with_post_index_change(
614 &self,
615 path: &std::path::Path,
616 index: &mut Index,
617 split: WriteSplitIndexRequest,
618 updated_workdir: bool,
619 updated_skipworktree: bool,
620 ) -> Result<()> {
621 self.finalize_sparse_index_if_needed(index)?;
622 let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
623 let skip_hash = crate::index::index_skip_hash_for_write(Some(&cfg));
624 write_index_file_split(path, &self.git_dir, index, &cfg, split, skip_hash)?;
625 let updated_workdir_arg = if updated_workdir { "1" } else { "0" };
627 let updated_skipworktree_arg = if updated_skipworktree { "1" } else { "0" };
628 let _ = run_hook(
629 self,
630 "post-index-change",
631 &[updated_workdir_arg, updated_skipworktree_arg],
632 None,
633 );
634 Ok(())
635 }
636
637 fn finalize_sparse_index_if_needed(&self, index: &mut Index) -> Result<()> {
638 let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
639 let sparse_enabled = cfg
640 .get("core.sparseCheckout")
641 .map(|v| v == "true")
642 .unwrap_or(false);
643 if !sparse_enabled {
644 index.sparse_directories = false;
645 return Ok(());
646 }
647 let cone_cfg = cfg
648 .get("core.sparseCheckoutCone")
649 .and_then(|v| v.parse::<bool>().ok())
650 .unwrap_or(true);
651 let sparse_ix = cfg
652 .get("index.sparse")
653 .map(|v| v == "true")
654 .unwrap_or(false);
655 let patterns = read_sparse_checkout_patterns(&self.git_dir);
656 let cone = effective_cone_mode_for_sparse_file(cone_cfg, &patterns);
657 let head = resolve_head(&self.git_dir)?;
658 let tree_oid = if let Some(oid) = head.oid() {
659 let obj = self.odb.read(oid)?;
660 let commit = parse_commit(&obj.data)?;
661 Some(commit.tree)
662 } else {
663 None
664 };
665 if let Some(t) = tree_oid {
666 index.try_collapse_sparse_directories(&self.odb, &t, &patterns, cone, sparse_ix)?;
667 } else {
668 index.sparse_directories = false;
669 }
670 Ok(())
671 }
672
673 #[must_use]
675 pub fn refs_dir(&self) -> PathBuf {
676 self.git_dir.join("refs")
677 }
678
679 #[must_use]
681 pub fn head_path(&self) -> PathBuf {
682 self.git_dir.join("HEAD")
683 }
684
685 #[must_use]
690 pub fn bloom_pathspec_cwd(&self) -> Option<String> {
691 let wt = self.work_tree.as_ref()?;
692 let cwd = env::current_dir().ok()?;
693 let wt = wt.canonicalize().ok()?;
694 let cwd = cwd.canonicalize().ok()?;
695 let rel = cwd.strip_prefix(&wt).ok()?;
696 let s = rel.to_string_lossy().replace('\\', "/");
697 let s = s.trim_start_matches('/').to_string();
698 Some(s)
699 }
700
701 #[must_use]
703 pub fn is_bare(&self) -> bool {
704 if let Ok(cfg) = ConfigSet::load(Some(&self.git_dir), true) {
705 if let Some(Ok(bare)) = cfg.get_bool("core.bare") {
706 return bare;
707 }
708 }
709 self.work_tree.is_none()
710 }
711
712 pub fn read_replaced(&self, oid: &crate::objects::ObjectId) -> Result<crate::objects::Object> {
719 if std::env::var_os("GIT_NO_REPLACE_OBJECTS").is_some() {
720 return self.odb.read(oid);
721 }
722 let settings = self.cached_settings();
723 if !settings.use_replace_refs {
724 return self.odb.read(oid);
725 }
726 let replace_ref =
727 self.git_dir
728 .join(format!("{}{}", settings.replace_ref_base, oid.to_hex()));
729 if replace_ref.is_file() {
730 if let Ok(content) = std::fs::read_to_string(&replace_ref) {
731 let hex = content.trim();
732 if let Ok(replacement_oid) = hex.parse::<crate::objects::ObjectId>() {
733 if let Ok(obj) = self.odb.read(&replacement_oid) {
734 return Ok(obj);
735 }
736 }
737 }
738 }
739 self.odb.read(oid)
740 }
741}
742
743pub fn trace_repo_setup_if_requested(repo: &Repository) -> std::io::Result<()> {
748 let Ok(path) = env::var("GIT_TRACE_SETUP") else {
749 return Ok(());
750 };
751 if path.is_empty() || path == "0" {
752 return Ok(());
753 }
754 let trace_path = Path::new(&path);
755 if !trace_path.is_absolute() {
756 return Ok(());
757 }
758
759 let actual_cwd = env::current_dir()?;
760 let actual_cwd = actual_cwd
761 .canonicalize()
762 .unwrap_or_else(|_| actual_cwd.clone());
763
764 let (trace_cwd, prefix) = if let Some(ref wt) = repo.work_tree {
767 let wt_canon = wt.canonicalize().unwrap_or_else(|_| wt.clone());
768 if actual_cwd.starts_with(&wt_canon) {
769 let rel = actual_cwd
770 .strip_prefix(&wt_canon)
771 .map(|p| p.to_path_buf())
772 .unwrap_or_default();
773 let prefix = if rel.as_os_str().is_empty() {
774 "(null)".to_owned()
775 } else {
776 let mut s = rel.to_string_lossy().replace('\\', "/");
777 if !s.ends_with('/') {
778 s.push('/');
779 }
780 s
781 };
782 (wt_canon, prefix)
783 } else {
784 (actual_cwd.clone(), "(null)".to_owned())
785 }
786 } else {
787 (actual_cwd.clone(), "(null)".to_owned())
788 };
789
790 let git_dir_display =
791 display_git_dir_for_setup_trace(repo, &trace_cwd, &actual_cwd, prefix.as_str());
792 let common_display = display_common_dir_for_setup_trace(
793 repo,
794 &trace_cwd,
795 &actual_cwd,
796 prefix.as_str(),
797 &git_dir_display,
798 );
799 let worktree_display = repo
800 .work_tree
801 .as_ref()
802 .map(|p| {
803 p.canonicalize()
804 .unwrap_or_else(|_| lexical_normalize_path(p))
805 .display()
806 .to_string()
807 })
808 .unwrap_or_else(|| "(null)".to_owned());
809
810 let mut f = OpenOptions::new()
811 .create(true)
812 .append(true)
813 .open(trace_path)?;
814 writeln!(f, "setup: git_dir: {git_dir_display}")?;
815 writeln!(f, "setup: git_common_dir: {common_display}")?;
816 writeln!(f, "setup: worktree: {worktree_display}")?;
817 writeln!(f, "setup: cwd: {}", trace_cwd.display())?;
818 writeln!(f, "setup: prefix: {prefix}")?;
819 Ok(())
820}
821
822fn lexical_normalize_path(path: &Path) -> PathBuf {
824 let mut out = PathBuf::new();
825 let mut absolute = false;
826 for c in path.components() {
827 match c {
828 Component::Prefix(p) => {
829 out.push(p.as_os_str());
830 }
831 Component::RootDir => {
832 absolute = true;
833 out.push(c.as_os_str());
834 }
835 Component::CurDir => {}
836 Component::ParentDir => {
837 if absolute {
838 let _ = out.pop();
839 } else if !out.pop() {
840 out.push("..");
841 }
842 }
843 Component::Normal(s) => out.push(s),
844 }
845 }
846 if out.as_os_str().is_empty() {
847 PathBuf::from(".")
848 } else {
849 out
850 }
851}
852
853fn path_relative_to(target: &Path, base: &Path) -> Option<PathBuf> {
855 let t = target.canonicalize().ok()?;
856 let b = base.canonicalize().ok()?;
857 let tc: Vec<_> = t.components().collect();
858 let bc: Vec<_> = b.components().collect();
859 let mut i = 0usize;
860 while i < tc.len() && i < bc.len() && tc[i] == bc[i] {
861 i += 1;
862 }
863 let up = bc.len().saturating_sub(i);
864 let mut out = PathBuf::new();
865 for _ in 0..up {
866 out.push("..");
867 }
868 for comp in &tc[i..] {
869 out.push(comp.as_os_str());
870 }
871 Some(out)
872}
873
874fn rel_path_for_setup_trace(target: &Path, trace_cwd: &Path) -> String {
875 let t = target
876 .canonicalize()
877 .unwrap_or_else(|_| target.to_path_buf());
878 let tc = trace_cwd
879 .canonicalize()
880 .unwrap_or_else(|_| trace_cwd.to_path_buf());
881 if let Some(rel) = path_relative_to(&t, &tc) {
882 let s = rel.to_string_lossy().replace('\\', "/");
883 return if s.is_empty() || s == "." {
884 ".".to_owned()
885 } else {
886 s
887 };
888 }
889 t.display().to_string()
890}
891
892fn trace_cwd_strictly_inside_git_parent(trace_cwd: &Path, git_dir: &Path) -> bool {
893 let tc = trace_cwd
894 .canonicalize()
895 .unwrap_or_else(|_| trace_cwd.to_path_buf());
896 let gd = git_dir
897 .canonicalize()
898 .unwrap_or_else(|_| git_dir.to_path_buf());
899 let Some(parent) = gd.parent() else {
900 return false;
901 };
902 let parent = parent.to_path_buf();
903 if tc == parent {
904 return false;
905 }
906 tc.starts_with(&parent) && tc != parent
907}
908
909fn display_git_dir_for_setup_trace(
910 repo: &Repository,
911 trace_cwd: &Path,
912 actual_cwd: &Path,
913 setup_prefix: &str,
914) -> String {
915 let gd = repo
916 .git_dir
917 .canonicalize()
918 .unwrap_or_else(|_| repo.git_dir.clone());
919 let tc = trace_cwd
920 .canonicalize()
921 .unwrap_or_else(|_| trace_cwd.to_path_buf());
922 let ac = actual_cwd
923 .canonicalize()
924 .unwrap_or_else(|_| actual_cwd.to_path_buf());
925
926 if repo.work_tree.is_none() && !repo.explicit_git_dir {
929 if ac == gd {
930 return ".".to_owned();
931 }
932 if ac.starts_with(&gd) && ac != gd {
933 return gd.display().to_string();
934 }
935 }
936
937 if !repo.explicit_git_dir {
939 if let Some(wt) = &repo.work_tree {
940 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
941 if ac.starts_with(&gd) && ac != wt {
942 return gd.display().to_string();
943 }
944 }
945 }
946
947 if repo.explicit_git_dir {
951 if repo.work_tree.is_none() {
952 if let Ok(raw) = env::var("GIT_DIR") {
953 let p = Path::new(raw.trim());
954 if p.is_absolute() {
955 return gd.display().to_string();
956 }
957 let joined = ac.join(p);
958 if joined.is_file() {
959 return gd.display().to_string();
960 }
961 if let Some(rel) = path_relative_to(&gd, &tc) {
962 let s = rel.to_string_lossy().replace('\\', "/");
963 return if s.is_empty() || s == "." {
964 ".".to_owned()
965 } else {
966 s
967 };
968 }
969 }
970 return gd.display().to_string();
971 }
972 if let Some(wt) = &repo.work_tree {
973 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
974 let strictly_inside_wt = ac.starts_with(&wt) && ac != wt;
975 if strictly_inside_wt {
976 return gd.display().to_string();
977 }
978 if let Ok(raw) = env::var("GIT_DIR") {
979 let p = Path::new(raw.trim());
980 if p.is_relative() {
981 let joined = ac.join(p);
982 if joined.is_file() {
983 return gd.display().to_string();
985 }
986 if let Some(rel) = path_relative_to(&gd, &tc) {
987 let s = rel.to_string_lossy().replace('\\', "/");
988 return if s.is_empty() || s == "." {
989 ".".to_owned()
990 } else {
991 s
992 };
993 }
994 }
995 return gd.display().to_string();
996 }
997 }
998 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
999 return rel_path_for_setup_trace(&gd, trace_cwd);
1000 }
1001 return gd.display().to_string();
1002 }
1003
1004 let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
1005 (Some(root), Some(wt)) if !repo.work_tree_from_env => {
1006 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
1007 let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
1008 r != w
1009 }
1010 _ => false,
1011 };
1012
1013 if repo.work_tree_from_env {
1014 if !repo.discovery_via_gitfile {
1015 if setup_prefix == "(null)" {
1016 if let (Some(root), Some(wt)) = (&repo.discovery_root, &repo.work_tree) {
1017 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
1018 let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
1019 if r == w {
1020 let dot_git = r.join(".git");
1021 let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
1022 if gd == dot_git {
1023 return ".git".to_owned();
1024 }
1025 }
1026 }
1027 }
1028 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
1029 return rel_path_for_setup_trace(&gd, trace_cwd);
1030 }
1031 }
1032 return gd.display().to_string();
1033 }
1034
1035 if work_relocated {
1036 if let Some(wt) = &repo.work_tree {
1037 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
1038 if ac == wt {
1039 return gd.display().to_string();
1040 }
1041 let inside_wt = ac.starts_with(&wt) && ac != wt;
1042 if inside_wt {
1043 if let Some(rel) = path_relative_to(&gd, &ac) {
1044 let s = rel.to_string_lossy().replace('\\', "/");
1045 return if s.is_empty() || s == "." {
1046 ".".to_owned()
1047 } else {
1048 s
1049 };
1050 }
1051 }
1052 }
1053 }
1054 if repo.work_tree.is_some() {
1055 if let Some(root) = &repo.discovery_root {
1056 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
1057 let dot_git = r.join(".git");
1058 let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
1059 if gd == dot_git {
1060 return ".git".to_owned();
1061 }
1062 } else if let Some(wt) = &repo.work_tree {
1063 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
1064 let dot_git = wt.join(".git");
1065 let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
1066 if gd == dot_git {
1067 return ".git".to_owned();
1068 }
1069 }
1070 }
1071
1072 if repo.discovery_via_gitfile && !repo.explicit_git_dir {
1073 return gd.display().to_string();
1074 }
1075
1076 if repo.work_tree.is_none() && !repo.explicit_git_dir {
1080 if let Some(gp) = gd.parent() {
1081 let gp = gp.canonicalize().unwrap_or_else(|_| gp.to_path_buf());
1082 let gdc = gd.canonicalize().unwrap_or_else(|_| gd.clone());
1083 if tc.starts_with(&gp) && tc != gp && !tc.starts_with(&gdc) {
1084 return gdc.display().to_string();
1085 }
1086 if tc == gp {
1087 return rel_path_for_setup_trace(&gd, trace_cwd);
1088 }
1089 }
1090 }
1091
1092 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
1093 rel_path_for_setup_trace(&gd, trace_cwd)
1094 } else {
1095 gd.display().to_string()
1096 }
1097}
1098
1099fn display_common_dir_for_setup_trace(
1100 repo: &Repository,
1101 trace_cwd: &Path,
1102 actual_cwd: &Path,
1103 _setup_prefix: &str,
1104 git_dir_display: &str,
1105) -> String {
1106 let gd = repo
1107 .git_dir
1108 .canonicalize()
1109 .unwrap_or_else(|_| repo.git_dir.clone());
1110 let Some(common) = resolve_common_dir(&gd) else {
1111 return git_dir_display.to_owned();
1112 };
1113 let common = common.canonicalize().unwrap_or(common);
1114 if common == gd {
1115 return git_dir_display.to_owned();
1116 }
1117
1118 let ac = actual_cwd
1119 .canonicalize()
1120 .unwrap_or_else(|_| actual_cwd.to_path_buf());
1121 if repo.work_tree.is_none() && !repo.explicit_git_dir {
1122 if ac == common {
1123 return ".".to_owned();
1124 }
1125 if ac.starts_with(&common) && ac != common {
1126 return common.display().to_string();
1127 }
1128 }
1129
1130 let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
1131 (Some(root), Some(wt)) if !repo.work_tree_from_env => {
1132 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
1133 let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
1134 r != w
1135 }
1136 _ => false,
1137 };
1138 if work_relocated {
1139 if let Some(wt) = &repo.work_tree {
1140 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
1141 if ac == wt {
1142 return common.display().to_string();
1143 }
1144 let inside_wt = ac.starts_with(&wt) && ac != wt;
1145 if inside_wt {
1146 if let Some(rel) = path_relative_to(&common, &ac) {
1147 let s = rel.to_string_lossy().replace('\\', "/");
1148 return if s.is_empty() || s == "." {
1149 ".".to_owned()
1150 } else {
1151 s
1152 };
1153 }
1154 }
1155 }
1156 }
1157
1158 if repo.discovery_via_gitfile && !repo.explicit_git_dir {
1159 return common.display().to_string();
1160 }
1161
1162 if repo.work_tree.is_none() && !repo.explicit_git_dir {
1163 let tc = trace_cwd
1164 .canonicalize()
1165 .unwrap_or_else(|_| trace_cwd.to_path_buf());
1166 if let Some(cp) = common.parent() {
1167 let cp = cp.canonicalize().unwrap_or_else(|_| cp.to_path_buf());
1168 let comc = common.canonicalize().unwrap_or_else(|_| common.clone());
1169 if tc.starts_with(&cp) && tc != cp && !tc.starts_with(&comc) {
1170 return comc.display().to_string();
1171 }
1172 if tc == cp {
1173 return rel_path_for_setup_trace(&common, trace_cwd);
1174 }
1175 }
1176 }
1177
1178 if trace_cwd_strictly_inside_git_parent(trace_cwd, &common) {
1179 rel_path_for_setup_trace(&common, trace_cwd)
1180 } else {
1181 common.display().to_string()
1182 }
1183}
1184
1185fn resolve_common_dir(git_dir: &Path) -> Option<PathBuf> {
1187 let common_raw = fs::read_to_string(git_dir.join("commondir")).ok()?;
1188 let common_rel = common_raw.trim();
1189 if common_rel.is_empty() {
1190 return None;
1191 }
1192 let common_dir = if Path::new(common_rel).is_absolute() {
1193 PathBuf::from(common_rel)
1194 } else {
1195 git_dir.join(common_rel)
1196 };
1197 Some(common_dir.canonicalize().unwrap_or(common_dir))
1198}
1199
1200#[must_use]
1202pub fn common_git_dir_for_config(git_dir: &Path) -> PathBuf {
1203 resolve_common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf())
1204}
1205
1206pub fn worktree_config_enabled(common_dir: &Path) -> bool {
1208 let path = common_dir.join("config");
1209 let Ok(content) = fs::read_to_string(&path) else {
1210 return false;
1211 };
1212 let mut in_extensions = false;
1213 for raw_line in content.lines() {
1214 let mut line = raw_line.trim();
1215 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
1216 continue;
1217 }
1218 if line.starts_with('[') {
1219 let Some(end_idx) = line.find(']') else {
1220 continue;
1221 };
1222 let section = line[1..end_idx].trim();
1223 let section_name = section
1224 .split_whitespace()
1225 .next()
1226 .unwrap_or_default()
1227 .to_ascii_lowercase();
1228 in_extensions = section_name == "extensions";
1229 let remainder = line[end_idx + 1..].trim();
1230 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
1231 continue;
1232 }
1233 line = remainder;
1234 }
1235 if in_extensions {
1236 let Some((key, value)) = line.split_once('=') else {
1237 continue;
1238 };
1239 if key.trim().eq_ignore_ascii_case("worktreeconfig") {
1240 let v = value.trim();
1241 return v.eq_ignore_ascii_case("true")
1242 || v.eq_ignore_ascii_case("yes")
1243 || v.eq_ignore_ascii_case("on")
1244 || v == "1";
1245 }
1246 }
1247 }
1248 false
1249}
1250
1251fn open_or_create_config_file(path: &Path, scope: ConfigScope) -> Result<ConfigFile> {
1252 match ConfigFile::from_path(path, scope)? {
1253 Some(f) => Ok(f),
1254 None => {
1255 if let Some(parent) = path.parent() {
1256 fs::create_dir_all(parent).map_err(Error::Io)?;
1257 }
1258 ConfigFile::parse(path, "", scope)
1259 }
1260 }
1261}
1262
1263fn config_file_bool_true(cfg: &ConfigFile, key: &str) -> bool {
1264 cfg.get(key).is_some_and(|v| {
1265 matches!(
1266 v.trim().to_ascii_lowercase().as_str(),
1267 "true" | "yes" | "on" | "1"
1268 )
1269 })
1270}
1271
1272pub fn init_worktree_config(git_dir: &Path) -> Result<()> {
1282 let common_dir = common_git_dir_for_config(git_dir);
1283 let common_config_path = common_dir.join("config");
1284 let worktree_config_path = git_dir.join("config.worktree");
1285
1286 if worktree_config_enabled(&common_dir) {
1287 if !worktree_config_path.exists() {
1288 if let Some(parent) = worktree_config_path.parent() {
1289 fs::create_dir_all(parent).map_err(Error::Io)?;
1290 }
1291 fs::write(&worktree_config_path, "").map_err(Error::Io)?;
1292 }
1293 return Ok(());
1294 }
1295
1296 let mut common_cfg = open_or_create_config_file(&common_config_path, ConfigScope::Local)?;
1297 common_cfg.set("extensions.worktreeConfig", "true")?;
1298
1299 let mut wt_cfg = open_or_create_config_file(&worktree_config_path, ConfigScope::Worktree)?;
1300
1301 if config_file_bool_true(&common_cfg, "core.bare") {
1302 wt_cfg.set("core.bare", "true")?;
1303 common_cfg.unset("core.bare")?;
1304 }
1305 if let Some(worktree) = common_cfg.get("core.worktree") {
1306 wt_cfg.set("core.worktree", &worktree)?;
1307 common_cfg.unset("core.worktree")?;
1308 }
1309
1310 common_cfg.write()?;
1311 wt_cfg.write()?;
1312 Ok(())
1313}
1314
1315pub fn early_config_ignore_repo_reason(common_dir: &Path) -> Option<String> {
1319 const GIT_REPO_VERSION_READ: u32 = 1;
1320 let path = common_dir.join("config");
1321 let content = fs::read_to_string(&path).ok()?;
1322 let mut version = 0u32;
1323 let mut in_core = false;
1324 for raw_line in content.lines() {
1325 let mut line = raw_line.trim();
1326 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
1327 continue;
1328 }
1329 if line.starts_with('[') {
1330 let Some(end_idx) = line.find(']') else {
1331 continue;
1332 };
1333 let section = line[1..end_idx].trim();
1334 let section_name = section
1335 .split_whitespace()
1336 .next()
1337 .unwrap_or_default()
1338 .to_ascii_lowercase();
1339 in_core = section_name == "core";
1340 let remainder = line[end_idx + 1..].trim();
1341 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
1342 continue;
1343 }
1344 line = remainder;
1345 }
1346 if in_core {
1347 if let Some((key, value)) = line.split_once('=') {
1348 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
1349 if let Ok(v) = value.trim().parse::<u32>() {
1350 version = v;
1351 }
1352 }
1353 }
1354 }
1355 }
1356 if version > GIT_REPO_VERSION_READ {
1357 Some(format!(
1358 "Expected git repo version <= {GIT_REPO_VERSION_READ}, found {version}"
1359 ))
1360 } else {
1361 None
1362 }
1363}
1364
1365fn path_for_ceiling_compare(path: &Path) -> String {
1366 let path = path.to_string_lossy();
1367 #[cfg(windows)]
1368 {
1369 path.replace('\\', "/")
1370 }
1371 #[cfg(not(windows))]
1372 {
1373 path.into_owned()
1374 }
1375}
1376
1377fn offset_1st_component(path: &str) -> usize {
1378 if path.starts_with('/') {
1379 1
1380 } else {
1381 0
1382 }
1383}
1384
1385fn longest_ancestor_length(path: &str, ceilings: &[String]) -> Option<usize> {
1387 if path == "/" {
1388 return None;
1389 }
1390 let mut max_len: Option<usize> = None;
1391 for ceil in ceilings {
1392 let mut len = ceil.len();
1393 while len > 0 && ceil.as_bytes().get(len - 1) == Some(&b'/') {
1394 len -= 1;
1395 }
1396 if len == 0 {
1397 continue;
1398 }
1399 if path.len() <= len + 1 {
1400 continue;
1401 }
1402 if !path.starts_with(&ceil[..len]) {
1403 continue;
1404 }
1405 if path.as_bytes().get(len) != Some(&b'/') {
1406 continue;
1407 }
1408 if path.as_bytes().get(len + 1).is_none() {
1409 continue;
1410 }
1411 max_len = Some(max_len.map_or(len, |m| m.max(len)));
1412 }
1413 max_len
1414}
1415
1416fn repository_config_path(git_dir: &Path) -> Option<PathBuf> {
1418 let local = git_dir.join("config");
1419 if local.exists() {
1420 return Some(local);
1421 }
1422 let common = resolve_common_dir(git_dir)?;
1423 let shared = common.join("config");
1424 if shared.exists() {
1425 Some(shared)
1426 } else {
1427 None
1428 }
1429}
1430
1431pub fn validate_repo_format(git_dir: &Path) -> Result<()> {
1437 validate_repository_format(git_dir)
1438}
1439
1440fn validate_repository_format(git_dir: &Path) -> Result<()> {
1441 let Some(config_path) = repository_config_path(git_dir) else {
1442 return Ok(());
1443 };
1444
1445 let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
1446 let parsed = parse_repository_format(&content, &config_path)?;
1447
1448 if parsed.repo_version > 1 {
1449 return Err(Error::UnsupportedRepositoryFormatVersion(
1450 parsed.repo_version,
1451 ));
1452 }
1453
1454 if let Some(raw) = parsed.ref_storage.as_deref() {
1455 let lower = raw.to_ascii_lowercase();
1456 let name = lower
1457 .split_once(':')
1458 .map(|(prefix, _)| prefix)
1459 .unwrap_or(lower.as_str());
1460 if !matches!(name, "files" | "reftable") {
1461 return Err(Error::Message(format!(
1462 "error: invalid value for 'extensions.refstorage': '{raw}'"
1463 )));
1464 }
1465 }
1466
1467 if let Some(msg) = parsed.format_error_message() {
1468 return Err(Error::Message(msg));
1469 }
1470
1471 Ok(())
1472}
1473
1474struct RepositoryFormat {
1477 repo_version: u32,
1479 extensions: BTreeSet<String>,
1481 ref_storage: Option<String>,
1483}
1484
1485impl RepositoryFormat {
1486 fn format_error_message(&self) -> Option<String> {
1493 let mut v1_only_found: Vec<&str> = Vec::new();
1500 let mut unknown_found: Vec<&str> = Vec::new();
1501 for extension in &self.extensions {
1502 match extension.as_str() {
1503 "noop" | "preciousobjects" | "partialclone" | "worktreeconfig" => {}
1505 "noop-v1"
1507 | "objectformat"
1508 | "compatobjectformat"
1509 | "refstorage"
1510 | "relativeworktrees"
1511 | "submodulepathconfig" => {
1512 if self.repo_version == 0 {
1513 v1_only_found.push(extension);
1514 }
1515 }
1516 _ => {
1518 if self.repo_version >= 1 {
1519 unknown_found.push(extension);
1520 }
1521 }
1522 }
1523 }
1524
1525 if !unknown_found.is_empty() {
1526 let mut msg = if unknown_found.len() == 1 {
1527 "unknown repository extension found:".to_owned()
1528 } else {
1529 "unknown repository extensions found:".to_owned()
1530 };
1531 for ext in &unknown_found {
1532 msg.push_str(&format!("\n\t{ext}"));
1533 }
1534 return Some(msg);
1535 }
1536
1537 if !v1_only_found.is_empty() {
1538 let mut msg = if v1_only_found.len() == 1 {
1539 "repo version is 0, but v1-only extension found:".to_owned()
1540 } else {
1541 "repo version is 0, but v1-only extensions found:".to_owned()
1542 };
1543 for ext in &v1_only_found {
1544 msg.push_str(&format!("\n\t{ext}"));
1545 }
1546 return Some(msg);
1547 }
1548
1549 None
1550 }
1551}
1552
1553fn parse_repository_format(content: &str, config_path: &Path) -> Result<RepositoryFormat> {
1560 let mut in_core = false;
1561 let mut in_extensions = false;
1562 let mut repo_version = 0u32;
1563 let mut extensions = BTreeSet::new();
1564 let mut ref_storage: Option<String> = None;
1565
1566 for raw_line in content.lines() {
1567 let mut line = raw_line.trim();
1568 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
1569 continue;
1570 }
1571
1572 if line.starts_with('[') {
1573 let Some(end_idx) = line.find(']') else {
1574 return Err(Error::ConfigError(format!(
1575 "invalid config in {}",
1576 config_path.display()
1577 )));
1578 };
1579
1580 let section = line[1..end_idx].trim();
1581 let section_name = section
1582 .split_whitespace()
1583 .next()
1584 .unwrap_or_default()
1585 .to_ascii_lowercase();
1586 in_core = section_name == "core";
1587 in_extensions = section_name == "extensions";
1588
1589 let remainder = line[end_idx + 1..].trim();
1590 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
1591 continue;
1592 }
1593 line = remainder;
1594 }
1595
1596 if in_core {
1597 if let Some((key, value)) = line.split_once('=') {
1598 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
1599 if let Ok(v) = value.trim().parse::<u32>() {
1601 repo_version = v;
1602 }
1603 }
1604 }
1605 }
1606
1607 if in_extensions {
1608 let (key, value) = if let Some((key, value)) = line.split_once('=') {
1609 (key.trim(), Some(value.trim()))
1610 } else {
1611 (line, None)
1612 };
1613 if key.eq_ignore_ascii_case("refstorage") {
1614 ref_storage = value.map(str::to_owned);
1615 }
1616 if !key.is_empty() {
1617 extensions.insert(key.to_ascii_lowercase());
1618 }
1619 }
1620 }
1621
1622 Ok(RepositoryFormat {
1623 repo_version,
1624 extensions,
1625 ref_storage,
1626 })
1627}
1628
1629pub fn repository_format_warning(git_dir: &Path) -> Result<Option<String>> {
1646 const GIT_REPO_VERSION_READ: u32 = 1;
1647 let Some(config_path) = repository_config_path(git_dir) else {
1648 return Ok(None);
1649 };
1650 let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
1651 let parsed = parse_repository_format(&content, &config_path)?;
1652
1653 if parsed.repo_version > GIT_REPO_VERSION_READ {
1654 return Ok(Some(format!(
1655 "Expected git repo version <= {GIT_REPO_VERSION_READ}, found {}",
1656 parsed.repo_version
1657 )));
1658 }
1659
1660 Ok(parsed.format_error_message())
1661}
1662
1663struct DiscoveredAt {
1669 repo: Repository,
1670 gitfile: Option<PathBuf>,
1672}
1673
1674fn try_open_at(dir: &Path) -> Result<Option<DiscoveredAt>> {
1675 let dot_git = dir.join(".git");
1676
1677 #[cfg(unix)]
1680 {
1681 use std::os::unix::fs::FileTypeExt;
1682 if let Ok(meta) = fs::symlink_metadata(&dot_git) {
1683 let ft = meta.file_type();
1684 if ft.is_fifo() || ft.is_socket() || ft.is_block_device() || ft.is_char_device() {
1685 return Err(Error::NotARepository(format!(
1686 "invalid gitfile format: {} is not a regular file",
1687 dot_git.display()
1688 )));
1689 }
1690 if ft.is_symlink() {
1691 if let Ok(target_meta) = fs::metadata(&dot_git) {
1692 let tft = target_meta.file_type();
1693 if tft.is_fifo()
1694 || tft.is_socket()
1695 || tft.is_block_device()
1696 || tft.is_char_device()
1697 {
1698 return Err(Error::NotARepository(format!(
1699 "invalid gitfile format: {} is not a regular file",
1700 dot_git.display()
1701 )));
1702 }
1703 }
1704 }
1705 }
1706 }
1707
1708 if dot_git.is_file() {
1709 let content =
1711 fs::read_to_string(&dot_git).map_err(|e| Error::NotARepository(e.to_string()))?;
1712 let git_dir = parse_gitfile(&content, dir)?;
1713 let mut repo = Repository::open_skipping_format_validation(&git_dir, Some(dir))?;
1714 if resolve_common_dir(&git_dir).is_some() {
1718 let cwd = env::current_dir().map_err(Error::Io)?;
1719 if repo.work_tree.is_some() && !is_inside_work_tree(&repo, &cwd) {
1720 let root = if dir.is_absolute() {
1721 dir.to_path_buf()
1722 } else {
1723 cwd.join(dir)
1724 };
1725 repo.work_tree = Some(root.canonicalize().unwrap_or(root));
1726 }
1727 }
1728 let root = if dir.is_absolute() {
1729 dir.to_path_buf()
1730 } else {
1731 env::current_dir().map_err(Error::Io)?.join(dir)
1732 };
1733 repo.discovery_root = Some(root.canonicalize().unwrap_or(root));
1734 repo.discovery_via_gitfile = true;
1735 warn_core_bare_worktree_conflict(&git_dir);
1736 return Ok(Some(DiscoveredAt {
1737 repo,
1738 gitfile: Some(dot_git.clone()),
1739 }));
1740 }
1741
1742 if dot_git.is_dir() {
1743 let open_path = if dot_git.is_symlink() {
1747 dot_git.read_link().unwrap_or_else(|_| dot_git.clone())
1749 } else {
1750 dot_git.clone()
1751 };
1752 match Repository::open_skipping_format_validation(&open_path, Some(dir)) {
1755 Ok(mut repo) => {
1756 if dot_git.is_symlink() {
1759 let abs_dot_git = if dot_git.is_absolute() {
1760 dot_git
1761 } else {
1762 dir.join(".git")
1763 };
1764 repo.git_dir = abs_dot_git;
1765 }
1766 let root = if dir.is_absolute() {
1767 dir.to_path_buf()
1768 } else {
1769 env::current_dir().map_err(Error::Io)?.join(dir)
1770 };
1771 repo.discovery_root = Some(root.canonicalize().unwrap_or(root));
1772 repo.discovery_via_gitfile = false;
1773 return Ok(Some(DiscoveredAt {
1774 repo,
1775 gitfile: None,
1776 }));
1777 }
1778 Err(Error::NotARepository(_)) | Err(Error::ConfigError(_)) => return Ok(None),
1779 Err(Error::Message(ref msg)) if msg.contains("bad config") => return Ok(None),
1780 Err(e) => return Err(e),
1781 }
1782 }
1783
1784 if dir.join("HEAD").is_file() && dir.join("commondir").is_file() {
1787 maybe_trace_implicit_bare_repository(dir);
1788 let repo = Repository::open(dir, None)?;
1789 warn_core_bare_worktree_conflict(dir);
1790 return Ok(Some(DiscoveredAt {
1791 repo,
1792 gitfile: None,
1793 }));
1794 }
1795
1796 if dir.join("objects").is_dir() && dir.join("HEAD").is_file() {
1798 maybe_trace_implicit_bare_repository(dir);
1799 if !is_inside_dot_git(dir) {
1803 if let Ok(cfg) = crate::config::ConfigSet::load(None, true) {
1804 if let Some(val) = cfg.get("safe.bareRepository") {
1805 if val.eq_ignore_ascii_case("explicit") {
1806 return Err(Error::ForbiddenBareRepository(dir.display().to_string()));
1807 }
1808 }
1809 }
1810 }
1811 let repo = Repository::open(dir, None)?;
1812 warn_core_bare_worktree_conflict(dir);
1813 return Ok(Some(DiscoveredAt {
1814 repo,
1815 gitfile: None,
1816 }));
1817 }
1818
1819 Ok(None)
1820}
1821
1822fn is_inside_dot_git(path: &Path) -> bool {
1823 path.components().any(|c| c.as_os_str() == ".git")
1824}
1825
1826fn maybe_trace_implicit_bare_repository(dir: &Path) {
1827 let path = match std::env::var("GIT_TRACE2_PERF") {
1828 Ok(p) if !p.is_empty() => p,
1829 _ => return,
1830 };
1831
1832 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
1833 let _ = writeln!(file, "setup: implicit-bare-repository:{}", dir.display());
1834 }
1835}
1836
1837fn safe_directory_effective_values(git_dir: &Path) -> Vec<String> {
1840 let cfg = crate::config::ConfigSet::load(Some(git_dir), true)
1841 .unwrap_or_else(|_| crate::config::ConfigSet::new());
1842 let mut values: Vec<String> = Vec::new();
1843 for e in cfg.entries() {
1844 if e.key == "safe.directory"
1845 && e.scope != crate::config::ConfigScope::Local
1846 && e.scope != crate::config::ConfigScope::Worktree
1847 {
1848 values.push(e.value.clone().unwrap_or_else(|| "true".to_owned()));
1849 }
1850 }
1851 let mut effective: Vec<String> = Vec::new();
1852 for v in values {
1853 if v.is_empty() {
1854 effective.clear();
1855 } else {
1856 effective.push(v);
1857 }
1858 }
1859 effective
1860}
1861
1862fn ensure_safe_directory_allows(git_dir: &Path, checked: &Path) -> Result<()> {
1863 let effective = safe_directory_effective_values(git_dir);
1864 let checked_s = checked.to_string_lossy().to_string();
1865 if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
1866 eprintln!("debug-safe-directory values={:?}", effective);
1867 }
1868 if effective
1869 .iter()
1870 .any(|v| safe_directory_matches(v, &checked_s))
1871 {
1872 return Ok(());
1873 }
1874 Err(Error::DubiousOwnership(checked_s))
1875}
1876
1877#[cfg(unix)]
1878fn path_lstat_uid(path: &Path) -> std::io::Result<u32> {
1879 use std::os::unix::fs::MetadataExt;
1880 let meta = fs::symlink_metadata(path)?;
1881 Ok(meta.uid())
1882}
1883
1884#[cfg(unix)]
1885fn extract_uid_from_env(name: &str) -> Option<u32> {
1886 let raw = std::env::var(name).ok()?;
1887 if raw.is_empty() {
1888 return None;
1889 }
1890 raw.parse::<u32>().ok()
1891}
1892
1893#[cfg(unix)]
1896fn ensure_valid_ownership(
1897 gitfile: Option<&Path>,
1898 worktree: Option<&Path>,
1899 gitdir: &Path,
1900) -> Result<()> {
1901 const ROOT_UID: u32 = 0;
1902
1903 fn owned_by_effective_user(path: &Path) -> std::io::Result<bool> {
1904 let st_uid = path_lstat_uid(path)?;
1905 let mut euid = unsafe { libc::geteuid() };
1906 if euid == ROOT_UID {
1907 if st_uid == ROOT_UID {
1908 return Ok(true);
1909 }
1910 if let Some(sudo_uid) = extract_uid_from_env("SUDO_UID") {
1911 euid = sudo_uid;
1912 }
1913 }
1914 Ok(st_uid == euid)
1915 }
1916
1917 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
1918 .ok()
1919 .map(|v| {
1920 let lower = v.to_ascii_lowercase();
1921 v == "1" || lower == "true" || lower == "yes" || lower == "on"
1922 })
1923 .unwrap_or(false);
1924 if !assume_different {
1925 let gitfile_ok = gitfile
1926 .map(owned_by_effective_user)
1927 .transpose()?
1928 .unwrap_or(true);
1929 let wt_ok = match worktree {
1932 None => true,
1933 Some(wt) => match owned_by_effective_user(wt) {
1934 Ok(ok) => ok,
1935 Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
1936 Err(e) => return Err(Error::Io(e)),
1937 },
1938 };
1939 let gd_ok = owned_by_effective_user(gitdir)?;
1940 if gitfile_ok && wt_ok && gd_ok {
1941 return Ok(());
1942 }
1943 }
1944
1945 let data_path = if let Some(wt) = worktree {
1946 wt.canonicalize().unwrap_or_else(|_| wt.to_path_buf())
1947 } else {
1948 gitdir
1949 .canonicalize()
1950 .unwrap_or_else(|_| gitdir.to_path_buf())
1951 };
1952 ensure_safe_directory_allows(gitdir, &data_path)
1953}
1954
1955#[cfg(not(unix))]
1956fn ensure_valid_ownership(
1957 _gitfile: Option<&Path>,
1958 _worktree: Option<&Path>,
1959 _gitdir: &Path,
1960) -> Result<()> {
1961 Ok(())
1962}
1963
1964impl Repository {
1965 pub fn enforce_safe_directory(&self) -> Result<()> {
1971 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
1972 .ok()
1973 .map(|v| {
1974 let lower = v.to_ascii_lowercase();
1975 v == "1" || lower == "true" || lower == "yes" || lower == "on"
1976 })
1977 .unwrap_or(false);
1978 if !assume_different {
1979 return Ok(());
1980 }
1981
1982 if self.explicit_git_dir {
1983 return Ok(());
1984 }
1985
1986 let checked = if let Some(wt) = &self.work_tree {
1990 let cwd = std::env::current_dir().ok();
1991 if let Some(cwd) = cwd {
1992 if cwd
1993 .canonicalize()
1994 .ok()
1995 .is_some_and(|c| c.starts_with(&self.git_dir))
1996 {
1997 self.git_dir
1998 .canonicalize()
1999 .unwrap_or_else(|_| self.git_dir.clone())
2000 } else {
2001 wt.canonicalize().unwrap_or_else(|_| wt.clone())
2002 }
2003 } else {
2004 wt.canonicalize().unwrap_or_else(|_| wt.clone())
2005 }
2006 } else {
2007 self.git_dir
2008 .canonicalize()
2009 .unwrap_or_else(|_| self.git_dir.clone())
2010 };
2011
2012 if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
2013 eprintln!(
2014 "debug-safe-directory checked={} git_dir={} work_tree={:?} cwd={:?}",
2015 checked.display(),
2016 self.git_dir.display(),
2017 self.work_tree,
2018 std::env::current_dir().ok()
2019 );
2020 }
2021 self.enforce_safe_directory_checked(&checked)
2022 }
2023
2024 pub fn enforce_safe_directory_git_dir(&self) -> Result<()> {
2029 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
2030 .ok()
2031 .map(|v| {
2032 let lower = v.to_ascii_lowercase();
2033 v == "1" || lower == "true" || lower == "yes" || lower == "on"
2034 })
2035 .unwrap_or(false);
2036 if !assume_different {
2037 return Ok(());
2038 }
2039 let checked = self
2040 .git_dir
2041 .canonicalize()
2042 .unwrap_or_else(|_| self.git_dir.clone());
2043 if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
2044 eprintln!(
2045 "debug-safe-directory(gitdir) checked={} git_dir={} work_tree={:?}",
2046 checked.display(),
2047 self.git_dir.display(),
2048 self.work_tree
2049 );
2050 }
2051 self.enforce_safe_directory_checked(&checked)
2052 }
2053
2054 pub fn enforce_safe_directory_git_dir_with_path(&self, checked: &Path) -> Result<()> {
2056 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
2057 .ok()
2058 .map(|v| {
2059 let lower = v.to_ascii_lowercase();
2060 v == "1" || lower == "true" || lower == "yes" || lower == "on"
2061 })
2062 .unwrap_or(false);
2063 if !assume_different {
2064 return Ok(());
2065 }
2066 self.enforce_safe_directory_checked(checked)
2067 }
2068
2069 fn enforce_safe_directory_checked(&self, checked: &Path) -> Result<()> {
2070 ensure_safe_directory_allows(&self.git_dir, checked)
2071 }
2072
2073 pub fn verify_safe_for_clone_source(&self) -> Result<()> {
2079 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
2080 .ok()
2081 .map(|v| {
2082 let lower = v.to_ascii_lowercase();
2083 v == "1" || lower == "true" || lower == "yes" || lower == "on"
2084 })
2085 .unwrap_or(false);
2086 if assume_different {
2087 self.enforce_safe_directory_git_dir()
2088 } else {
2089 #[cfg(unix)]
2090 {
2091 ensure_valid_ownership(None, None, &self.git_dir)
2092 }
2093 #[cfg(not(unix))]
2094 {
2095 Ok(())
2096 }
2097 }
2098 }
2099}
2100
2101fn normalize_fs_path(raw: &str) -> String {
2102 use std::path::Component;
2103 let p = std::path::Path::new(raw);
2104 let mut parts: Vec<String> = Vec::new();
2105 let mut absolute = false;
2106 for c in p.components() {
2107 match c {
2108 Component::RootDir => {
2109 absolute = true;
2110 parts.clear();
2111 }
2112 Component::CurDir => {}
2113 Component::ParentDir => {
2114 if !parts.is_empty() {
2115 parts.pop();
2116 }
2117 }
2118 Component::Normal(s) => parts.push(s.to_string_lossy().to_string()),
2119 Component::Prefix(_) => {}
2120 }
2121 }
2122 let mut out = if absolute {
2123 String::from("/")
2124 } else {
2125 String::new()
2126 };
2127 out.push_str(&parts.join("/"));
2128 out
2129}
2130
2131fn safe_directory_matches(config_value: &str, checked: &str) -> bool {
2132 if config_value == "*" {
2133 return true;
2134 }
2135 if config_value == "." {
2136 if let Ok(cwd) = std::env::current_dir() {
2138 let cwd_s = normalize_fs_path(&cwd.to_string_lossy());
2139 let checked_s = normalize_fs_path(checked);
2140 return cwd_s == checked_s;
2141 }
2142 return false;
2143 }
2144
2145 let canonicalize_or_normalize = |raw: &str| -> String {
2146 let p = std::path::Path::new(raw);
2147 if p.exists() {
2148 p.canonicalize()
2149 .map(|c| c.to_string_lossy().to_string())
2150 .map(|s| normalize_fs_path(&s))
2151 .unwrap_or_else(|_| normalize_fs_path(raw))
2152 } else {
2153 normalize_fs_path(raw)
2154 }
2155 };
2156
2157 let config_norm = canonicalize_or_normalize(config_value);
2158 let checked_norm = normalize_fs_path(checked);
2159
2160 if config_norm.ends_with("/*") {
2161 let prefix_raw = &config_norm[..config_norm.len() - 2];
2162 let prefix_norm = canonicalize_or_normalize(prefix_raw);
2163 let mut prefix = prefix_norm;
2164 if !prefix.ends_with('/') {
2165 prefix.push('/');
2166 }
2167 return checked_norm.starts_with(&prefix);
2168 }
2169
2170 config_norm == checked_norm
2171}
2172
2173fn warn_core_bare_worktree_conflict(git_dir: &Path) {
2174 if env::var("GIT_WORK_TREE")
2175 .ok()
2176 .filter(|s| !s.trim().is_empty())
2177 .is_some()
2178 {
2179 return;
2180 }
2181 static WARNED_DIRS: Mutex<Option<HashSet<String>>> = Mutex::new(None);
2182 if let Ok((bare, wt)) = read_core_bare_and_worktree(git_dir) {
2183 if bare && wt.is_some() {
2184 let key = git_dir
2185 .canonicalize()
2186 .unwrap_or_else(|_| git_dir.to_path_buf())
2187 .to_string_lossy()
2188 .to_string();
2189 let mut guard = WARNED_DIRS.lock().unwrap_or_else(|e| e.into_inner());
2190 let set = guard.get_or_insert_with(HashSet::new);
2191 if set.insert(key) {
2192 eprintln!("warning: core.bare and core.worktree do not make sense");
2193 }
2194 }
2195 }
2196}
2197
2198fn read_core_bare_and_worktree(git_dir: &Path) -> Result<(bool, Option<String>)> {
2199 let Some(config_path) = repository_config_path(git_dir) else {
2200 return Ok((false, None));
2201 };
2202 let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
2203 let mut in_core = false;
2204 let mut bare = false;
2205 let mut worktree: Option<String> = None;
2206 for raw_line in content.lines() {
2207 let line = raw_line.trim();
2208 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
2209 continue;
2210 }
2211 if line.starts_with('[') {
2212 in_core = line.eq_ignore_ascii_case("[core]");
2213 continue;
2214 }
2215 if !in_core {
2216 continue;
2217 }
2218 if let Some((k, v)) = line.split_once('=') {
2219 let key = k.trim();
2220 let val = v.trim();
2221 if key.eq_ignore_ascii_case("bare") {
2222 bare = val.eq_ignore_ascii_case("true");
2223 } else if key.eq_ignore_ascii_case("worktree") {
2224 worktree = Some(val.to_owned());
2225 }
2226 }
2227 }
2228 Ok((bare, worktree))
2229}
2230
2231fn validate_git_work_tree_path(path: &Path) -> Result<()> {
2234 if !path.is_absolute() {
2235 return Ok(());
2236 }
2237 let comps: Vec<Component<'_>> = path.components().collect();
2238 let Some(last_normal_idx) = comps
2239 .iter()
2240 .enumerate()
2241 .rev()
2242 .find_map(|(i, c)| matches!(c, Component::Normal(_)).then_some(i))
2243 else {
2244 return Ok(());
2245 };
2246 let mut cur = PathBuf::new();
2247 for (i, comp) in comps.iter().enumerate() {
2248 match comp {
2249 Component::Prefix(p) => cur.push(p.as_os_str()),
2250 Component::RootDir => cur.push(comp.as_os_str()),
2251 Component::CurDir => {}
2252 Component::ParentDir => {
2253 let _ = cur.pop();
2254 }
2255 Component::Normal(seg) => {
2256 cur.push(seg);
2257 if i != last_normal_idx && !cur.exists() {
2258 return Err(Error::PathError(format!(
2259 "Invalid path '{}': No such file or directory",
2260 cur.display()
2261 )));
2262 }
2263 }
2264 }
2265 }
2266 Ok(())
2267}
2268
2269fn resolve_core_worktree_path(git_dir: &Path, raw: &str) -> Result<PathBuf> {
2270 let p = Path::new(raw);
2271 if p.is_absolute() {
2272 return Ok(p.canonicalize().unwrap_or_else(|_| p.to_path_buf()));
2273 }
2274 let old = env::current_dir().map_err(Error::Io)?;
2275 env::set_current_dir(git_dir).map_err(Error::Io)?;
2276 env::set_current_dir(raw).map_err(Error::Io)?;
2277 let resolved = env::current_dir().map_err(Error::Io)?;
2278 env::set_current_dir(&old).map_err(Error::Io)?;
2279 Ok(resolved.canonicalize().unwrap_or(resolved))
2280}
2281
2282fn resolve_git_dir_env_path(git_dir: &Path) -> Result<PathBuf> {
2284 if git_dir.is_file() {
2285 let content =
2286 fs::read_to_string(git_dir).map_err(|e| Error::NotARepository(e.to_string()))?;
2287 let base = git_dir
2288 .parent()
2289 .ok_or_else(|| Error::NotARepository(git_dir.display().to_string()))?;
2290 return parse_gitfile(&content, base);
2291 }
2292 Ok(git_dir.to_path_buf())
2293}
2294
2295pub fn resolve_git_directory_arg(git_dir: &Path) -> Result<PathBuf> {
2301 resolve_git_dir_env_path(git_dir)
2302}
2303
2304pub fn resolve_dot_git(dot_git: &Path) -> Result<PathBuf> {
2310 if dot_git.is_dir() {
2311 return dot_git
2312 .canonicalize()
2313 .map_err(|_| Error::NotARepository(dot_git.display().to_string()));
2314 }
2315 if dot_git.is_file() {
2316 let content =
2317 fs::read_to_string(dot_git).map_err(|e| Error::NotARepository(e.to_string()))?;
2318 let base = dot_git
2319 .parent()
2320 .ok_or_else(|| Error::NotARepository(dot_git.display().to_string()))?;
2321 return parse_gitfile(&content, base);
2322 }
2323 Err(Error::NotARepository(dot_git.display().to_string()))
2324}
2325
2326fn parse_gitfile(content: &str, base: &Path) -> Result<PathBuf> {
2328 for line in content.lines() {
2329 if let Some(rest) = line.strip_prefix("gitdir:") {
2330 let rel = rest.trim();
2331 let path = if Path::new(rel).is_absolute() {
2332 PathBuf::from(rel)
2333 } else {
2334 base.join(rel)
2335 };
2336 if !path.exists() {
2337 return Err(Error::NotARepository(path.display().to_string()));
2338 }
2339 return Ok(path);
2340 }
2341 }
2342 Err(Error::NotARepository("invalid gitfile format".to_owned()))
2343}
2344
2345fn write_fresh_git_directory(
2362 git_dir: &Path,
2363 bare: bool,
2364 initial_branch: &str,
2365 template_dir: Option<&Path>,
2366 ref_storage: &str,
2367 skip_hooks_and_info: bool,
2368) -> Result<()> {
2369 let mut subs = vec![
2370 "objects",
2371 "objects/info",
2372 "objects/pack",
2373 "refs",
2374 "refs/heads",
2375 "refs/tags",
2376 ];
2377 if !bare && !skip_hooks_and_info {
2378 subs.push("info");
2379 subs.push("hooks");
2380 }
2381 for sub in subs {
2382 fs::create_dir_all(git_dir.join(sub))?;
2383 }
2384
2385 if ref_storage == "reftable" {
2386 let reftable_dir = git_dir.join("reftable");
2387 fs::create_dir_all(&reftable_dir)?;
2388 let tables_list = reftable_dir.join("tables.list");
2389 if !tables_list.exists() {
2390 fs::write(&tables_list, "")?;
2391 }
2392 }
2393
2394 if let Some(tmpl) = template_dir {
2395 if tmpl.is_dir() {
2396 copy_template(tmpl, git_dir)?;
2397 }
2398 }
2399
2400 let head_content = format!("ref: refs/heads/{initial_branch}\n");
2401 fs::write(git_dir.join("HEAD"), head_content)?;
2402
2403 let needs_extensions = ref_storage == "reftable";
2404 let repo_version = if needs_extensions { 1 } else { 0 };
2405
2406 let mut config_content = String::from("[core]\n");
2407 config_content.push_str(&format!("\trepositoryformatversion = {repo_version}\n"));
2408 config_content.push_str("\tfilemode = true\n");
2409 if bare {
2410 config_content.push_str("\tbare = true\n");
2411 } else {
2412 config_content.push_str("\tbare = false\n");
2413 config_content.push_str("\tlogallrefupdates = true\n");
2414 }
2415 if needs_extensions {
2416 config_content.push_str("[extensions]\n");
2417 config_content.push_str("\trefStorage = reftable\n");
2418 }
2419 fs::write(git_dir.join("config"), config_content)?;
2420
2421 if let Some(tmpl) = template_dir {
2423 if tmpl.is_dir() {
2424 let tmpl_config = tmpl.join("config");
2425 if tmpl_config.is_file() {
2426 let tmpl_text = fs::read_to_string(&tmpl_config)?;
2427 let tmpl_parsed = ConfigFile::parse(&tmpl_config, &tmpl_text, ConfigScope::Local)?;
2428 let dest_path = git_dir.join("config");
2429 let dest_text = fs::read_to_string(&dest_path)?;
2430 let mut dest_parsed =
2431 ConfigFile::parse(&dest_path, &dest_text, ConfigScope::Local)?;
2432 for e in &tmpl_parsed.entries {
2433 if e.key == "core.bare" {
2435 continue;
2436 }
2437 if let Some(v) = &e.value {
2438 let _ = dest_parsed.set(&e.key, v);
2439 } else {
2440 let _ = dest_parsed.set(&e.key, "true");
2441 }
2442 }
2443 dest_parsed.write()?;
2444 }
2445 }
2446 }
2447
2448 fs::write(
2449 git_dir.join("description"),
2450 "Unnamed repository; edit this file 'description' to name the repository.\n",
2451 )?;
2452 Ok(())
2453}
2454
2455pub fn init_repository_separate_git_dir(
2464 work_tree: &Path,
2465 git_dir: &Path,
2466 initial_branch: &str,
2467 template_dir: Option<&Path>,
2468 ref_storage: &str,
2469) -> Result<Repository> {
2470 let skip_hooks_info = template_dir.is_some_and(|p| p.as_os_str().is_empty());
2471 fs::create_dir_all(work_tree)?;
2472 fs::create_dir_all(git_dir)?;
2473 write_fresh_git_directory(
2474 git_dir,
2475 false,
2476 initial_branch,
2477 template_dir,
2478 ref_storage,
2479 skip_hooks_info,
2480 )?;
2481
2482 let gitfile = work_tree.join(".git");
2488 let abs_git_dir = fs::canonicalize(git_dir).unwrap_or_else(|_| git_dir.to_path_buf());
2489 let abs_git_dir = abs_git_dir.to_string_lossy().replace('\\', "/");
2490 fs::write(gitfile, format!("gitdir: {abs_git_dir}\n"))?;
2491
2492 Repository::open(git_dir, Some(work_tree))
2493}
2494
2495pub fn ensure_core_bare(git_dir: &Path) -> Result<()> {
2510 let path = git_dir.join("config");
2511 let text = fs::read_to_string(&path).unwrap_or_default();
2512 if text.lines().any(|l| {
2513 let t = l.trim();
2514 t == "bare = true" || t == "bare=true"
2515 }) {
2516 return Ok(());
2517 }
2518 let mut out = text;
2519 if !out.ends_with('\n') && !out.is_empty() {
2520 out.push('\n');
2521 }
2522 if !out.contains("[core]") {
2523 out.push_str("[core]\n");
2524 }
2525 out.push_str("\tbare = true\n");
2526 fs::write(path, out).map_err(Error::Io)
2527}
2528
2529pub fn init_bare_clone_minimal(
2530 git_dir: &Path,
2531 initial_branch: &str,
2532 ref_storage: &str,
2533) -> Result<()> {
2534 for sub in &[
2535 "objects",
2536 "objects/info",
2537 "objects/pack",
2538 "refs",
2539 "refs/heads",
2540 "refs/tags",
2541 ] {
2542 fs::create_dir_all(git_dir.join(sub))?;
2543 }
2544
2545 if ref_storage == "reftable" {
2546 let reftable_dir = git_dir.join("reftable");
2547 fs::create_dir_all(&reftable_dir)?;
2548 let tables_list = reftable_dir.join("tables.list");
2549 if !tables_list.exists() {
2550 fs::write(&tables_list, "")?;
2551 }
2552 }
2553
2554 let head_content = format!("ref: refs/heads/{initial_branch}\n");
2555 fs::write(git_dir.join("HEAD"), head_content)?;
2556
2557 let needs_extensions = ref_storage == "reftable";
2558 let repo_version = if needs_extensions { 1 } else { 0 };
2559 let mut config_content = String::from("[core]\n");
2560 config_content.push_str(&format!("\trepositoryformatversion = {repo_version}\n"));
2561 config_content.push_str("\tfilemode = true\n");
2562 config_content.push_str("\tbare = true\n");
2563 if needs_extensions {
2564 config_content.push_str("[extensions]\n");
2565 config_content.push_str("\trefStorage = reftable\n");
2566 }
2567 fs::write(git_dir.join("config"), config_content)?;
2568
2569 fs::write(
2570 git_dir.join("packed-refs"),
2571 "# pack-refs with: peeled fully-peeled sorted\n",
2572 )?;
2573 Ok(())
2574}
2575
2576pub fn init_repository(
2577 path: &Path,
2578 bare: bool,
2579 initial_branch: &str,
2580 template_dir: Option<&Path>,
2581 ref_storage: &str,
2582) -> Result<Repository> {
2583 let skip_hooks_info = !bare && template_dir.is_some_and(|p| p.as_os_str().is_empty());
2584 let git_dir = if bare {
2585 path.to_path_buf()
2586 } else {
2587 path.join(".git")
2588 };
2589
2590 if !bare {
2591 fs::create_dir_all(path)?;
2592 }
2593 fs::create_dir_all(&git_dir)?;
2594 write_fresh_git_directory(
2595 &git_dir,
2596 bare,
2597 initial_branch,
2598 template_dir,
2599 ref_storage,
2600 skip_hooks_info,
2601 )?;
2602
2603 let work_tree = if bare { None } else { Some(path) };
2604 Repository::open(&git_dir, work_tree)
2605}
2606
2607pub fn init_bare_with_env_worktree(
2616 git_dir: &Path,
2617 work_tree: &Path,
2618 initial_branch: &str,
2619 template_dir: Option<&Path>,
2620 ref_storage: &str,
2621) -> Result<Repository> {
2622 fs::create_dir_all(git_dir)?;
2623 fs::create_dir_all(work_tree)?;
2624 write_fresh_git_directory(
2625 git_dir,
2626 true,
2627 initial_branch,
2628 template_dir,
2629 ref_storage,
2630 false,
2631 )?;
2632 let work_tree_abs = fs::canonicalize(work_tree).unwrap_or_else(|_| work_tree.to_path_buf());
2633 let config_path = git_dir.join("config");
2634 let mut config = match ConfigFile::from_path(&config_path, ConfigScope::Local)? {
2635 Some(c) => c,
2636 None => ConfigFile::parse(&config_path, "", ConfigScope::Local)?,
2637 };
2638 config.set("core.worktree", &work_tree_abs.to_string_lossy())?;
2639 config.write()?;
2640 Repository::open(git_dir, Some(work_tree))
2641}
2642
2643pub fn init_repository_separate(
2648 work_tree: &Path,
2649 git_dir: &Path,
2650 initial_branch: &str,
2651 template_dir: Option<&Path>,
2652) -> Result<Repository> {
2653 fs::create_dir_all(work_tree)?;
2654 if git_dir.exists() {
2655 return Err(Error::PathError(format!(
2656 "git directory '{}' already exists",
2657 git_dir.display()
2658 )));
2659 }
2660
2661 for sub in &[
2662 "objects",
2663 "objects/info",
2664 "objects/pack",
2665 "refs",
2666 "refs/heads",
2667 "refs/tags",
2668 "info",
2669 "hooks",
2670 ] {
2671 fs::create_dir_all(git_dir.join(sub))?;
2672 }
2673
2674 if let Some(tmpl) = template_dir {
2675 if tmpl.is_dir() {
2676 copy_template(tmpl, git_dir)?;
2677 }
2678 }
2679
2680 fs::write(
2681 git_dir.join("HEAD"),
2682 format!("ref: refs/heads/{initial_branch}\n"),
2683 )?;
2684
2685 let work_tree_abs = fs::canonicalize(work_tree).unwrap_or_else(|_| work_tree.to_path_buf());
2686 let git_dir_abs = fs::canonicalize(git_dir).unwrap_or_else(|_| git_dir.to_path_buf());
2687 let config_content = format!(
2688 "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n\tlogallrefupdates = true\n\tworktree = {}\n",
2689 work_tree_abs.display()
2690 );
2691 fs::write(git_dir.join("config"), config_content)?;
2692 fs::write(
2693 git_dir.join("description"),
2694 "Unnamed repository; edit this file 'description' to name the repository.\n",
2695 )?;
2696
2697 let gitfile = work_tree.join(".git");
2698 fs::write(&gitfile, format!("gitdir: {}\n", git_dir_abs.display()))?;
2699
2700 Repository::open(git_dir, Some(work_tree))
2701}
2702
2703fn copy_template(src: &Path, dst: &Path) -> Result<()> {
2705 for entry in fs::read_dir(src)? {
2706 let entry = entry?;
2707 let src_path = entry.path();
2708 let dst_path = dst.join(entry.file_name());
2709 if src_path.is_dir() {
2710 fs::create_dir_all(&dst_path)?;
2711 copy_template(&src_path, &dst_path)?;
2712 } else {
2713 fs::copy(&src_path, &dst_path)?;
2714 }
2715 }
2716 Ok(())
2717}
2718
2719fn parse_ceiling_directories() -> (Vec<PathBuf>, bool) {
2728 let raw = match env::var("GIT_CEILING_DIRECTORIES") {
2729 Ok(val) => val,
2730 Err(_) => return (Vec::new(), false),
2731 };
2732 if raw.is_empty() {
2733 return (Vec::new(), false);
2734 }
2735 let (no_resolve, effective) = if raw.starts_with(':') {
2737 (true, &raw[1..])
2738 } else {
2739 (false, raw.as_str())
2740 };
2741 let paths = effective
2742 .split(':')
2743 .filter(|s| !s.is_empty())
2744 .filter_map(|s| {
2745 let p = PathBuf::from(s);
2746 if !p.is_absolute() {
2747 return None;
2748 }
2749 if no_resolve {
2750 let s = s.trim_end_matches('/');
2752 Some(PathBuf::from(s))
2753 } else {
2754 Some(p.canonicalize().unwrap_or_else(|_| {
2757 let s = s.trim_end_matches('/');
2758 PathBuf::from(s)
2759 }))
2760 }
2761 })
2762 .collect();
2763 (paths, no_resolve)
2764}
2765
2766pub fn validate_repo_config(config_text: &str) -> std::result::Result<(), String> {
2769 let mut version: u32 = 0;
2770 let mut in_core = false;
2771 for line in config_text.lines() {
2772 let trimmed = line.trim();
2773 if trimmed.starts_with('[') {
2774 in_core = trimmed.to_lowercase().starts_with("[core");
2775 continue;
2776 }
2777 if in_core {
2778 if let Some(rest) = trimmed.strip_prefix("repositoryformatversion") {
2779 let val = rest.trim_start_matches([' ', '=']).trim();
2780 if let Ok(v) = val.parse::<u32>() {
2781 version = v;
2782 }
2783 }
2784 }
2785 }
2786 if version >= 2 {
2787 return Err(format!("unknown repository format version: {version}"));
2788 }
2789 Ok(())
2790}