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_dirs: Vec<String> = parse_ceiling_directories()
328 .into_iter()
329 .map(|p| path_for_ceiling_compare(&p))
330 .collect();
331
332 let start_canon = start.canonicalize().unwrap_or_else(|_| start.clone());
333 let mut dir_buf = path_for_ceiling_compare(&start_canon);
334 let min_offset = offset_1st_component(&dir_buf);
335 let mut ceil_offset: isize = longest_ancestor_length(&dir_buf, &ceiling_dirs)
336 .map(|n| n as isize)
337 .unwrap_or(-1);
338 if ceil_offset < 0 {
339 ceil_offset = min_offset as isize - 2;
340 }
341
342 loop {
343 let current = Path::new(&dir_buf);
344 if let Some(DiscoveredAt { mut repo, gitfile }) = try_open_at(current)? {
345 if let Some(ref wt) = env_work_tree {
346 repo.work_tree = Some(wt.canonicalize().unwrap_or_else(|_| wt.clone()));
347 repo.work_tree_from_env = true;
348 } else {
349 repo.work_tree_from_env = false;
350 let linked_gitfile =
355 repo.discovery_via_gitfile && resolve_common_dir(&repo.git_dir).is_some();
356 if !linked_gitfile {
357 let (is_bare, core_wt) = read_core_bare_and_worktree(&repo.git_dir)?;
358 if is_bare {
359 repo.work_tree = None;
360 } else if let Some(raw) = core_wt {
361 repo.work_tree = Some(resolve_core_worktree_path(&repo.git_dir, &raw)?);
362 }
363 }
364 }
365 let assume_different = env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
366 .ok()
367 .map(|v| {
368 let lower = v.to_ascii_lowercase();
369 v == "1" || lower == "true" || lower == "yes" || lower == "on"
370 })
371 .unwrap_or(false);
372 if assume_different {
373 repo.enforce_safe_directory()?;
374 } else {
375 #[cfg(unix)]
376 ensure_valid_ownership(
377 gitfile.as_deref(),
378 repo.work_tree.as_deref(),
379 &repo.git_dir,
380 )?;
381 }
382 export_git_prefix_env(&repo);
383 return Ok(repo);
384 }
385
386 let mut offset: isize = dir_buf.len() as isize;
387 if offset <= min_offset as isize {
388 break;
389 }
390 loop {
391 offset -= 1;
392 if offset <= ceil_offset {
393 break;
394 }
395 if dir_buf
396 .as_bytes()
397 .get(offset as usize)
398 .is_some_and(|b| *b == b'/')
399 {
400 break;
401 }
402 }
403 if offset <= ceil_offset {
404 break;
405 }
406 let off_u = offset as usize;
407 let new_len = if off_u > min_offset {
408 off_u
409 } else {
410 min_offset
411 };
412 dir_buf.truncate(new_len);
413 }
414
415 Err(Error::NotARepository(start.display().to_string()))
416 }
417
418 #[must_use]
424 pub fn effective_pathspec_cwd(&self) -> PathBuf {
425 let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
426 let Some(wt) = self.work_tree.as_ref() else {
427 return cwd;
428 };
429 let inside_lexical = cwd.strip_prefix(wt).is_ok();
430 let inside_canon = cwd
431 .canonicalize()
432 .ok()
433 .zip(wt.canonicalize().ok())
434 .is_some_and(|(c, w)| c.starts_with(&w));
435 if inside_lexical || inside_canon {
436 cwd
437 } else {
438 wt.clone()
439 }
440 }
441
442 #[must_use]
444 pub fn index_path(&self) -> PathBuf {
445 self.git_dir.join("index")
446 }
447
448 pub fn index_path_for_env(&self) -> Result<PathBuf> {
452 if let Ok(raw) = env::var("GIT_INDEX_FILE") {
453 if !raw.is_empty() {
454 let p = PathBuf::from(raw);
455 return Ok(if p.is_absolute() {
456 p
457 } else {
458 env::current_dir().map_err(Error::Io)?.join(p)
459 });
460 }
461 }
462 Ok(self.index_path())
463 }
464
465 pub fn load_index(&self) -> Result<Index> {
469 let path = self.index_path_for_env()?;
470 self.load_index_at(&path)
471 }
472
473 pub fn load_index_at(&self, path: &std::path::Path) -> Result<Index> {
476 let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
477 if let Some(res) = cfg.get_bool("index.sparse") {
478 res.map_err(Error::ConfigError)?;
479 }
480 let mut idx = Index::load_expand_sparse_optional(path, &self.odb)?;
481 crate::split_index::resolve_split_index_if_needed(&mut idx, &self.git_dir, path)?;
482 if let Some(ref wt) = self.work_tree {
483 crate::sparse_checkout::clear_skip_worktree_from_present_files(
484 &self.git_dir,
485 wt,
486 &mut idx,
487 );
488 }
489 Ok(idx)
490 }
491
492 pub fn write_index(&self, index: &mut Index) -> Result<()> {
495 self.write_index_at(&self.index_path(), index)
496 }
497
498 pub fn write_index_at(&self, path: &std::path::Path, index: &mut Index) -> Result<()> {
500 self.write_index_at_split(path, index, WriteSplitIndexRequest::default())
501 }
502
503 pub fn write_index_at_split(
505 &self,
506 path: &std::path::Path,
507 index: &mut Index,
508 split: WriteSplitIndexRequest,
509 ) -> Result<()> {
510 self.finalize_sparse_index_if_needed(index)?;
511 let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
512 let skip_hash = crate::index::index_skip_hash_for_write(Some(&cfg));
513 write_index_file_split(path, &self.git_dir, index, &cfg, split, skip_hash)?;
514 let _ = run_hook(self, "post-index-change", &["0", "0"], None);
517 Ok(())
518 }
519
520 fn finalize_sparse_index_if_needed(&self, index: &mut Index) -> Result<()> {
521 let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
522 let sparse_enabled = cfg
523 .get("core.sparseCheckout")
524 .map(|v| v == "true")
525 .unwrap_or(false);
526 if !sparse_enabled {
527 index.sparse_directories = false;
528 return Ok(());
529 }
530 let cone_cfg = cfg
531 .get("core.sparseCheckoutCone")
532 .and_then(|v| v.parse::<bool>().ok())
533 .unwrap_or(true);
534 let sparse_ix = cfg
535 .get("index.sparse")
536 .map(|v| v == "true")
537 .unwrap_or(false);
538 let patterns = read_sparse_checkout_patterns(&self.git_dir);
539 let cone = effective_cone_mode_for_sparse_file(cone_cfg, &patterns);
540 let head = resolve_head(&self.git_dir)?;
541 let tree_oid = if let Some(oid) = head.oid() {
542 let obj = self.odb.read(oid)?;
543 let commit = parse_commit(&obj.data)?;
544 Some(commit.tree)
545 } else {
546 None
547 };
548 if let Some(t) = tree_oid {
549 index.try_collapse_sparse_directories(&self.odb, &t, &patterns, cone, sparse_ix)?;
550 } else {
551 index.sparse_directories = false;
552 }
553 Ok(())
554 }
555
556 #[must_use]
558 pub fn refs_dir(&self) -> PathBuf {
559 self.git_dir.join("refs")
560 }
561
562 #[must_use]
564 pub fn head_path(&self) -> PathBuf {
565 self.git_dir.join("HEAD")
566 }
567
568 #[must_use]
573 pub fn bloom_pathspec_cwd(&self) -> Option<String> {
574 let wt = self.work_tree.as_ref()?;
575 let cwd = env::current_dir().ok()?;
576 let wt = wt.canonicalize().ok()?;
577 let cwd = cwd.canonicalize().ok()?;
578 let rel = cwd.strip_prefix(&wt).ok()?;
579 let s = rel.to_string_lossy().replace('\\', "/");
580 let s = s.trim_start_matches('/').to_string();
581 Some(s)
582 }
583
584 #[must_use]
586 pub fn is_bare(&self) -> bool {
587 let config_path = self.git_dir.join("config");
589 if let Ok(content) = std::fs::read_to_string(&config_path) {
590 let mut in_core = false;
591 for line in content.lines() {
592 let t = line.trim();
593 if t.starts_with('[') {
594 in_core = t.eq_ignore_ascii_case("[core]");
595 continue;
596 }
597 if in_core {
598 if let Some((k, v)) = t.split_once('=') {
599 if k.trim().eq_ignore_ascii_case("bare") {
600 if v.trim().eq_ignore_ascii_case("true") {
601 return true;
602 } else if v.trim().eq_ignore_ascii_case("false") {
603 return false;
604 }
605 }
606 }
607 }
608 }
609 }
610 if self.work_tree.is_some() {
611 return false;
612 }
613 let config_path = self.git_dir.join("config");
616 if let Ok(content) = std::fs::read_to_string(&config_path) {
617 let mut in_core = false;
618 for line in content.lines() {
619 let t = line.trim();
620 if t.starts_with('[') {
621 in_core = t.eq_ignore_ascii_case("[core]");
622 continue;
623 }
624 if in_core {
625 if let Some((k, v)) = t.split_once('=') {
626 if k.trim().eq_ignore_ascii_case("bare") {
627 return v.trim().eq_ignore_ascii_case("true");
628 }
629 }
630 }
631 }
632 }
633 true
635 }
636
637 pub fn read_replaced(&self, oid: &crate::objects::ObjectId) -> Result<crate::objects::Object> {
644 if std::env::var_os("GIT_NO_REPLACE_OBJECTS").is_some() {
645 return self.odb.read(oid);
646 }
647 let settings = self.cached_settings();
648 if !settings.use_replace_refs {
649 return self.odb.read(oid);
650 }
651 let replace_ref =
652 self.git_dir
653 .join(format!("{}{}", settings.replace_ref_base, oid.to_hex()));
654 if replace_ref.is_file() {
655 if let Ok(content) = std::fs::read_to_string(&replace_ref) {
656 let hex = content.trim();
657 if let Ok(replacement_oid) = hex.parse::<crate::objects::ObjectId>() {
658 if let Ok(obj) = self.odb.read(&replacement_oid) {
659 return Ok(obj);
660 }
661 }
662 }
663 }
664 self.odb.read(oid)
665 }
666}
667
668pub fn trace_repo_setup_if_requested(repo: &Repository) -> std::io::Result<()> {
673 let Ok(path) = env::var("GIT_TRACE_SETUP") else {
674 return Ok(());
675 };
676 if path.is_empty() || path == "0" {
677 return Ok(());
678 }
679 let trace_path = Path::new(&path);
680 if !trace_path.is_absolute() {
681 return Ok(());
682 }
683
684 let actual_cwd = env::current_dir()?;
685 let actual_cwd = actual_cwd
686 .canonicalize()
687 .unwrap_or_else(|_| actual_cwd.clone());
688
689 let (trace_cwd, prefix) = if let Some(ref wt) = repo.work_tree {
692 let wt_canon = wt.canonicalize().unwrap_or_else(|_| wt.clone());
693 if actual_cwd.starts_with(&wt_canon) {
694 let rel = actual_cwd
695 .strip_prefix(&wt_canon)
696 .map(|p| p.to_path_buf())
697 .unwrap_or_default();
698 let prefix = if rel.as_os_str().is_empty() {
699 "(null)".to_owned()
700 } else {
701 let mut s = rel.to_string_lossy().replace('\\', "/");
702 if !s.ends_with('/') {
703 s.push('/');
704 }
705 s
706 };
707 (wt_canon, prefix)
708 } else {
709 (actual_cwd.clone(), "(null)".to_owned())
710 }
711 } else {
712 (actual_cwd.clone(), "(null)".to_owned())
713 };
714
715 let git_dir_display =
716 display_git_dir_for_setup_trace(repo, &trace_cwd, &actual_cwd, prefix.as_str());
717 let common_display = display_common_dir_for_setup_trace(
718 repo,
719 &trace_cwd,
720 &actual_cwd,
721 prefix.as_str(),
722 &git_dir_display,
723 );
724 let worktree_display = repo
725 .work_tree
726 .as_ref()
727 .map(|p| {
728 p.canonicalize()
729 .unwrap_or_else(|_| lexical_normalize_path(p))
730 .display()
731 .to_string()
732 })
733 .unwrap_or_else(|| "(null)".to_owned());
734
735 let mut f = OpenOptions::new()
736 .create(true)
737 .append(true)
738 .open(trace_path)?;
739 writeln!(f, "setup: git_dir: {git_dir_display}")?;
740 writeln!(f, "setup: git_common_dir: {common_display}")?;
741 writeln!(f, "setup: worktree: {worktree_display}")?;
742 writeln!(f, "setup: cwd: {}", trace_cwd.display())?;
743 writeln!(f, "setup: prefix: {prefix}")?;
744 Ok(())
745}
746
747fn lexical_normalize_path(path: &Path) -> PathBuf {
749 let mut out = PathBuf::new();
750 let mut absolute = false;
751 for c in path.components() {
752 match c {
753 Component::Prefix(p) => {
754 out.push(p.as_os_str());
755 }
756 Component::RootDir => {
757 absolute = true;
758 out.push(c.as_os_str());
759 }
760 Component::CurDir => {}
761 Component::ParentDir => {
762 if absolute {
763 let _ = out.pop();
764 } else if !out.pop() {
765 out.push("..");
766 }
767 }
768 Component::Normal(s) => out.push(s),
769 }
770 }
771 if out.as_os_str().is_empty() {
772 PathBuf::from(".")
773 } else {
774 out
775 }
776}
777
778fn path_relative_to(target: &Path, base: &Path) -> Option<PathBuf> {
780 let t = target.canonicalize().ok()?;
781 let b = base.canonicalize().ok()?;
782 let tc: Vec<_> = t.components().collect();
783 let bc: Vec<_> = b.components().collect();
784 let mut i = 0usize;
785 while i < tc.len() && i < bc.len() && tc[i] == bc[i] {
786 i += 1;
787 }
788 let up = bc.len().saturating_sub(i);
789 let mut out = PathBuf::new();
790 for _ in 0..up {
791 out.push("..");
792 }
793 for comp in &tc[i..] {
794 out.push(comp.as_os_str());
795 }
796 Some(out)
797}
798
799fn rel_path_for_setup_trace(target: &Path, trace_cwd: &Path) -> String {
800 let t = target
801 .canonicalize()
802 .unwrap_or_else(|_| target.to_path_buf());
803 let tc = trace_cwd
804 .canonicalize()
805 .unwrap_or_else(|_| trace_cwd.to_path_buf());
806 if let Some(rel) = path_relative_to(&t, &tc) {
807 let s = rel.to_string_lossy().replace('\\', "/");
808 return if s.is_empty() || s == "." {
809 ".".to_owned()
810 } else {
811 s
812 };
813 }
814 t.display().to_string()
815}
816
817fn trace_cwd_strictly_inside_git_parent(trace_cwd: &Path, git_dir: &Path) -> bool {
818 let tc = trace_cwd
819 .canonicalize()
820 .unwrap_or_else(|_| trace_cwd.to_path_buf());
821 let gd = git_dir
822 .canonicalize()
823 .unwrap_or_else(|_| git_dir.to_path_buf());
824 let Some(parent) = gd.parent() else {
825 return false;
826 };
827 let parent = parent.to_path_buf();
828 if tc == parent {
829 return false;
830 }
831 tc.starts_with(&parent) && tc != parent
832}
833
834fn display_git_dir_for_setup_trace(
835 repo: &Repository,
836 trace_cwd: &Path,
837 actual_cwd: &Path,
838 setup_prefix: &str,
839) -> String {
840 let gd = repo
841 .git_dir
842 .canonicalize()
843 .unwrap_or_else(|_| repo.git_dir.clone());
844 let tc = trace_cwd
845 .canonicalize()
846 .unwrap_or_else(|_| trace_cwd.to_path_buf());
847 let ac = actual_cwd
848 .canonicalize()
849 .unwrap_or_else(|_| actual_cwd.to_path_buf());
850
851 if repo.work_tree.is_none() && !repo.explicit_git_dir {
854 if ac == gd {
855 return ".".to_owned();
856 }
857 if ac.starts_with(&gd) && ac != gd {
858 return gd.display().to_string();
859 }
860 }
861
862 if !repo.explicit_git_dir {
864 if let Some(wt) = &repo.work_tree {
865 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
866 if ac.starts_with(&gd) && ac != wt {
867 return gd.display().to_string();
868 }
869 }
870 }
871
872 if repo.explicit_git_dir {
876 if repo.work_tree.is_none() {
877 if let Ok(raw) = env::var("GIT_DIR") {
878 let p = Path::new(raw.trim());
879 if p.is_absolute() {
880 return gd.display().to_string();
881 }
882 let joined = ac.join(p);
883 if joined.is_file() {
884 return gd.display().to_string();
885 }
886 if let Some(rel) = path_relative_to(&gd, &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 }
895 return gd.display().to_string();
896 }
897 if let Some(wt) = &repo.work_tree {
898 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
899 let strictly_inside_wt = ac.starts_with(&wt) && ac != wt;
900 if strictly_inside_wt {
901 return gd.display().to_string();
902 }
903 if let Ok(raw) = env::var("GIT_DIR") {
904 let p = Path::new(raw.trim());
905 if p.is_relative() {
906 let joined = ac.join(p);
907 if joined.is_file() {
908 return gd.display().to_string();
910 }
911 if let Some(rel) = path_relative_to(&gd, &tc) {
912 let s = rel.to_string_lossy().replace('\\', "/");
913 return if s.is_empty() || s == "." {
914 ".".to_owned()
915 } else {
916 s
917 };
918 }
919 }
920 return gd.display().to_string();
921 }
922 }
923 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
924 return rel_path_for_setup_trace(&gd, trace_cwd);
925 }
926 return gd.display().to_string();
927 }
928
929 let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
930 (Some(root), Some(wt)) if !repo.work_tree_from_env => {
931 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
932 let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
933 r != w
934 }
935 _ => false,
936 };
937
938 if repo.work_tree_from_env {
939 if !repo.discovery_via_gitfile {
940 if setup_prefix == "(null)" {
941 if let (Some(root), Some(wt)) = (&repo.discovery_root, &repo.work_tree) {
942 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
943 let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
944 if r == w {
945 let dot_git = r.join(".git");
946 let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
947 if gd == dot_git {
948 return ".git".to_owned();
949 }
950 }
951 }
952 }
953 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
954 return rel_path_for_setup_trace(&gd, trace_cwd);
955 }
956 }
957 return gd.display().to_string();
958 }
959
960 if work_relocated {
961 if let Some(wt) = &repo.work_tree {
962 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
963 if ac == wt {
964 return gd.display().to_string();
965 }
966 let inside_wt = ac.starts_with(&wt) && ac != wt;
967 if inside_wt {
968 if let Some(rel) = path_relative_to(&gd, &ac) {
969 let s = rel.to_string_lossy().replace('\\', "/");
970 return if s.is_empty() || s == "." {
971 ".".to_owned()
972 } else {
973 s
974 };
975 }
976 }
977 }
978 }
979 if repo.work_tree.is_some() {
980 if let Some(root) = &repo.discovery_root {
981 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
982 let dot_git = r.join(".git");
983 let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
984 if gd == dot_git {
985 return ".git".to_owned();
986 }
987 } else if let Some(wt) = &repo.work_tree {
988 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
989 let dot_git = wt.join(".git");
990 let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
991 if gd == dot_git {
992 return ".git".to_owned();
993 }
994 }
995 }
996
997 if repo.discovery_via_gitfile && !repo.explicit_git_dir {
998 return gd.display().to_string();
999 }
1000
1001 if repo.work_tree.is_none() && !repo.explicit_git_dir {
1005 if let Some(gp) = gd.parent() {
1006 let gp = gp.canonicalize().unwrap_or_else(|_| gp.to_path_buf());
1007 let gdc = gd.canonicalize().unwrap_or_else(|_| gd.clone());
1008 if tc.starts_with(&gp) && tc != gp && !tc.starts_with(&gdc) {
1009 return gdc.display().to_string();
1010 }
1011 if tc == gp {
1012 return rel_path_for_setup_trace(&gd, trace_cwd);
1013 }
1014 }
1015 }
1016
1017 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
1018 rel_path_for_setup_trace(&gd, trace_cwd)
1019 } else {
1020 gd.display().to_string()
1021 }
1022}
1023
1024fn display_common_dir_for_setup_trace(
1025 repo: &Repository,
1026 trace_cwd: &Path,
1027 actual_cwd: &Path,
1028 _setup_prefix: &str,
1029 git_dir_display: &str,
1030) -> String {
1031 let gd = repo
1032 .git_dir
1033 .canonicalize()
1034 .unwrap_or_else(|_| repo.git_dir.clone());
1035 let Some(common) = resolve_common_dir(&gd) else {
1036 return git_dir_display.to_owned();
1037 };
1038 let common = common.canonicalize().unwrap_or(common);
1039 if common == gd {
1040 return git_dir_display.to_owned();
1041 }
1042
1043 let ac = actual_cwd
1044 .canonicalize()
1045 .unwrap_or_else(|_| actual_cwd.to_path_buf());
1046 if repo.work_tree.is_none() && !repo.explicit_git_dir {
1047 if ac == common {
1048 return ".".to_owned();
1049 }
1050 if ac.starts_with(&common) && ac != common {
1051 return common.display().to_string();
1052 }
1053 }
1054
1055 let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
1056 (Some(root), Some(wt)) if !repo.work_tree_from_env => {
1057 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
1058 let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
1059 r != w
1060 }
1061 _ => false,
1062 };
1063 if work_relocated {
1064 if let Some(wt) = &repo.work_tree {
1065 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
1066 if ac == wt {
1067 return common.display().to_string();
1068 }
1069 let inside_wt = ac.starts_with(&wt) && ac != wt;
1070 if inside_wt {
1071 if let Some(rel) = path_relative_to(&common, &ac) {
1072 let s = rel.to_string_lossy().replace('\\', "/");
1073 return if s.is_empty() || s == "." {
1074 ".".to_owned()
1075 } else {
1076 s
1077 };
1078 }
1079 }
1080 }
1081 }
1082
1083 if repo.discovery_via_gitfile && !repo.explicit_git_dir {
1084 return common.display().to_string();
1085 }
1086
1087 if repo.work_tree.is_none() && !repo.explicit_git_dir {
1088 let tc = trace_cwd
1089 .canonicalize()
1090 .unwrap_or_else(|_| trace_cwd.to_path_buf());
1091 if let Some(cp) = common.parent() {
1092 let cp = cp.canonicalize().unwrap_or_else(|_| cp.to_path_buf());
1093 let comc = common.canonicalize().unwrap_or_else(|_| common.clone());
1094 if tc.starts_with(&cp) && tc != cp && !tc.starts_with(&comc) {
1095 return comc.display().to_string();
1096 }
1097 if tc == cp {
1098 return rel_path_for_setup_trace(&common, trace_cwd);
1099 }
1100 }
1101 }
1102
1103 if trace_cwd_strictly_inside_git_parent(trace_cwd, &common) {
1104 rel_path_for_setup_trace(&common, trace_cwd)
1105 } else {
1106 common.display().to_string()
1107 }
1108}
1109
1110fn resolve_common_dir(git_dir: &Path) -> Option<PathBuf> {
1112 let common_raw = fs::read_to_string(git_dir.join("commondir")).ok()?;
1113 let common_rel = common_raw.trim();
1114 if common_rel.is_empty() {
1115 return None;
1116 }
1117 let common_dir = if Path::new(common_rel).is_absolute() {
1118 PathBuf::from(common_rel)
1119 } else {
1120 git_dir.join(common_rel)
1121 };
1122 Some(common_dir.canonicalize().unwrap_or(common_dir))
1123}
1124
1125#[must_use]
1127pub fn common_git_dir_for_config(git_dir: &Path) -> PathBuf {
1128 resolve_common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf())
1129}
1130
1131pub fn worktree_config_enabled(common_dir: &Path) -> bool {
1133 let path = common_dir.join("config");
1134 let Ok(content) = fs::read_to_string(&path) else {
1135 return false;
1136 };
1137 let mut in_extensions = false;
1138 for raw_line in content.lines() {
1139 let mut line = raw_line.trim();
1140 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
1141 continue;
1142 }
1143 if line.starts_with('[') {
1144 let Some(end_idx) = line.find(']') else {
1145 continue;
1146 };
1147 let section = line[1..end_idx].trim();
1148 let section_name = section
1149 .split_whitespace()
1150 .next()
1151 .unwrap_or_default()
1152 .to_ascii_lowercase();
1153 in_extensions = section_name == "extensions";
1154 let remainder = line[end_idx + 1..].trim();
1155 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
1156 continue;
1157 }
1158 line = remainder;
1159 }
1160 if in_extensions {
1161 let Some((key, value)) = line.split_once('=') else {
1162 continue;
1163 };
1164 if key.trim().eq_ignore_ascii_case("worktreeconfig") {
1165 let v = value.trim();
1166 return v.eq_ignore_ascii_case("true")
1167 || v.eq_ignore_ascii_case("yes")
1168 || v.eq_ignore_ascii_case("on")
1169 || v == "1";
1170 }
1171 }
1172 }
1173 false
1174}
1175
1176pub fn early_config_ignore_repo_reason(common_dir: &Path) -> Option<String> {
1180 const GIT_REPO_VERSION_READ: u32 = 1;
1181 let path = common_dir.join("config");
1182 let content = fs::read_to_string(&path).ok()?;
1183 let mut version = 0u32;
1184 let mut in_core = false;
1185 for raw_line in content.lines() {
1186 let mut line = raw_line.trim();
1187 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
1188 continue;
1189 }
1190 if line.starts_with('[') {
1191 let Some(end_idx) = line.find(']') else {
1192 continue;
1193 };
1194 let section = line[1..end_idx].trim();
1195 let section_name = section
1196 .split_whitespace()
1197 .next()
1198 .unwrap_or_default()
1199 .to_ascii_lowercase();
1200 in_core = section_name == "core";
1201 let remainder = line[end_idx + 1..].trim();
1202 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
1203 continue;
1204 }
1205 line = remainder;
1206 }
1207 if in_core {
1208 if let Some((key, value)) = line.split_once('=') {
1209 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
1210 if let Ok(v) = value.trim().parse::<u32>() {
1211 version = v;
1212 }
1213 }
1214 }
1215 }
1216 }
1217 if version > GIT_REPO_VERSION_READ {
1218 Some(format!(
1219 "Expected git repo version <= {GIT_REPO_VERSION_READ}, found {version}"
1220 ))
1221 } else {
1222 None
1223 }
1224}
1225
1226fn path_for_ceiling_compare(path: &Path) -> String {
1227 path.to_string_lossy().replace('\\', "/")
1228}
1229
1230fn offset_1st_component(path: &str) -> usize {
1231 if path.starts_with('/') {
1232 1
1233 } else {
1234 0
1235 }
1236}
1237
1238fn longest_ancestor_length(path: &str, ceilings: &[String]) -> Option<usize> {
1240 if path == "/" {
1241 return None;
1242 }
1243 let mut max_len: Option<usize> = None;
1244 for ceil in ceilings {
1245 let mut len = ceil.len();
1246 while len > 0 && ceil.as_bytes().get(len - 1) == Some(&b'/') {
1247 len -= 1;
1248 }
1249 if len == 0 {
1250 continue;
1251 }
1252 if path.len() <= len + 1 {
1253 continue;
1254 }
1255 if !path.starts_with(&ceil[..len]) {
1256 continue;
1257 }
1258 if path.as_bytes().get(len) != Some(&b'/') {
1259 continue;
1260 }
1261 if path.as_bytes().get(len + 1).is_none() {
1262 continue;
1263 }
1264 max_len = Some(max_len.map_or(len, |m| m.max(len)));
1265 }
1266 max_len
1267}
1268
1269fn repository_config_path(git_dir: &Path) -> Option<PathBuf> {
1271 let local = git_dir.join("config");
1272 if local.exists() {
1273 return Some(local);
1274 }
1275 let common = resolve_common_dir(git_dir)?;
1276 let shared = common.join("config");
1277 if shared.exists() {
1278 Some(shared)
1279 } else {
1280 None
1281 }
1282}
1283
1284pub fn validate_repo_format(git_dir: &Path) -> Result<()> {
1290 validate_repository_format(git_dir)
1291}
1292
1293fn validate_repository_format(git_dir: &Path) -> Result<()> {
1294 let Some(config_path) = repository_config_path(git_dir) else {
1295 return Ok(());
1296 };
1297
1298 let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
1299 let mut in_core = false;
1300 let mut in_extensions = false;
1301 let mut repo_version = 0u32;
1302 let mut extensions = BTreeSet::new();
1303
1304 for raw_line in content.lines() {
1305 let mut line = raw_line.trim();
1306 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
1307 continue;
1308 }
1309
1310 if line.starts_with('[') {
1311 let Some(end_idx) = line.find(']') else {
1312 return Err(Error::ConfigError(format!(
1313 "invalid config in {}",
1314 config_path.display()
1315 )));
1316 };
1317
1318 let section = line[1..end_idx].trim();
1319 let section_name = section
1320 .split_whitespace()
1321 .next()
1322 .unwrap_or_default()
1323 .to_ascii_lowercase();
1324 in_core = section_name == "core";
1325 in_extensions = section_name == "extensions";
1326
1327 let remainder = line[end_idx + 1..].trim();
1328 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
1329 continue;
1330 }
1331 line = remainder;
1332 }
1333
1334 if in_core {
1335 if let Some((key, value)) = line.split_once('=') {
1336 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
1337 if let Ok(v) = value.trim().parse::<u32>() {
1339 repo_version = v;
1340 }
1341 }
1342 }
1343 }
1344
1345 if in_extensions {
1346 let key = if let Some((key, _)) = line.split_once('=') {
1347 key.trim()
1348 } else {
1349 line
1350 };
1351 if !key.is_empty() {
1352 extensions.insert(key.to_ascii_lowercase());
1353 }
1354 }
1355 }
1356
1357 if repo_version > 1 {
1358 return Err(Error::UnsupportedRepositoryFormatVersion(repo_version));
1359 }
1360
1361 for extension in extensions {
1362 if repo_version == 0 {
1363 if extension.ends_with("-v1") {
1364 return Err(Error::UnsupportedRepositoryExtension(extension));
1365 }
1366 continue;
1367 }
1368
1369 if matches!(
1370 extension.as_str(),
1371 "noop"
1372 | "noop-v1"
1373 | "preciousobjects"
1374 | "partialclone"
1375 | "worktreeconfig"
1376 | "objectformat"
1377 | "compatobjectformat"
1378 | "refstorage"
1379 | "submodulepathconfig"
1380 ) {
1381 continue;
1382 }
1383
1384 return Err(Error::UnsupportedRepositoryExtension(extension));
1385 }
1386
1387 Ok(())
1388}
1389
1390struct DiscoveredAt {
1396 repo: Repository,
1397 gitfile: Option<PathBuf>,
1399}
1400
1401fn try_open_at(dir: &Path) -> Result<Option<DiscoveredAt>> {
1402 let dot_git = dir.join(".git");
1403
1404 #[cfg(unix)]
1407 {
1408 use std::os::unix::fs::FileTypeExt;
1409 if let Ok(meta) = fs::symlink_metadata(&dot_git) {
1410 let ft = meta.file_type();
1411 if ft.is_fifo() || ft.is_socket() || ft.is_block_device() || ft.is_char_device() {
1412 return Err(Error::NotARepository(format!(
1413 "invalid gitfile format: {} is not a regular file",
1414 dot_git.display()
1415 )));
1416 }
1417 if ft.is_symlink() {
1418 if let Ok(target_meta) = fs::metadata(&dot_git) {
1419 let tft = target_meta.file_type();
1420 if tft.is_fifo()
1421 || tft.is_socket()
1422 || tft.is_block_device()
1423 || tft.is_char_device()
1424 {
1425 return Err(Error::NotARepository(format!(
1426 "invalid gitfile format: {} is not a regular file",
1427 dot_git.display()
1428 )));
1429 }
1430 }
1431 }
1432 }
1433 }
1434
1435 if dot_git.is_file() {
1436 let content =
1438 fs::read_to_string(&dot_git).map_err(|e| Error::NotARepository(e.to_string()))?;
1439 let git_dir = parse_gitfile(&content, dir)?;
1440 let mut repo = Repository::open_skipping_format_validation(&git_dir, Some(dir))?;
1441 if resolve_common_dir(&git_dir).is_some() {
1445 let cwd = env::current_dir().map_err(Error::Io)?;
1446 if repo.work_tree.is_some() && !is_inside_work_tree(&repo, &cwd) {
1447 let root = if dir.is_absolute() {
1448 dir.to_path_buf()
1449 } else {
1450 cwd.join(dir)
1451 };
1452 repo.work_tree = Some(root.canonicalize().unwrap_or(root));
1453 }
1454 }
1455 let root = if dir.is_absolute() {
1456 dir.to_path_buf()
1457 } else {
1458 env::current_dir().map_err(Error::Io)?.join(dir)
1459 };
1460 repo.discovery_root = Some(root.canonicalize().unwrap_or(root));
1461 repo.discovery_via_gitfile = true;
1462 warn_core_bare_worktree_conflict(&git_dir);
1463 return Ok(Some(DiscoveredAt {
1464 repo,
1465 gitfile: Some(dot_git.clone()),
1466 }));
1467 }
1468
1469 if dot_git.is_dir() {
1470 let open_path = if dot_git.is_symlink() {
1474 dot_git.read_link().unwrap_or_else(|_| dot_git.clone())
1476 } else {
1477 dot_git.clone()
1478 };
1479 match Repository::open_skipping_format_validation(&open_path, Some(dir)) {
1482 Ok(mut repo) => {
1483 if dot_git.is_symlink() {
1486 let abs_dot_git = if dot_git.is_absolute() {
1487 dot_git
1488 } else {
1489 dir.join(".git")
1490 };
1491 repo.git_dir = abs_dot_git;
1492 }
1493 let root = if dir.is_absolute() {
1494 dir.to_path_buf()
1495 } else {
1496 env::current_dir().map_err(Error::Io)?.join(dir)
1497 };
1498 repo.discovery_root = Some(root.canonicalize().unwrap_or(root));
1499 repo.discovery_via_gitfile = false;
1500 return Ok(Some(DiscoveredAt {
1501 repo,
1502 gitfile: None,
1503 }));
1504 }
1505 Err(Error::NotARepository(_)) => return Ok(None),
1506 Err(e) => return Err(e),
1507 }
1508 }
1509
1510 if dir.join("HEAD").is_file() && dir.join("commondir").is_file() {
1513 maybe_trace_implicit_bare_repository(dir);
1514 let repo = Repository::open(dir, None)?;
1515 warn_core_bare_worktree_conflict(dir);
1516 return Ok(Some(DiscoveredAt {
1517 repo,
1518 gitfile: None,
1519 }));
1520 }
1521
1522 if dir.join("objects").is_dir() && dir.join("HEAD").is_file() {
1524 maybe_trace_implicit_bare_repository(dir);
1525 if !is_inside_dot_git(dir) {
1529 if let Ok(cfg) = crate::config::ConfigSet::load(None, true) {
1530 if let Some(val) = cfg.get("safe.bareRepository") {
1531 if val.eq_ignore_ascii_case("explicit") {
1532 return Err(Error::ForbiddenBareRepository(dir.display().to_string()));
1533 }
1534 }
1535 }
1536 }
1537 let repo = Repository::open(dir, None)?;
1538 warn_core_bare_worktree_conflict(dir);
1539 return Ok(Some(DiscoveredAt {
1540 repo,
1541 gitfile: None,
1542 }));
1543 }
1544
1545 Ok(None)
1546}
1547
1548fn is_inside_dot_git(path: &Path) -> bool {
1549 path.components().any(|c| c.as_os_str() == ".git")
1550}
1551
1552fn maybe_trace_implicit_bare_repository(dir: &Path) {
1553 let path = match std::env::var("GIT_TRACE2_PERF") {
1554 Ok(p) if !p.is_empty() => p,
1555 _ => return,
1556 };
1557
1558 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
1559 let _ = writeln!(file, "setup: implicit-bare-repository:{}", dir.display());
1560 }
1561}
1562
1563fn safe_directory_effective_values(git_dir: &Path) -> Vec<String> {
1566 let cfg = crate::config::ConfigSet::load(Some(git_dir), true)
1567 .unwrap_or_else(|_| crate::config::ConfigSet::new());
1568 let mut values: Vec<String> = Vec::new();
1569 for e in cfg.entries() {
1570 if e.key == "safe.directory"
1571 && e.scope != crate::config::ConfigScope::Local
1572 && e.scope != crate::config::ConfigScope::Worktree
1573 {
1574 values.push(e.value.clone().unwrap_or_else(|| "true".to_owned()));
1575 }
1576 }
1577 let mut effective: Vec<String> = Vec::new();
1578 for v in values {
1579 if v.is_empty() {
1580 effective.clear();
1581 } else {
1582 effective.push(v);
1583 }
1584 }
1585 effective
1586}
1587
1588fn ensure_safe_directory_allows(git_dir: &Path, checked: &Path) -> Result<()> {
1589 let effective = safe_directory_effective_values(git_dir);
1590 let checked_s = checked.to_string_lossy().to_string();
1591 if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
1592 eprintln!("debug-safe-directory values={:?}", effective);
1593 }
1594 if effective
1595 .iter()
1596 .any(|v| safe_directory_matches(v, &checked_s))
1597 {
1598 return Ok(());
1599 }
1600 Err(Error::DubiousOwnership(checked_s))
1601}
1602
1603#[cfg(unix)]
1604fn path_lstat_uid(path: &Path) -> std::io::Result<u32> {
1605 use std::os::unix::fs::MetadataExt;
1606 let meta = fs::symlink_metadata(path)?;
1607 Ok(meta.uid())
1608}
1609
1610#[cfg(unix)]
1611fn extract_uid_from_env(name: &str) -> Option<u32> {
1612 let raw = std::env::var(name).ok()?;
1613 if raw.is_empty() {
1614 return None;
1615 }
1616 raw.parse::<u32>().ok()
1617}
1618
1619#[cfg(unix)]
1622fn ensure_valid_ownership(
1623 gitfile: Option<&Path>,
1624 worktree: Option<&Path>,
1625 gitdir: &Path,
1626) -> Result<()> {
1627 const ROOT_UID: u32 = 0;
1628
1629 fn owned_by_effective_user(path: &Path) -> std::io::Result<bool> {
1630 let st_uid = path_lstat_uid(path)?;
1631 let mut euid = unsafe { libc::geteuid() };
1632 if euid == ROOT_UID {
1633 if st_uid == ROOT_UID {
1634 return Ok(true);
1635 }
1636 if let Some(sudo_uid) = extract_uid_from_env("SUDO_UID") {
1637 euid = sudo_uid;
1638 }
1639 }
1640 Ok(st_uid == euid)
1641 }
1642
1643 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
1644 .ok()
1645 .map(|v| {
1646 let lower = v.to_ascii_lowercase();
1647 v == "1" || lower == "true" || lower == "yes" || lower == "on"
1648 })
1649 .unwrap_or(false);
1650 if !assume_different {
1651 let gitfile_ok = gitfile
1652 .map(owned_by_effective_user)
1653 .transpose()?
1654 .unwrap_or(true);
1655 let wt_ok = match worktree {
1658 None => true,
1659 Some(wt) => match owned_by_effective_user(wt) {
1660 Ok(ok) => ok,
1661 Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
1662 Err(e) => return Err(Error::Io(e)),
1663 },
1664 };
1665 let gd_ok = owned_by_effective_user(gitdir)?;
1666 if gitfile_ok && wt_ok && gd_ok {
1667 return Ok(());
1668 }
1669 }
1670
1671 let data_path = if let Some(wt) = worktree {
1672 wt.canonicalize().unwrap_or_else(|_| wt.to_path_buf())
1673 } else {
1674 gitdir
1675 .canonicalize()
1676 .unwrap_or_else(|_| gitdir.to_path_buf())
1677 };
1678 ensure_safe_directory_allows(gitdir, &data_path)
1679}
1680
1681#[cfg(not(unix))]
1682fn ensure_valid_ownership(
1683 _gitfile: Option<&Path>,
1684 _worktree: Option<&Path>,
1685 _gitdir: &Path,
1686) -> Result<()> {
1687 Ok(())
1688}
1689
1690impl Repository {
1691 pub fn enforce_safe_directory(&self) -> Result<()> {
1697 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
1698 .ok()
1699 .map(|v| {
1700 let lower = v.to_ascii_lowercase();
1701 v == "1" || lower == "true" || lower == "yes" || lower == "on"
1702 })
1703 .unwrap_or(false);
1704 if !assume_different {
1705 return Ok(());
1706 }
1707
1708 if self.explicit_git_dir {
1709 return Ok(());
1710 }
1711
1712 let checked = if let Some(wt) = &self.work_tree {
1716 let cwd = std::env::current_dir().ok();
1717 if let Some(cwd) = cwd {
1718 if cwd
1719 .canonicalize()
1720 .ok()
1721 .is_some_and(|c| c.starts_with(&self.git_dir))
1722 {
1723 self.git_dir
1724 .canonicalize()
1725 .unwrap_or_else(|_| self.git_dir.clone())
1726 } else {
1727 wt.canonicalize().unwrap_or_else(|_| wt.clone())
1728 }
1729 } else {
1730 wt.canonicalize().unwrap_or_else(|_| wt.clone())
1731 }
1732 } else {
1733 self.git_dir
1734 .canonicalize()
1735 .unwrap_or_else(|_| self.git_dir.clone())
1736 };
1737
1738 if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
1739 eprintln!(
1740 "debug-safe-directory checked={} git_dir={} work_tree={:?} cwd={:?}",
1741 checked.display(),
1742 self.git_dir.display(),
1743 self.work_tree,
1744 std::env::current_dir().ok()
1745 );
1746 }
1747 self.enforce_safe_directory_checked(&checked)
1748 }
1749
1750 pub fn enforce_safe_directory_git_dir(&self) -> Result<()> {
1755 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
1756 .ok()
1757 .map(|v| {
1758 let lower = v.to_ascii_lowercase();
1759 v == "1" || lower == "true" || lower == "yes" || lower == "on"
1760 })
1761 .unwrap_or(false);
1762 if !assume_different {
1763 return Ok(());
1764 }
1765 let checked = self
1766 .git_dir
1767 .canonicalize()
1768 .unwrap_or_else(|_| self.git_dir.clone());
1769 if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
1770 eprintln!(
1771 "debug-safe-directory(gitdir) checked={} git_dir={} work_tree={:?}",
1772 checked.display(),
1773 self.git_dir.display(),
1774 self.work_tree
1775 );
1776 }
1777 self.enforce_safe_directory_checked(&checked)
1778 }
1779
1780 pub fn enforce_safe_directory_git_dir_with_path(&self, checked: &Path) -> Result<()> {
1782 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
1783 .ok()
1784 .map(|v| {
1785 let lower = v.to_ascii_lowercase();
1786 v == "1" || lower == "true" || lower == "yes" || lower == "on"
1787 })
1788 .unwrap_or(false);
1789 if !assume_different {
1790 return Ok(());
1791 }
1792 self.enforce_safe_directory_checked(checked)
1793 }
1794
1795 fn enforce_safe_directory_checked(&self, checked: &Path) -> Result<()> {
1796 ensure_safe_directory_allows(&self.git_dir, checked)
1797 }
1798
1799 pub fn verify_safe_for_clone_source(&self) -> Result<()> {
1805 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
1806 .ok()
1807 .map(|v| {
1808 let lower = v.to_ascii_lowercase();
1809 v == "1" || lower == "true" || lower == "yes" || lower == "on"
1810 })
1811 .unwrap_or(false);
1812 if assume_different {
1813 self.enforce_safe_directory_git_dir()
1814 } else {
1815 #[cfg(unix)]
1816 {
1817 ensure_valid_ownership(None, None, &self.git_dir)
1818 }
1819 #[cfg(not(unix))]
1820 {
1821 Ok(())
1822 }
1823 }
1824 }
1825}
1826
1827fn normalize_fs_path(raw: &str) -> String {
1828 use std::path::Component;
1829 let p = std::path::Path::new(raw);
1830 let mut parts: Vec<String> = Vec::new();
1831 let mut absolute = false;
1832 for c in p.components() {
1833 match c {
1834 Component::RootDir => {
1835 absolute = true;
1836 parts.clear();
1837 }
1838 Component::CurDir => {}
1839 Component::ParentDir => {
1840 if !parts.is_empty() {
1841 parts.pop();
1842 }
1843 }
1844 Component::Normal(s) => parts.push(s.to_string_lossy().to_string()),
1845 Component::Prefix(_) => {}
1846 }
1847 }
1848 let mut out = if absolute {
1849 String::from("/")
1850 } else {
1851 String::new()
1852 };
1853 out.push_str(&parts.join("/"));
1854 out
1855}
1856
1857fn safe_directory_matches(config_value: &str, checked: &str) -> bool {
1858 if config_value == "*" {
1859 return true;
1860 }
1861 if config_value == "." {
1862 if let Ok(cwd) = std::env::current_dir() {
1864 let cwd_s = normalize_fs_path(&cwd.to_string_lossy());
1865 let checked_s = normalize_fs_path(checked);
1866 return cwd_s == checked_s;
1867 }
1868 return false;
1869 }
1870
1871 let canonicalize_or_normalize = |raw: &str| -> String {
1872 let p = std::path::Path::new(raw);
1873 if p.exists() {
1874 p.canonicalize()
1875 .map(|c| c.to_string_lossy().to_string())
1876 .map(|s| normalize_fs_path(&s))
1877 .unwrap_or_else(|_| normalize_fs_path(raw))
1878 } else {
1879 normalize_fs_path(raw)
1880 }
1881 };
1882
1883 let config_norm = canonicalize_or_normalize(config_value);
1884 let checked_norm = normalize_fs_path(checked);
1885
1886 if config_norm.ends_with("/*") {
1887 let prefix_raw = &config_norm[..config_norm.len() - 2];
1888 let prefix_norm = canonicalize_or_normalize(prefix_raw);
1889 let mut prefix = prefix_norm;
1890 if !prefix.ends_with('/') {
1891 prefix.push('/');
1892 }
1893 return checked_norm.starts_with(&prefix);
1894 }
1895
1896 config_norm == checked_norm
1897}
1898
1899fn warn_core_bare_worktree_conflict(git_dir: &Path) {
1900 if env::var("GIT_WORK_TREE")
1901 .ok()
1902 .filter(|s| !s.trim().is_empty())
1903 .is_some()
1904 {
1905 return;
1906 }
1907 static WARNED_DIRS: Mutex<Option<HashSet<String>>> = Mutex::new(None);
1908 if let Ok((bare, wt)) = read_core_bare_and_worktree(git_dir) {
1909 if bare && wt.is_some() {
1910 let key = git_dir
1911 .canonicalize()
1912 .unwrap_or_else(|_| git_dir.to_path_buf())
1913 .to_string_lossy()
1914 .to_string();
1915 let mut guard = WARNED_DIRS.lock().unwrap_or_else(|e| e.into_inner());
1916 let set = guard.get_or_insert_with(HashSet::new);
1917 if set.insert(key) {
1918 eprintln!("warning: core.bare and core.worktree do not make sense");
1919 }
1920 }
1921 }
1922}
1923
1924fn read_core_bare_and_worktree(git_dir: &Path) -> Result<(bool, Option<String>)> {
1925 let Some(config_path) = repository_config_path(git_dir) else {
1926 return Ok((false, None));
1927 };
1928 let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
1929 let mut in_core = false;
1930 let mut bare = false;
1931 let mut worktree: Option<String> = None;
1932 for raw_line in content.lines() {
1933 let line = raw_line.trim();
1934 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
1935 continue;
1936 }
1937 if line.starts_with('[') {
1938 in_core = line.eq_ignore_ascii_case("[core]");
1939 continue;
1940 }
1941 if !in_core {
1942 continue;
1943 }
1944 if let Some((k, v)) = line.split_once('=') {
1945 let key = k.trim();
1946 let val = v.trim();
1947 if key.eq_ignore_ascii_case("bare") {
1948 bare = val.eq_ignore_ascii_case("true");
1949 } else if key.eq_ignore_ascii_case("worktree") {
1950 worktree = Some(val.to_owned());
1951 }
1952 }
1953 }
1954 Ok((bare, worktree))
1955}
1956
1957fn validate_git_work_tree_path(path: &Path) -> Result<()> {
1960 if !path.is_absolute() {
1961 return Ok(());
1962 }
1963 let comps: Vec<Component<'_>> = path.components().collect();
1964 let Some(last_normal_idx) = comps
1965 .iter()
1966 .enumerate()
1967 .rev()
1968 .find_map(|(i, c)| matches!(c, Component::Normal(_)).then_some(i))
1969 else {
1970 return Ok(());
1971 };
1972 let mut cur = PathBuf::new();
1973 for (i, comp) in comps.iter().enumerate() {
1974 match comp {
1975 Component::Prefix(p) => cur.push(p.as_os_str()),
1976 Component::RootDir => cur.push(comp.as_os_str()),
1977 Component::CurDir => {}
1978 Component::ParentDir => {
1979 let _ = cur.pop();
1980 }
1981 Component::Normal(seg) => {
1982 cur.push(seg);
1983 if i != last_normal_idx && !cur.exists() {
1984 return Err(Error::PathError(format!(
1985 "Invalid path '{}': No such file or directory",
1986 cur.display()
1987 )));
1988 }
1989 }
1990 }
1991 }
1992 Ok(())
1993}
1994
1995fn resolve_core_worktree_path(git_dir: &Path, raw: &str) -> Result<PathBuf> {
1996 let p = Path::new(raw);
1997 if p.is_absolute() {
1998 return Ok(p.canonicalize().unwrap_or_else(|_| p.to_path_buf()));
1999 }
2000 let old = env::current_dir().map_err(Error::Io)?;
2001 env::set_current_dir(git_dir).map_err(Error::Io)?;
2002 env::set_current_dir(raw).map_err(Error::Io)?;
2003 let resolved = env::current_dir().map_err(Error::Io)?;
2004 env::set_current_dir(&old).map_err(Error::Io)?;
2005 Ok(resolved.canonicalize().unwrap_or(resolved))
2006}
2007
2008fn resolve_git_dir_env_path(git_dir: &Path) -> Result<PathBuf> {
2010 if git_dir.is_file() {
2011 let content =
2012 fs::read_to_string(git_dir).map_err(|e| Error::NotARepository(e.to_string()))?;
2013 let base = git_dir
2014 .parent()
2015 .ok_or_else(|| Error::NotARepository(git_dir.display().to_string()))?;
2016 return parse_gitfile(&content, base);
2017 }
2018 Ok(git_dir.to_path_buf())
2019}
2020
2021pub fn resolve_git_directory_arg(git_dir: &Path) -> Result<PathBuf> {
2027 resolve_git_dir_env_path(git_dir)
2028}
2029
2030pub fn resolve_dot_git(dot_git: &Path) -> Result<PathBuf> {
2036 if dot_git.is_dir() {
2037 return dot_git
2038 .canonicalize()
2039 .map_err(|_| Error::NotARepository(dot_git.display().to_string()));
2040 }
2041 if dot_git.is_file() {
2042 let content =
2043 fs::read_to_string(dot_git).map_err(|e| Error::NotARepository(e.to_string()))?;
2044 let base = dot_git
2045 .parent()
2046 .ok_or_else(|| Error::NotARepository(dot_git.display().to_string()))?;
2047 return parse_gitfile(&content, base);
2048 }
2049 Err(Error::NotARepository(dot_git.display().to_string()))
2050}
2051
2052fn parse_gitfile(content: &str, base: &Path) -> Result<PathBuf> {
2054 for line in content.lines() {
2055 if let Some(rest) = line.strip_prefix("gitdir:") {
2056 let rel = rest.trim();
2057 let path = if Path::new(rel).is_absolute() {
2058 PathBuf::from(rel)
2059 } else {
2060 base.join(rel)
2061 };
2062 if !path.exists() {
2063 return Err(Error::NotARepository(path.display().to_string()));
2064 }
2065 return Ok(path);
2066 }
2067 }
2068 Err(Error::NotARepository("invalid gitfile format".to_owned()))
2069}
2070
2071fn write_fresh_git_directory(
2088 git_dir: &Path,
2089 bare: bool,
2090 initial_branch: &str,
2091 template_dir: Option<&Path>,
2092 ref_storage: &str,
2093 skip_hooks_and_info: bool,
2094) -> Result<()> {
2095 let mut subs = vec![
2096 "objects",
2097 "objects/info",
2098 "objects/pack",
2099 "refs",
2100 "refs/heads",
2101 "refs/tags",
2102 ];
2103 if !bare && !skip_hooks_and_info {
2104 subs.push("info");
2105 subs.push("hooks");
2106 }
2107 for sub in subs {
2108 fs::create_dir_all(git_dir.join(sub))?;
2109 }
2110
2111 if ref_storage == "reftable" {
2112 let reftable_dir = git_dir.join("reftable");
2113 fs::create_dir_all(&reftable_dir)?;
2114 let tables_list = reftable_dir.join("tables.list");
2115 if !tables_list.exists() {
2116 fs::write(&tables_list, "")?;
2117 }
2118 }
2119
2120 if let Some(tmpl) = template_dir {
2121 if tmpl.is_dir() {
2122 copy_template(tmpl, git_dir)?;
2123 }
2124 }
2125
2126 let head_content = format!("ref: refs/heads/{initial_branch}\n");
2127 fs::write(git_dir.join("HEAD"), head_content)?;
2128
2129 let needs_extensions = ref_storage == "reftable";
2130 let repo_version = if needs_extensions { 1 } else { 0 };
2131
2132 let mut config_content = String::from("[core]\n");
2133 config_content.push_str(&format!("\trepositoryformatversion = {repo_version}\n"));
2134 config_content.push_str("\tfilemode = true\n");
2135 if bare {
2136 config_content.push_str("\tbare = true\n");
2137 } else {
2138 config_content.push_str("\tbare = false\n");
2139 config_content.push_str("\tlogallrefupdates = true\n");
2140 }
2141 if needs_extensions {
2142 config_content.push_str("[extensions]\n");
2143 config_content.push_str("\trefStorage = reftable\n");
2144 }
2145 fs::write(git_dir.join("config"), config_content)?;
2146
2147 if let Some(tmpl) = template_dir {
2149 if tmpl.is_dir() {
2150 let tmpl_config = tmpl.join("config");
2151 if tmpl_config.is_file() {
2152 let tmpl_text = fs::read_to_string(&tmpl_config)?;
2153 let tmpl_parsed = ConfigFile::parse(&tmpl_config, &tmpl_text, ConfigScope::Local)?;
2154 let dest_path = git_dir.join("config");
2155 let dest_text = fs::read_to_string(&dest_path)?;
2156 let mut dest_parsed =
2157 ConfigFile::parse(&dest_path, &dest_text, ConfigScope::Local)?;
2158 for e in &tmpl_parsed.entries {
2159 if e.key == "core.bare" {
2161 continue;
2162 }
2163 if let Some(v) = &e.value {
2164 let _ = dest_parsed.set(&e.key, v);
2165 } else {
2166 let _ = dest_parsed.set(&e.key, "true");
2167 }
2168 }
2169 dest_parsed.write()?;
2170 }
2171 }
2172 }
2173
2174 fs::write(
2175 git_dir.join("description"),
2176 "Unnamed repository; edit this file 'description' to name the repository.\n",
2177 )?;
2178 Ok(())
2179}
2180
2181pub fn init_repository_separate_git_dir(
2190 work_tree: &Path,
2191 git_dir: &Path,
2192 initial_branch: &str,
2193 template_dir: Option<&Path>,
2194 ref_storage: &str,
2195) -> Result<Repository> {
2196 let skip_hooks_info = template_dir.is_some_and(|p| p.as_os_str().is_empty());
2197 fs::create_dir_all(work_tree)?;
2198 fs::create_dir_all(git_dir)?;
2199 write_fresh_git_directory(
2200 git_dir,
2201 false,
2202 initial_branch,
2203 template_dir,
2204 ref_storage,
2205 skip_hooks_info,
2206 )?;
2207
2208 let gitfile = work_tree.join(".git");
2212 let rel_git_dir = pathdiff_relative_gitfile(work_tree, git_dir);
2213 fs::write(gitfile, format!("gitdir: {rel_git_dir}\n"))?;
2214
2215 Repository::open(git_dir, Some(work_tree))
2216}
2217
2218fn pathdiff_relative_gitfile(from: &Path, to: &Path) -> String {
2220 let from_c = fs::canonicalize(from).unwrap_or_else(|_| from.to_path_buf());
2221 let to_c = fs::canonicalize(to).unwrap_or_else(|_| to.to_path_buf());
2222 let from_comp: Vec<Component<'_>> = from_c.components().collect();
2223 let to_comp: Vec<Component<'_>> = to_c.components().collect();
2224 let mut i = 0usize;
2225 while i < from_comp.len() && i < to_comp.len() && from_comp[i] == to_comp[i] {
2226 i += 1;
2227 }
2228 let mut out = PathBuf::new();
2229 for _ in i..from_comp.len() {
2230 out.push("..");
2231 }
2232 for c in &to_comp[i..] {
2233 out.push(c.as_os_str());
2234 }
2235 out.to_string_lossy().replace('\\', "/")
2236}
2237
2238pub fn init_bare_clone_minimal(
2252 git_dir: &Path,
2253 initial_branch: &str,
2254 ref_storage: &str,
2255) -> Result<()> {
2256 for sub in &[
2257 "objects",
2258 "objects/info",
2259 "objects/pack",
2260 "refs",
2261 "refs/heads",
2262 "refs/tags",
2263 ] {
2264 fs::create_dir_all(git_dir.join(sub))?;
2265 }
2266
2267 if ref_storage == "reftable" {
2268 let reftable_dir = git_dir.join("reftable");
2269 fs::create_dir_all(&reftable_dir)?;
2270 let tables_list = reftable_dir.join("tables.list");
2271 if !tables_list.exists() {
2272 fs::write(&tables_list, "")?;
2273 }
2274 }
2275
2276 let head_content = format!("ref: refs/heads/{initial_branch}\n");
2277 fs::write(git_dir.join("HEAD"), head_content)?;
2278
2279 let needs_extensions = ref_storage == "reftable";
2280 let repo_version = if needs_extensions { 1 } else { 0 };
2281 let mut config_content = String::from("[core]\n");
2282 config_content.push_str(&format!("\trepositoryformatversion = {repo_version}\n"));
2283 config_content.push_str("\tfilemode = true\n");
2284 config_content.push_str("\tbare = true\n");
2285 if needs_extensions {
2286 config_content.push_str("[extensions]\n");
2287 config_content.push_str("\trefStorage = reftable\n");
2288 }
2289 fs::write(git_dir.join("config"), config_content)?;
2290
2291 fs::write(
2292 git_dir.join("packed-refs"),
2293 "# pack-refs with: peeled fully-peeled sorted\n",
2294 )?;
2295 Ok(())
2296}
2297
2298pub fn init_repository(
2299 path: &Path,
2300 bare: bool,
2301 initial_branch: &str,
2302 template_dir: Option<&Path>,
2303 ref_storage: &str,
2304) -> Result<Repository> {
2305 let skip_hooks_info = !bare && template_dir.is_some_and(|p| p.as_os_str().is_empty());
2306 let git_dir = if bare {
2307 path.to_path_buf()
2308 } else {
2309 path.join(".git")
2310 };
2311
2312 if !bare {
2313 fs::create_dir_all(path)?;
2314 }
2315 fs::create_dir_all(&git_dir)?;
2316 write_fresh_git_directory(
2317 &git_dir,
2318 bare,
2319 initial_branch,
2320 template_dir,
2321 ref_storage,
2322 skip_hooks_info,
2323 )?;
2324
2325 let work_tree = if bare { None } else { Some(path) };
2326 Repository::open(&git_dir, work_tree)
2327}
2328
2329pub fn init_bare_with_env_worktree(
2338 git_dir: &Path,
2339 work_tree: &Path,
2340 initial_branch: &str,
2341 template_dir: Option<&Path>,
2342 ref_storage: &str,
2343) -> Result<Repository> {
2344 fs::create_dir_all(git_dir)?;
2345 fs::create_dir_all(work_tree)?;
2346 write_fresh_git_directory(
2347 git_dir,
2348 true,
2349 initial_branch,
2350 template_dir,
2351 ref_storage,
2352 false,
2353 )?;
2354 let work_tree_abs = fs::canonicalize(work_tree).unwrap_or_else(|_| work_tree.to_path_buf());
2355 let config_path = git_dir.join("config");
2356 let mut config = match ConfigFile::from_path(&config_path, ConfigScope::Local)? {
2357 Some(c) => c,
2358 None => ConfigFile::parse(&config_path, "", ConfigScope::Local)?,
2359 };
2360 config.set("core.worktree", &work_tree_abs.to_string_lossy())?;
2361 config.write()?;
2362 Repository::open(git_dir, Some(work_tree))
2363}
2364
2365pub fn init_repository_separate(
2370 work_tree: &Path,
2371 git_dir: &Path,
2372 initial_branch: &str,
2373 template_dir: Option<&Path>,
2374) -> Result<Repository> {
2375 fs::create_dir_all(work_tree)?;
2376 if git_dir.exists() {
2377 return Err(Error::PathError(format!(
2378 "git directory '{}' already exists",
2379 git_dir.display()
2380 )));
2381 }
2382
2383 for sub in &[
2384 "objects",
2385 "objects/info",
2386 "objects/pack",
2387 "refs",
2388 "refs/heads",
2389 "refs/tags",
2390 "info",
2391 "hooks",
2392 ] {
2393 fs::create_dir_all(git_dir.join(sub))?;
2394 }
2395
2396 if let Some(tmpl) = template_dir {
2397 if tmpl.is_dir() {
2398 copy_template(tmpl, git_dir)?;
2399 }
2400 }
2401
2402 fs::write(
2403 git_dir.join("HEAD"),
2404 format!("ref: refs/heads/{initial_branch}\n"),
2405 )?;
2406
2407 let work_tree_abs = fs::canonicalize(work_tree).unwrap_or_else(|_| work_tree.to_path_buf());
2408 let git_dir_abs = fs::canonicalize(git_dir).unwrap_or_else(|_| git_dir.to_path_buf());
2409 let config_content = format!(
2410 "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n\tlogallrefupdates = true\n\tworktree = {}\n",
2411 work_tree_abs.display()
2412 );
2413 fs::write(git_dir.join("config"), config_content)?;
2414 fs::write(
2415 git_dir.join("description"),
2416 "Unnamed repository; edit this file 'description' to name the repository.\n",
2417 )?;
2418
2419 let gitfile = work_tree.join(".git");
2420 fs::write(&gitfile, format!("gitdir: {}\n", git_dir_abs.display()))?;
2421
2422 Repository::open(git_dir, Some(work_tree))
2423}
2424
2425fn copy_template(src: &Path, dst: &Path) -> Result<()> {
2427 for entry in fs::read_dir(src)? {
2428 let entry = entry?;
2429 let src_path = entry.path();
2430 let dst_path = dst.join(entry.file_name());
2431 if src_path.is_dir() {
2432 fs::create_dir_all(&dst_path)?;
2433 copy_template(&src_path, &dst_path)?;
2434 } else {
2435 fs::copy(&src_path, &dst_path)?;
2436 }
2437 }
2438 Ok(())
2439}
2440
2441fn parse_ceiling_directories() -> Vec<PathBuf> {
2446 let raw = match env::var("GIT_CEILING_DIRECTORIES") {
2447 Ok(val) => val,
2448 Err(_) => return Vec::new(),
2449 };
2450 if raw.is_empty() {
2451 return Vec::new();
2452 }
2453 raw.split(':')
2454 .filter(|s| !s.is_empty())
2455 .filter_map(|s| {
2456 let p = PathBuf::from(s);
2457 if !p.is_absolute() {
2458 return None;
2459 }
2460 Some(p.canonicalize().unwrap_or_else(|_| {
2463 let s = s.trim_end_matches('/');
2465 PathBuf::from(s)
2466 }))
2467 })
2468 .collect()
2469}
2470
2471pub fn validate_repo_config(config_text: &str) -> std::result::Result<(), String> {
2474 let mut version: u32 = 0;
2475 let mut in_core = false;
2476 for line in config_text.lines() {
2477 let trimmed = line.trim();
2478 if trimmed.starts_with('[') {
2479 in_core = trimmed.to_lowercase().starts_with("[core");
2480 continue;
2481 }
2482 if in_core {
2483 if let Some(rest) = trimmed.strip_prefix("repositoryformatversion") {
2484 let val = rest.trim_start_matches([' ', '=']).trim();
2485 if let Ok(v) = val.parse::<u32>() {
2486 version = v;
2487 }
2488 }
2489 }
2490 }
2491 if version >= 2 {
2492 return Err(format!("unknown repository format version: {version}"));
2493 }
2494 Ok(())
2495}