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