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::index::Index;
29use crate::objects::parse_commit;
30use crate::odb::Odb;
31use crate::rev_parse::is_inside_work_tree;
32use crate::sparse_checkout::effective_cone_mode_for_sparse_file;
33use crate::state::resolve_head;
34
35fn read_sparse_checkout_patterns(git_dir: &Path) -> Vec<String> {
36 let path = git_dir.join("info").join("sparse-checkout");
37 let Ok(content) = fs::read_to_string(&path) else {
38 return Vec::new();
39 };
40 content
41 .lines()
42 .map(|l| l.trim())
43 .filter(|l| !l.is_empty() && !l.starts_with('#'))
44 .map(String::from)
45 .collect()
46}
47
48#[derive(Debug)]
50pub struct Repository {
51 pub git_dir: PathBuf,
53 pub work_tree: Option<PathBuf>,
55 pub odb: Odb,
57 pub explicit_git_dir: bool,
61 pub discovery_root: Option<PathBuf>,
64 pub work_tree_from_env: bool,
66 pub discovery_via_gitfile: bool,
68}
69
70impl Repository {
71 fn from_canonical_git_dir(git_dir: PathBuf, work_tree: Option<&Path>) -> Result<Self> {
72 let head_path = git_dir.join("HEAD");
74 if !head_path.exists() && !head_path.is_symlink() {
75 return Err(Error::NotARepository(git_dir.display().to_string()));
76 }
77
78 let objects_dir = if git_dir.join("objects").exists() {
81 git_dir.join("objects")
82 } else if let Some(common_dir) = resolve_common_dir(&git_dir) {
83 common_dir.join("objects")
84 } else {
85 return Err(Error::NotARepository(git_dir.display().to_string()));
86 };
87
88 if !objects_dir.exists() {
89 return Err(Error::NotARepository(git_dir.display().to_string()));
90 }
91
92 let work_tree = match work_tree {
93 Some(p) => {
94 let cwd = env::current_dir().map_err(Error::Io)?;
95 let mut resolved = if p.is_absolute() {
96 p.to_path_buf()
97 } else {
98 cwd.join(p)
99 };
100 if resolved.exists() {
101 resolved = resolved
102 .canonicalize()
103 .map_err(|_| Error::PathError(p.display().to_string()))?;
104 }
105 Some(resolved)
106 }
107 None => None,
108 };
109
110 let odb = if let Some(ref wt) = work_tree {
111 Odb::with_work_tree(&objects_dir, wt)
112 } else {
113 Odb::new(&objects_dir)
114 };
115
116 Ok(Self {
117 git_dir,
118 work_tree,
119 odb,
120 explicit_git_dir: false,
121 discovery_root: None,
122 work_tree_from_env: false,
123 discovery_via_gitfile: false,
124 })
125 }
126
127 pub fn open(git_dir: &Path, work_tree: Option<&Path>) -> Result<Self> {
134 let git_dir = git_dir
135 .canonicalize()
136 .map_err(|_| Error::NotARepository(git_dir.display().to_string()))?;
137
138 validate_repository_format(&git_dir)?;
139
140 Self::from_canonical_git_dir(git_dir, work_tree)
141 }
142
143 pub fn open_skipping_format_validation(
148 git_dir: &Path,
149 work_tree: Option<&Path>,
150 ) -> Result<Self> {
151 let git_dir = git_dir
152 .canonicalize()
153 .map_err(|_| Error::NotARepository(git_dir.display().to_string()))?;
154 Self::from_canonical_git_dir(git_dir, work_tree)
155 }
156
157 pub fn discover(start: Option<&Path>) -> Result<Self> {
166 if let Ok(dir) = env::var("GIT_DIR") {
168 let cwd = env::current_dir()?;
169 let mut git_dir = PathBuf::from(&dir);
170 if git_dir.is_relative() {
171 git_dir = cwd.join(git_dir);
172 }
173 git_dir = resolve_git_dir_env_path(&git_dir)?;
175 let work_tree = env::var("GIT_WORK_TREE").ok().map(|wt| {
176 let p = PathBuf::from(wt);
177 if p.is_absolute() {
178 p
179 } else {
180 cwd.join(p)
181 }
182 });
183 if let Some(ref wt_path) = work_tree {
184 if env::var("GIT_WORK_TREE")
185 .ok()
186 .is_some_and(|raw| Path::new(&raw).is_absolute())
187 {
188 validate_git_work_tree_path(wt_path)?;
189 }
190 }
191 if work_tree.is_some() {
192 let mut repo = Self::open(&git_dir, work_tree.as_deref())?;
193 repo.explicit_git_dir = true;
194 repo.discovery_root = None;
195 repo.work_tree_from_env = false;
196 repo.discovery_via_gitfile = false;
197 return Ok(repo);
198 }
199 let (is_bare, core_wt) = read_core_bare_and_worktree(&git_dir)?;
201 if is_bare && core_wt.is_some() {
202 warn_core_bare_worktree_conflict(&git_dir);
203 }
204 let resolved_wt = if is_bare {
205 None
206 } else if let Some(raw) = core_wt {
207 Some(resolve_core_worktree_path(&git_dir, &raw)?)
208 } else {
209 Some(cwd.canonicalize().unwrap_or_else(|_| cwd.clone()))
215 };
216 let mut repo = Self::open(&git_dir, resolved_wt.as_deref())?;
217 repo.explicit_git_dir = true;
218 repo.discovery_root = None;
219 repo.work_tree_from_env = false;
220 repo.discovery_via_gitfile = false;
221 return Ok(repo);
222 }
223
224 let cwd = env::current_dir()?;
225
226 let env_work_tree = env::var("GIT_WORK_TREE").ok().map(|wt| {
229 let p = PathBuf::from(wt);
230 if p.is_absolute() {
231 p
232 } else {
233 cwd.join(p)
234 }
235 });
236 if let Some(ref p) = env_work_tree {
237 if env::var("GIT_WORK_TREE")
238 .ok()
239 .is_some_and(|raw| Path::new(&raw).is_absolute())
240 {
241 validate_git_work_tree_path(p)?;
242 }
243 }
244 let start = start.unwrap_or(&cwd);
245 let start = if start.is_absolute() {
246 start.to_path_buf()
247 } else {
248 cwd.join(start)
249 };
250
251 let ceiling_dirs: Vec<String> = parse_ceiling_directories()
254 .into_iter()
255 .map(|p| path_for_ceiling_compare(&p))
256 .collect();
257
258 let start_canon = start.canonicalize().unwrap_or_else(|_| start.clone());
259 let mut dir_buf = path_for_ceiling_compare(&start_canon);
260 let min_offset = offset_1st_component(&dir_buf);
261 let mut ceil_offset: isize = longest_ancestor_length(&dir_buf, &ceiling_dirs)
262 .map(|n| n as isize)
263 .unwrap_or(-1);
264 if ceil_offset < 0 {
265 ceil_offset = min_offset as isize - 2;
266 }
267
268 loop {
269 let current = Path::new(&dir_buf);
270 if let Some(DiscoveredAt { mut repo, gitfile }) = try_open_at(current)? {
271 if let Some(ref wt) = env_work_tree {
272 repo.work_tree = Some(wt.canonicalize().unwrap_or_else(|_| wt.clone()));
273 repo.work_tree_from_env = true;
274 } else {
275 repo.work_tree_from_env = false;
276 let linked_gitfile =
281 repo.discovery_via_gitfile && resolve_common_dir(&repo.git_dir).is_some();
282 if !linked_gitfile {
283 let (is_bare, core_wt) = read_core_bare_and_worktree(&repo.git_dir)?;
284 if is_bare {
285 repo.work_tree = None;
286 } else if let Some(raw) = core_wt {
287 repo.work_tree = Some(resolve_core_worktree_path(&repo.git_dir, &raw)?);
288 }
289 }
290 }
291 let assume_different = env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
292 .ok()
293 .map(|v| {
294 let lower = v.to_ascii_lowercase();
295 v == "1" || lower == "true" || lower == "yes" || lower == "on"
296 })
297 .unwrap_or(false);
298 if assume_different {
299 repo.enforce_safe_directory()?;
300 } else {
301 #[cfg(unix)]
302 ensure_valid_ownership(
303 gitfile.as_deref(),
304 repo.work_tree.as_deref(),
305 &repo.git_dir,
306 )?;
307 }
308 return Ok(repo);
309 }
310
311 let mut offset: isize = dir_buf.len() as isize;
312 if offset <= min_offset as isize {
313 break;
314 }
315 loop {
316 offset -= 1;
317 if offset <= ceil_offset {
318 break;
319 }
320 if dir_buf
321 .as_bytes()
322 .get(offset as usize)
323 .is_some_and(|b| *b == b'/')
324 {
325 break;
326 }
327 }
328 if offset <= ceil_offset {
329 break;
330 }
331 let off_u = offset as usize;
332 let new_len = if off_u > min_offset {
333 off_u
334 } else {
335 min_offset
336 };
337 dir_buf.truncate(new_len);
338 }
339
340 Err(Error::NotARepository(start.display().to_string()))
341 }
342
343 #[must_use]
349 pub fn effective_pathspec_cwd(&self) -> PathBuf {
350 let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
351 let Some(wt) = self.work_tree.as_ref() else {
352 return cwd;
353 };
354 let inside_lexical = cwd.strip_prefix(wt).is_ok();
355 let inside_canon = cwd
356 .canonicalize()
357 .ok()
358 .zip(wt.canonicalize().ok())
359 .is_some_and(|(c, w)| c.starts_with(&w));
360 if inside_lexical || inside_canon {
361 cwd
362 } else {
363 wt.clone()
364 }
365 }
366
367 #[must_use]
369 pub fn index_path(&self) -> PathBuf {
370 self.git_dir.join("index")
371 }
372
373 pub fn load_index(&self) -> Result<Index> {
377 let path = self.index_path();
378 self.load_index_at(&path)
379 }
380
381 pub fn load_index_at(&self, path: &std::path::Path) -> Result<Index> {
384 Index::load_expand_sparse_optional(path, &self.odb)
385 }
386
387 pub fn write_index(&self, index: &mut Index) -> Result<()> {
390 self.write_index_at(&self.index_path(), index)
391 }
392
393 pub fn write_index_at(&self, path: &std::path::Path, index: &mut Index) -> Result<()> {
395 self.finalize_sparse_index_if_needed(index)?;
396 index.write(path)
397 }
398
399 fn finalize_sparse_index_if_needed(&self, index: &mut Index) -> Result<()> {
400 let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
401 let sparse_enabled = cfg
402 .get("core.sparseCheckout")
403 .map(|v| v == "true")
404 .unwrap_or(false);
405 if !sparse_enabled {
406 index.sparse_directories = false;
407 return Ok(());
408 }
409 let cone_cfg = cfg
410 .get("core.sparseCheckoutCone")
411 .and_then(|v| v.parse::<bool>().ok())
412 .unwrap_or(true);
413 let sparse_ix = cfg
414 .get("index.sparse")
415 .map(|v| v == "true")
416 .unwrap_or(false);
417 let patterns = read_sparse_checkout_patterns(&self.git_dir);
418 let cone = effective_cone_mode_for_sparse_file(cone_cfg, &patterns);
419 let head = resolve_head(&self.git_dir)?;
420 let tree_oid = if let Some(oid) = head.oid() {
421 let obj = self.odb.read(oid)?;
422 let commit = parse_commit(&obj.data)?;
423 Some(commit.tree)
424 } else {
425 None
426 };
427 if let Some(t) = tree_oid {
428 index.try_collapse_sparse_directories(&self.odb, &t, &patterns, cone, sparse_ix)?;
429 } else {
430 index.sparse_directories = false;
431 }
432 Ok(())
433 }
434
435 #[must_use]
437 pub fn refs_dir(&self) -> PathBuf {
438 self.git_dir.join("refs")
439 }
440
441 #[must_use]
443 pub fn head_path(&self) -> PathBuf {
444 self.git_dir.join("HEAD")
445 }
446
447 #[must_use]
449 pub fn is_bare(&self) -> bool {
450 let config_path = self.git_dir.join("config");
452 if let Ok(content) = std::fs::read_to_string(&config_path) {
453 let mut in_core = false;
454 for line in content.lines() {
455 let t = line.trim();
456 if t.starts_with('[') {
457 in_core = t.eq_ignore_ascii_case("[core]");
458 continue;
459 }
460 if in_core {
461 if let Some((k, v)) = t.split_once('=') {
462 if k.trim().eq_ignore_ascii_case("bare") {
463 if v.trim().eq_ignore_ascii_case("true") {
464 return true;
465 } else if v.trim().eq_ignore_ascii_case("false") {
466 return false;
467 }
468 }
469 }
470 }
471 }
472 }
473 if self.work_tree.is_some() {
474 return false;
475 }
476 let config_path = self.git_dir.join("config");
479 if let Ok(content) = std::fs::read_to_string(&config_path) {
480 let mut in_core = false;
481 for line in content.lines() {
482 let t = line.trim();
483 if t.starts_with('[') {
484 in_core = t.eq_ignore_ascii_case("[core]");
485 continue;
486 }
487 if in_core {
488 if let Some((k, v)) = t.split_once('=') {
489 if k.trim().eq_ignore_ascii_case("bare") {
490 return v.trim().eq_ignore_ascii_case("true");
491 }
492 }
493 }
494 }
495 }
496 true
498 }
499
500 pub fn read_replaced(&self, oid: &crate::objects::ObjectId) -> Result<crate::objects::Object> {
507 if std::env::var_os("GIT_NO_REPLACE_OBJECTS").is_some() {
508 return self.odb.read(oid);
509 }
510 let replace_base = std::env::var("GIT_REPLACE_REF_BASE")
511 .ok()
512 .filter(|s| !s.is_empty())
513 .unwrap_or_else(|| "refs/replace/".to_owned());
514 let replace_base = if replace_base.ends_with('/') {
515 replace_base
516 } else {
517 format!("{replace_base}/")
518 };
519 let replace_ref = self
520 .git_dir
521 .join(format!("{}{}", replace_base, oid.to_hex()));
522 if replace_ref.is_file() {
523 if let Ok(content) = std::fs::read_to_string(&replace_ref) {
524 let hex = content.trim();
525 if let Ok(replacement_oid) = hex.parse::<crate::objects::ObjectId>() {
526 if let Ok(obj) = self.odb.read(&replacement_oid) {
527 return Ok(obj);
528 }
529 }
530 }
531 }
532 self.odb.read(oid)
533 }
534}
535
536pub fn trace_repo_setup_if_requested(repo: &Repository) -> std::io::Result<()> {
541 let Ok(path) = env::var("GIT_TRACE_SETUP") else {
542 return Ok(());
543 };
544 if path.is_empty() || path == "0" {
545 return Ok(());
546 }
547 let trace_path = Path::new(&path);
548 if !trace_path.is_absolute() {
549 return Ok(());
550 }
551
552 let actual_cwd = env::current_dir()?;
553 let actual_cwd = actual_cwd
554 .canonicalize()
555 .unwrap_or_else(|_| actual_cwd.clone());
556
557 let (trace_cwd, prefix) = if let Some(ref wt) = repo.work_tree {
560 let wt_canon = wt.canonicalize().unwrap_or_else(|_| wt.clone());
561 if actual_cwd.starts_with(&wt_canon) {
562 let rel = actual_cwd
563 .strip_prefix(&wt_canon)
564 .map(|p| p.to_path_buf())
565 .unwrap_or_default();
566 let prefix = if rel.as_os_str().is_empty() {
567 "(null)".to_owned()
568 } else {
569 let mut s = rel.to_string_lossy().replace('\\', "/");
570 if !s.ends_with('/') {
571 s.push('/');
572 }
573 s
574 };
575 (wt_canon, prefix)
576 } else {
577 (actual_cwd.clone(), "(null)".to_owned())
578 }
579 } else {
580 (actual_cwd.clone(), "(null)".to_owned())
581 };
582
583 let git_dir_display =
584 display_git_dir_for_setup_trace(repo, &trace_cwd, &actual_cwd, prefix.as_str());
585 let common_display = display_common_dir_for_setup_trace(
586 repo,
587 &trace_cwd,
588 &actual_cwd,
589 prefix.as_str(),
590 &git_dir_display,
591 );
592 let worktree_display = repo
593 .work_tree
594 .as_ref()
595 .map(|p| {
596 p.canonicalize()
597 .unwrap_or_else(|_| lexical_normalize_path(p))
598 .display()
599 .to_string()
600 })
601 .unwrap_or_else(|| "(null)".to_owned());
602
603 let mut f = OpenOptions::new()
604 .create(true)
605 .append(true)
606 .open(trace_path)?;
607 writeln!(f, "setup: git_dir: {git_dir_display}")?;
608 writeln!(f, "setup: git_common_dir: {common_display}")?;
609 writeln!(f, "setup: worktree: {worktree_display}")?;
610 writeln!(f, "setup: cwd: {}", trace_cwd.display())?;
611 writeln!(f, "setup: prefix: {prefix}")?;
612 Ok(())
613}
614
615fn lexical_normalize_path(path: &Path) -> PathBuf {
617 let mut out = PathBuf::new();
618 let mut absolute = false;
619 for c in path.components() {
620 match c {
621 Component::Prefix(p) => {
622 out.push(p.as_os_str());
623 }
624 Component::RootDir => {
625 absolute = true;
626 out.push(c.as_os_str());
627 }
628 Component::CurDir => {}
629 Component::ParentDir => {
630 if absolute {
631 let _ = out.pop();
632 } else if !out.pop() {
633 out.push("..");
634 }
635 }
636 Component::Normal(s) => out.push(s),
637 }
638 }
639 if out.as_os_str().is_empty() {
640 PathBuf::from(".")
641 } else {
642 out
643 }
644}
645
646fn path_relative_to(target: &Path, base: &Path) -> Option<PathBuf> {
648 let t = target.canonicalize().ok()?;
649 let b = base.canonicalize().ok()?;
650 let tc: Vec<_> = t.components().collect();
651 let bc: Vec<_> = b.components().collect();
652 let mut i = 0usize;
653 while i < tc.len() && i < bc.len() && tc[i] == bc[i] {
654 i += 1;
655 }
656 let up = bc.len().saturating_sub(i);
657 let mut out = PathBuf::new();
658 for _ in 0..up {
659 out.push("..");
660 }
661 for comp in &tc[i..] {
662 out.push(comp.as_os_str());
663 }
664 Some(out)
665}
666
667fn rel_path_for_setup_trace(target: &Path, trace_cwd: &Path) -> String {
668 let t = target
669 .canonicalize()
670 .unwrap_or_else(|_| target.to_path_buf());
671 let tc = trace_cwd
672 .canonicalize()
673 .unwrap_or_else(|_| trace_cwd.to_path_buf());
674 if let Some(rel) = path_relative_to(&t, &tc) {
675 let s = rel.to_string_lossy().replace('\\', "/");
676 return if s.is_empty() || s == "." {
677 ".".to_owned()
678 } else {
679 s
680 };
681 }
682 t.display().to_string()
683}
684
685fn trace_cwd_strictly_inside_git_parent(trace_cwd: &Path, git_dir: &Path) -> bool {
686 let tc = trace_cwd
687 .canonicalize()
688 .unwrap_or_else(|_| trace_cwd.to_path_buf());
689 let gd = git_dir
690 .canonicalize()
691 .unwrap_or_else(|_| git_dir.to_path_buf());
692 let Some(parent) = gd.parent() else {
693 return false;
694 };
695 let parent = parent.to_path_buf();
696 if tc == parent {
697 return false;
698 }
699 tc.starts_with(&parent) && tc != parent
700}
701
702fn display_git_dir_for_setup_trace(
703 repo: &Repository,
704 trace_cwd: &Path,
705 actual_cwd: &Path,
706 setup_prefix: &str,
707) -> String {
708 let gd = repo
709 .git_dir
710 .canonicalize()
711 .unwrap_or_else(|_| repo.git_dir.clone());
712 let tc = trace_cwd
713 .canonicalize()
714 .unwrap_or_else(|_| trace_cwd.to_path_buf());
715 let ac = actual_cwd
716 .canonicalize()
717 .unwrap_or_else(|_| actual_cwd.to_path_buf());
718
719 if repo.work_tree.is_none() && !repo.explicit_git_dir {
722 if ac == gd {
723 return ".".to_owned();
724 }
725 if ac.starts_with(&gd) && ac != gd {
726 return gd.display().to_string();
727 }
728 }
729
730 if !repo.explicit_git_dir {
732 if let Some(wt) = &repo.work_tree {
733 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
734 if ac.starts_with(&gd) && ac != wt {
735 return gd.display().to_string();
736 }
737 }
738 }
739
740 if repo.explicit_git_dir {
744 if repo.work_tree.is_none() {
745 if let Ok(raw) = env::var("GIT_DIR") {
746 let p = Path::new(raw.trim());
747 if p.is_absolute() {
748 return gd.display().to_string();
749 }
750 let joined = ac.join(p);
751 if joined.is_file() {
752 return gd.display().to_string();
753 }
754 if let Some(rel) = path_relative_to(&gd, &tc) {
755 let s = rel.to_string_lossy().replace('\\', "/");
756 return if s.is_empty() || s == "." {
757 ".".to_owned()
758 } else {
759 s
760 };
761 }
762 }
763 return gd.display().to_string();
764 }
765 if let Some(wt) = &repo.work_tree {
766 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
767 let strictly_inside_wt = ac.starts_with(&wt) && ac != wt;
768 if strictly_inside_wt {
769 return gd.display().to_string();
770 }
771 if let Ok(raw) = env::var("GIT_DIR") {
772 let p = Path::new(raw.trim());
773 if p.is_relative() {
774 let joined = ac.join(p);
775 if joined.is_file() {
776 return gd.display().to_string();
778 }
779 if let Some(rel) = path_relative_to(&gd, &tc) {
780 let s = rel.to_string_lossy().replace('\\', "/");
781 return if s.is_empty() || s == "." {
782 ".".to_owned()
783 } else {
784 s
785 };
786 }
787 }
788 return gd.display().to_string();
789 }
790 }
791 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
792 return rel_path_for_setup_trace(&gd, trace_cwd);
793 }
794 return gd.display().to_string();
795 }
796
797 let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
798 (Some(root), Some(wt)) if !repo.work_tree_from_env => {
799 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
800 let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
801 r != w
802 }
803 _ => false,
804 };
805
806 if repo.work_tree_from_env {
807 if !repo.discovery_via_gitfile {
808 if setup_prefix == "(null)" {
809 if let (Some(root), Some(wt)) = (&repo.discovery_root, &repo.work_tree) {
810 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
811 let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
812 if r == w {
813 let dot_git = r.join(".git");
814 let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
815 if gd == dot_git {
816 return ".git".to_owned();
817 }
818 }
819 }
820 }
821 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
822 return rel_path_for_setup_trace(&gd, trace_cwd);
823 }
824 }
825 return gd.display().to_string();
826 }
827
828 if work_relocated {
829 if let Some(wt) = &repo.work_tree {
830 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
831 if ac == wt {
832 return gd.display().to_string();
833 }
834 let inside_wt = ac.starts_with(&wt) && ac != wt;
835 if inside_wt {
836 if let Some(rel) = path_relative_to(&gd, &ac) {
837 let s = rel.to_string_lossy().replace('\\', "/");
838 return if s.is_empty() || s == "." {
839 ".".to_owned()
840 } else {
841 s
842 };
843 }
844 }
845 }
846 }
847 if repo.work_tree.is_some() {
848 if let Some(root) = &repo.discovery_root {
849 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
850 let dot_git = r.join(".git");
851 let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
852 if gd == dot_git {
853 return ".git".to_owned();
854 }
855 } else if let Some(wt) = &repo.work_tree {
856 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
857 let dot_git = wt.join(".git");
858 let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
859 if gd == dot_git {
860 return ".git".to_owned();
861 }
862 }
863 }
864
865 if repo.discovery_via_gitfile && !repo.explicit_git_dir {
866 return gd.display().to_string();
867 }
868
869 if repo.work_tree.is_none() && !repo.explicit_git_dir {
873 if let Some(gp) = gd.parent() {
874 let gp = gp.canonicalize().unwrap_or_else(|_| gp.to_path_buf());
875 let gdc = gd.canonicalize().unwrap_or_else(|_| gd.clone());
876 if tc.starts_with(&gp) && tc != gp && !tc.starts_with(&gdc) {
877 return gdc.display().to_string();
878 }
879 if tc == gp {
880 return rel_path_for_setup_trace(&gd, trace_cwd);
881 }
882 }
883 }
884
885 if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
886 rel_path_for_setup_trace(&gd, trace_cwd)
887 } else {
888 gd.display().to_string()
889 }
890}
891
892fn display_common_dir_for_setup_trace(
893 repo: &Repository,
894 trace_cwd: &Path,
895 actual_cwd: &Path,
896 _setup_prefix: &str,
897 git_dir_display: &str,
898) -> String {
899 let gd = repo
900 .git_dir
901 .canonicalize()
902 .unwrap_or_else(|_| repo.git_dir.clone());
903 let Some(common) = resolve_common_dir(&gd) else {
904 return git_dir_display.to_owned();
905 };
906 let common = common.canonicalize().unwrap_or(common);
907 if common == gd {
908 return git_dir_display.to_owned();
909 }
910
911 let ac = actual_cwd
912 .canonicalize()
913 .unwrap_or_else(|_| actual_cwd.to_path_buf());
914 if repo.work_tree.is_none() && !repo.explicit_git_dir {
915 if ac == common {
916 return ".".to_owned();
917 }
918 if ac.starts_with(&common) && ac != common {
919 return common.display().to_string();
920 }
921 }
922
923 let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
924 (Some(root), Some(wt)) if !repo.work_tree_from_env => {
925 let r = root.canonicalize().unwrap_or_else(|_| root.clone());
926 let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
927 r != w
928 }
929 _ => false,
930 };
931 if work_relocated {
932 if let Some(wt) = &repo.work_tree {
933 let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
934 if ac == wt {
935 return common.display().to_string();
936 }
937 let inside_wt = ac.starts_with(&wt) && ac != wt;
938 if inside_wt {
939 if let Some(rel) = path_relative_to(&common, &ac) {
940 let s = rel.to_string_lossy().replace('\\', "/");
941 return if s.is_empty() || s == "." {
942 ".".to_owned()
943 } else {
944 s
945 };
946 }
947 }
948 }
949 }
950
951 if repo.discovery_via_gitfile && !repo.explicit_git_dir {
952 return common.display().to_string();
953 }
954
955 if repo.work_tree.is_none() && !repo.explicit_git_dir {
956 let tc = trace_cwd
957 .canonicalize()
958 .unwrap_or_else(|_| trace_cwd.to_path_buf());
959 if let Some(cp) = common.parent() {
960 let cp = cp.canonicalize().unwrap_or_else(|_| cp.to_path_buf());
961 let comc = common.canonicalize().unwrap_or_else(|_| common.clone());
962 if tc.starts_with(&cp) && tc != cp && !tc.starts_with(&comc) {
963 return comc.display().to_string();
964 }
965 if tc == cp {
966 return rel_path_for_setup_trace(&common, trace_cwd);
967 }
968 }
969 }
970
971 if trace_cwd_strictly_inside_git_parent(trace_cwd, &common) {
972 rel_path_for_setup_trace(&common, trace_cwd)
973 } else {
974 common.display().to_string()
975 }
976}
977
978fn resolve_common_dir(git_dir: &Path) -> Option<PathBuf> {
980 let common_raw = fs::read_to_string(git_dir.join("commondir")).ok()?;
981 let common_rel = common_raw.trim();
982 if common_rel.is_empty() {
983 return None;
984 }
985 let common_dir = if Path::new(common_rel).is_absolute() {
986 PathBuf::from(common_rel)
987 } else {
988 git_dir.join(common_rel)
989 };
990 Some(common_dir.canonicalize().unwrap_or(common_dir))
991}
992
993#[must_use]
995pub fn common_git_dir_for_config(git_dir: &Path) -> PathBuf {
996 resolve_common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf())
997}
998
999pub fn worktree_config_enabled(common_dir: &Path) -> bool {
1001 let path = common_dir.join("config");
1002 let Ok(content) = fs::read_to_string(&path) else {
1003 return false;
1004 };
1005 let mut in_extensions = false;
1006 for raw_line in content.lines() {
1007 let mut line = raw_line.trim();
1008 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
1009 continue;
1010 }
1011 if line.starts_with('[') {
1012 let Some(end_idx) = line.find(']') else {
1013 continue;
1014 };
1015 let section = line[1..end_idx].trim();
1016 let section_name = section
1017 .split_whitespace()
1018 .next()
1019 .unwrap_or_default()
1020 .to_ascii_lowercase();
1021 in_extensions = section_name == "extensions";
1022 let remainder = line[end_idx + 1..].trim();
1023 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
1024 continue;
1025 }
1026 line = remainder;
1027 }
1028 if in_extensions {
1029 let Some((key, value)) = line.split_once('=') else {
1030 continue;
1031 };
1032 if key.trim().eq_ignore_ascii_case("worktreeconfig") {
1033 let v = value.trim();
1034 return v.eq_ignore_ascii_case("true")
1035 || v.eq_ignore_ascii_case("yes")
1036 || v.eq_ignore_ascii_case("on")
1037 || v == "1";
1038 }
1039 }
1040 }
1041 false
1042}
1043
1044pub fn early_config_ignore_repo_reason(common_dir: &Path) -> Option<String> {
1048 const GIT_REPO_VERSION_READ: u32 = 1;
1049 let path = common_dir.join("config");
1050 let content = fs::read_to_string(&path).ok()?;
1051 let mut version = 0u32;
1052 let mut in_core = false;
1053 for raw_line in content.lines() {
1054 let mut line = raw_line.trim();
1055 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
1056 continue;
1057 }
1058 if line.starts_with('[') {
1059 let Some(end_idx) = line.find(']') else {
1060 continue;
1061 };
1062 let section = line[1..end_idx].trim();
1063 let section_name = section
1064 .split_whitespace()
1065 .next()
1066 .unwrap_or_default()
1067 .to_ascii_lowercase();
1068 in_core = section_name == "core";
1069 let remainder = line[end_idx + 1..].trim();
1070 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
1071 continue;
1072 }
1073 line = remainder;
1074 }
1075 if in_core {
1076 if let Some((key, value)) = line.split_once('=') {
1077 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
1078 if let Ok(v) = value.trim().parse::<u32>() {
1079 version = v;
1080 }
1081 }
1082 }
1083 }
1084 }
1085 if version > GIT_REPO_VERSION_READ {
1086 Some(format!(
1087 "Expected git repo version <= {GIT_REPO_VERSION_READ}, found {version}"
1088 ))
1089 } else {
1090 None
1091 }
1092}
1093
1094fn path_for_ceiling_compare(path: &Path) -> String {
1095 path.to_string_lossy().replace('\\', "/")
1096}
1097
1098fn offset_1st_component(path: &str) -> usize {
1099 if path.starts_with('/') {
1100 1
1101 } else {
1102 0
1103 }
1104}
1105
1106fn longest_ancestor_length(path: &str, ceilings: &[String]) -> Option<usize> {
1108 if path == "/" {
1109 return None;
1110 }
1111 let mut max_len: Option<usize> = None;
1112 for ceil in ceilings {
1113 let mut len = ceil.len();
1114 while len > 0 && ceil.as_bytes().get(len - 1) == Some(&b'/') {
1115 len -= 1;
1116 }
1117 if len == 0 {
1118 continue;
1119 }
1120 if path.len() <= len + 1 {
1121 continue;
1122 }
1123 if !path.starts_with(&ceil[..len]) {
1124 continue;
1125 }
1126 if path.as_bytes().get(len) != Some(&b'/') {
1127 continue;
1128 }
1129 if path.as_bytes().get(len + 1).is_none() {
1130 continue;
1131 }
1132 max_len = Some(max_len.map_or(len, |m| m.max(len)));
1133 }
1134 max_len
1135}
1136
1137fn repository_config_path(git_dir: &Path) -> Option<PathBuf> {
1139 let local = git_dir.join("config");
1140 if local.exists() {
1141 return Some(local);
1142 }
1143 let common = resolve_common_dir(git_dir)?;
1144 let shared = common.join("config");
1145 if shared.exists() {
1146 Some(shared)
1147 } else {
1148 None
1149 }
1150}
1151
1152pub fn validate_repo_format(git_dir: &Path) -> Result<()> {
1158 validate_repository_format(git_dir)
1159}
1160
1161fn validate_repository_format(git_dir: &Path) -> Result<()> {
1162 let Some(config_path) = repository_config_path(git_dir) else {
1163 return Ok(());
1164 };
1165
1166 let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
1167 let mut in_core = false;
1168 let mut in_extensions = false;
1169 let mut repo_version = 0u32;
1170 let mut extensions = BTreeSet::new();
1171
1172 for raw_line in content.lines() {
1173 let mut line = raw_line.trim();
1174 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
1175 continue;
1176 }
1177
1178 if line.starts_with('[') {
1179 let Some(end_idx) = line.find(']') else {
1180 return Err(Error::ConfigError(format!(
1181 "invalid config in {}",
1182 config_path.display()
1183 )));
1184 };
1185
1186 let section = line[1..end_idx].trim();
1187 let section_name = section
1188 .split_whitespace()
1189 .next()
1190 .unwrap_or_default()
1191 .to_ascii_lowercase();
1192 in_core = section_name == "core";
1193 in_extensions = section_name == "extensions";
1194
1195 let remainder = line[end_idx + 1..].trim();
1196 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
1197 continue;
1198 }
1199 line = remainder;
1200 }
1201
1202 if in_core {
1203 if let Some((key, value)) = line.split_once('=') {
1204 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
1205 if let Ok(v) = value.trim().parse::<u32>() {
1207 repo_version = v;
1208 }
1209 }
1210 }
1211 }
1212
1213 if in_extensions {
1214 let key = if let Some((key, _)) = line.split_once('=') {
1215 key.trim()
1216 } else {
1217 line
1218 };
1219 if !key.is_empty() {
1220 extensions.insert(key.to_ascii_lowercase());
1221 }
1222 }
1223 }
1224
1225 if repo_version > 1 {
1226 return Err(Error::UnsupportedRepositoryFormatVersion(repo_version));
1227 }
1228
1229 for extension in extensions {
1230 if repo_version == 0 {
1231 if extension.ends_with("-v1") {
1232 return Err(Error::UnsupportedRepositoryExtension(extension));
1233 }
1234 continue;
1235 }
1236
1237 if matches!(
1238 extension.as_str(),
1239 "noop"
1240 | "noop-v1"
1241 | "preciousobjects"
1242 | "partialclone"
1243 | "worktreeconfig"
1244 | "objectformat"
1245 | "compatobjectformat"
1246 | "refstorage"
1247 | "submodulepathconfig"
1248 ) {
1249 continue;
1250 }
1251
1252 return Err(Error::UnsupportedRepositoryExtension(extension));
1253 }
1254
1255 Ok(())
1256}
1257
1258struct DiscoveredAt {
1264 repo: Repository,
1265 gitfile: Option<PathBuf>,
1267}
1268
1269fn try_open_at(dir: &Path) -> Result<Option<DiscoveredAt>> {
1270 let dot_git = dir.join(".git");
1271
1272 #[cfg(unix)]
1275 {
1276 use std::os::unix::fs::FileTypeExt;
1277 if let Ok(meta) = fs::symlink_metadata(&dot_git) {
1278 let ft = meta.file_type();
1279 if ft.is_fifo() || ft.is_socket() || ft.is_block_device() || ft.is_char_device() {
1280 return Err(Error::NotARepository(format!(
1281 "invalid gitfile format: {} is not a regular file",
1282 dot_git.display()
1283 )));
1284 }
1285 if ft.is_symlink() {
1286 if let Ok(target_meta) = fs::metadata(&dot_git) {
1287 let tft = target_meta.file_type();
1288 if tft.is_fifo()
1289 || tft.is_socket()
1290 || tft.is_block_device()
1291 || tft.is_char_device()
1292 {
1293 return Err(Error::NotARepository(format!(
1294 "invalid gitfile format: {} is not a regular file",
1295 dot_git.display()
1296 )));
1297 }
1298 }
1299 }
1300 }
1301 }
1302
1303 if dot_git.is_file() {
1304 let content =
1306 fs::read_to_string(&dot_git).map_err(|e| Error::NotARepository(e.to_string()))?;
1307 let git_dir = parse_gitfile(&content, dir)?;
1308 let mut repo = Repository::open_skipping_format_validation(&git_dir, Some(dir))?;
1309 if resolve_common_dir(&git_dir).is_some() {
1313 let cwd = env::current_dir().map_err(Error::Io)?;
1314 if repo.work_tree.is_some() && !is_inside_work_tree(&repo, &cwd) {
1315 let root = if dir.is_absolute() {
1316 dir.to_path_buf()
1317 } else {
1318 cwd.join(dir)
1319 };
1320 repo.work_tree = Some(root.canonicalize().unwrap_or(root));
1321 }
1322 }
1323 let root = if dir.is_absolute() {
1324 dir.to_path_buf()
1325 } else {
1326 env::current_dir().map_err(Error::Io)?.join(dir)
1327 };
1328 repo.discovery_root = Some(root.canonicalize().unwrap_or(root));
1329 repo.discovery_via_gitfile = true;
1330 warn_core_bare_worktree_conflict(&git_dir);
1331 return Ok(Some(DiscoveredAt {
1332 repo,
1333 gitfile: Some(dot_git.clone()),
1334 }));
1335 }
1336
1337 if dot_git.is_dir() {
1338 let open_path = if dot_git.is_symlink() {
1342 dot_git.read_link().unwrap_or_else(|_| dot_git.clone())
1344 } else {
1345 dot_git.clone()
1346 };
1347 match Repository::open_skipping_format_validation(&open_path, Some(dir)) {
1350 Ok(mut repo) => {
1351 if dot_git.is_symlink() {
1354 let abs_dot_git = if dot_git.is_absolute() {
1355 dot_git
1356 } else {
1357 dir.join(".git")
1358 };
1359 repo.git_dir = abs_dot_git;
1360 }
1361 let root = if dir.is_absolute() {
1362 dir.to_path_buf()
1363 } else {
1364 env::current_dir().map_err(Error::Io)?.join(dir)
1365 };
1366 repo.discovery_root = Some(root.canonicalize().unwrap_or(root));
1367 repo.discovery_via_gitfile = false;
1368 return Ok(Some(DiscoveredAt {
1369 repo,
1370 gitfile: None,
1371 }));
1372 }
1373 Err(Error::NotARepository(_)) => return Ok(None),
1374 Err(e) => return Err(e),
1375 }
1376 }
1377
1378 if dir.join("HEAD").is_file() && dir.join("commondir").is_file() {
1381 maybe_trace_implicit_bare_repository(dir);
1382 let repo = Repository::open(dir, None)?;
1383 warn_core_bare_worktree_conflict(dir);
1384 return Ok(Some(DiscoveredAt {
1385 repo,
1386 gitfile: None,
1387 }));
1388 }
1389
1390 if dir.join("objects").is_dir() && dir.join("HEAD").is_file() {
1392 maybe_trace_implicit_bare_repository(dir);
1393 if !is_inside_dot_git(dir) {
1397 if let Ok(cfg) = crate::config::ConfigSet::load(None, true) {
1398 if let Some(val) = cfg.get("safe.bareRepository") {
1399 if val.eq_ignore_ascii_case("explicit") {
1400 return Err(Error::ForbiddenBareRepository(dir.display().to_string()));
1401 }
1402 }
1403 }
1404 }
1405 let repo = Repository::open(dir, None)?;
1406 warn_core_bare_worktree_conflict(dir);
1407 return Ok(Some(DiscoveredAt {
1408 repo,
1409 gitfile: None,
1410 }));
1411 }
1412
1413 Ok(None)
1414}
1415
1416fn is_inside_dot_git(path: &Path) -> bool {
1417 path.components().any(|c| c.as_os_str() == ".git")
1418}
1419
1420fn maybe_trace_implicit_bare_repository(dir: &Path) {
1421 let path = match std::env::var("GIT_TRACE2_PERF") {
1422 Ok(p) if !p.is_empty() => p,
1423 _ => return,
1424 };
1425
1426 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
1427 let _ = writeln!(file, "setup: implicit-bare-repository:{}", dir.display());
1428 }
1429}
1430
1431fn safe_directory_effective_values(git_dir: &Path) -> Vec<String> {
1434 let cfg = crate::config::ConfigSet::load(Some(git_dir), true)
1435 .unwrap_or_else(|_| crate::config::ConfigSet::new());
1436 let mut values: Vec<String> = Vec::new();
1437 for e in cfg.entries() {
1438 if e.key == "safe.directory"
1439 && e.scope != crate::config::ConfigScope::Local
1440 && e.scope != crate::config::ConfigScope::Worktree
1441 {
1442 values.push(e.value.clone().unwrap_or_else(|| "true".to_owned()));
1443 }
1444 }
1445 let mut effective: Vec<String> = Vec::new();
1446 for v in values {
1447 if v.is_empty() {
1448 effective.clear();
1449 } else {
1450 effective.push(v);
1451 }
1452 }
1453 effective
1454}
1455
1456fn ensure_safe_directory_allows(git_dir: &Path, checked: &Path) -> Result<()> {
1457 let effective = safe_directory_effective_values(git_dir);
1458 let checked_s = checked.to_string_lossy().to_string();
1459 if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
1460 eprintln!("debug-safe-directory values={:?}", effective);
1461 }
1462 if effective
1463 .iter()
1464 .any(|v| safe_directory_matches(v, &checked_s))
1465 {
1466 return Ok(());
1467 }
1468 Err(Error::DubiousOwnership(checked_s))
1469}
1470
1471#[cfg(unix)]
1472fn path_lstat_uid(path: &Path) -> std::io::Result<u32> {
1473 use std::os::unix::fs::MetadataExt;
1474 let meta = fs::symlink_metadata(path)?;
1475 Ok(meta.uid())
1476}
1477
1478#[cfg(unix)]
1479fn extract_uid_from_env(name: &str) -> Option<u32> {
1480 let raw = std::env::var(name).ok()?;
1481 if raw.is_empty() {
1482 return None;
1483 }
1484 raw.parse::<u32>().ok()
1485}
1486
1487#[cfg(unix)]
1490fn ensure_valid_ownership(
1491 gitfile: Option<&Path>,
1492 worktree: Option<&Path>,
1493 gitdir: &Path,
1494) -> Result<()> {
1495 const ROOT_UID: u32 = 0;
1496
1497 fn owned_by_effective_user(path: &Path) -> std::io::Result<bool> {
1498 let st_uid = path_lstat_uid(path)?;
1499 let mut euid = unsafe { libc::geteuid() };
1500 if euid == ROOT_UID {
1501 if st_uid == ROOT_UID {
1502 return Ok(true);
1503 }
1504 if let Some(sudo_uid) = extract_uid_from_env("SUDO_UID") {
1505 euid = sudo_uid;
1506 }
1507 }
1508 Ok(st_uid == euid)
1509 }
1510
1511 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
1512 .ok()
1513 .map(|v| {
1514 let lower = v.to_ascii_lowercase();
1515 v == "1" || lower == "true" || lower == "yes" || lower == "on"
1516 })
1517 .unwrap_or(false);
1518 if !assume_different {
1519 let gitfile_ok = gitfile
1520 .map(owned_by_effective_user)
1521 .transpose()?
1522 .unwrap_or(true);
1523 let wt_ok = worktree
1524 .map(owned_by_effective_user)
1525 .transpose()?
1526 .unwrap_or(true);
1527 let gd_ok = owned_by_effective_user(gitdir)?;
1528 if gitfile_ok && wt_ok && gd_ok {
1529 return Ok(());
1530 }
1531 }
1532
1533 let data_path = if let Some(wt) = worktree {
1534 wt.canonicalize().unwrap_or_else(|_| wt.to_path_buf())
1535 } else {
1536 gitdir
1537 .canonicalize()
1538 .unwrap_or_else(|_| gitdir.to_path_buf())
1539 };
1540 ensure_safe_directory_allows(gitdir, &data_path)
1541}
1542
1543#[cfg(not(unix))]
1544fn ensure_valid_ownership(
1545 _gitfile: Option<&Path>,
1546 _worktree: Option<&Path>,
1547 _gitdir: &Path,
1548) -> Result<()> {
1549 Ok(())
1550}
1551
1552impl Repository {
1553 pub fn enforce_safe_directory(&self) -> Result<()> {
1559 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
1560 .ok()
1561 .map(|v| {
1562 let lower = v.to_ascii_lowercase();
1563 v == "1" || lower == "true" || lower == "yes" || lower == "on"
1564 })
1565 .unwrap_or(false);
1566 if !assume_different {
1567 return Ok(());
1568 }
1569
1570 if self.explicit_git_dir {
1571 return Ok(());
1572 }
1573
1574 let checked = if let Some(wt) = &self.work_tree {
1578 let cwd = std::env::current_dir().ok();
1579 if let Some(cwd) = cwd {
1580 if cwd
1581 .canonicalize()
1582 .ok()
1583 .is_some_and(|c| c.starts_with(&self.git_dir))
1584 {
1585 self.git_dir
1586 .canonicalize()
1587 .unwrap_or_else(|_| self.git_dir.clone())
1588 } else {
1589 wt.canonicalize().unwrap_or_else(|_| wt.clone())
1590 }
1591 } else {
1592 wt.canonicalize().unwrap_or_else(|_| wt.clone())
1593 }
1594 } else {
1595 self.git_dir
1596 .canonicalize()
1597 .unwrap_or_else(|_| self.git_dir.clone())
1598 };
1599
1600 if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
1601 eprintln!(
1602 "debug-safe-directory checked={} git_dir={} work_tree={:?} cwd={:?}",
1603 checked.display(),
1604 self.git_dir.display(),
1605 self.work_tree,
1606 std::env::current_dir().ok()
1607 );
1608 }
1609 self.enforce_safe_directory_checked(&checked)
1610 }
1611
1612 pub fn enforce_safe_directory_git_dir(&self) -> Result<()> {
1617 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
1618 .ok()
1619 .map(|v| {
1620 let lower = v.to_ascii_lowercase();
1621 v == "1" || lower == "true" || lower == "yes" || lower == "on"
1622 })
1623 .unwrap_or(false);
1624 if !assume_different {
1625 return Ok(());
1626 }
1627 let checked = self
1628 .git_dir
1629 .canonicalize()
1630 .unwrap_or_else(|_| self.git_dir.clone());
1631 if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
1632 eprintln!(
1633 "debug-safe-directory(gitdir) checked={} git_dir={} work_tree={:?}",
1634 checked.display(),
1635 self.git_dir.display(),
1636 self.work_tree
1637 );
1638 }
1639 self.enforce_safe_directory_checked(&checked)
1640 }
1641
1642 pub fn enforce_safe_directory_git_dir_with_path(&self, checked: &Path) -> Result<()> {
1644 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
1645 .ok()
1646 .map(|v| {
1647 let lower = v.to_ascii_lowercase();
1648 v == "1" || lower == "true" || lower == "yes" || lower == "on"
1649 })
1650 .unwrap_or(false);
1651 if !assume_different {
1652 return Ok(());
1653 }
1654 self.enforce_safe_directory_checked(checked)
1655 }
1656
1657 fn enforce_safe_directory_checked(&self, checked: &Path) -> Result<()> {
1658 ensure_safe_directory_allows(&self.git_dir, checked)
1659 }
1660
1661 pub fn verify_safe_for_clone_source(&self) -> Result<()> {
1667 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
1668 .ok()
1669 .map(|v| {
1670 let lower = v.to_ascii_lowercase();
1671 v == "1" || lower == "true" || lower == "yes" || lower == "on"
1672 })
1673 .unwrap_or(false);
1674 if assume_different {
1675 self.enforce_safe_directory_git_dir()
1676 } else {
1677 #[cfg(unix)]
1678 {
1679 ensure_valid_ownership(None, None, &self.git_dir)
1680 }
1681 #[cfg(not(unix))]
1682 {
1683 Ok(())
1684 }
1685 }
1686 }
1687}
1688
1689fn normalize_fs_path(raw: &str) -> String {
1690 use std::path::Component;
1691 let p = std::path::Path::new(raw);
1692 let mut parts: Vec<String> = Vec::new();
1693 let mut absolute = false;
1694 for c in p.components() {
1695 match c {
1696 Component::RootDir => {
1697 absolute = true;
1698 parts.clear();
1699 }
1700 Component::CurDir => {}
1701 Component::ParentDir => {
1702 if !parts.is_empty() {
1703 parts.pop();
1704 }
1705 }
1706 Component::Normal(s) => parts.push(s.to_string_lossy().to_string()),
1707 Component::Prefix(_) => {}
1708 }
1709 }
1710 let mut out = if absolute {
1711 String::from("/")
1712 } else {
1713 String::new()
1714 };
1715 out.push_str(&parts.join("/"));
1716 out
1717}
1718
1719fn safe_directory_matches(config_value: &str, checked: &str) -> bool {
1720 if config_value == "*" {
1721 return true;
1722 }
1723 if config_value == "." {
1724 if let Ok(cwd) = std::env::current_dir() {
1726 let cwd_s = normalize_fs_path(&cwd.to_string_lossy());
1727 let checked_s = normalize_fs_path(checked);
1728 return cwd_s == checked_s;
1729 }
1730 return false;
1731 }
1732
1733 let canonicalize_or_normalize = |raw: &str| -> String {
1734 let p = std::path::Path::new(raw);
1735 if p.exists() {
1736 p.canonicalize()
1737 .map(|c| c.to_string_lossy().to_string())
1738 .map(|s| normalize_fs_path(&s))
1739 .unwrap_or_else(|_| normalize_fs_path(raw))
1740 } else {
1741 normalize_fs_path(raw)
1742 }
1743 };
1744
1745 let config_norm = canonicalize_or_normalize(config_value);
1746 let checked_norm = normalize_fs_path(checked);
1747
1748 if config_norm.ends_with("/*") {
1749 let prefix_raw = &config_norm[..config_norm.len() - 2];
1750 let prefix_norm = canonicalize_or_normalize(prefix_raw);
1751 let mut prefix = prefix_norm;
1752 if !prefix.ends_with('/') {
1753 prefix.push('/');
1754 }
1755 return checked_norm.starts_with(&prefix);
1756 }
1757
1758 config_norm == checked_norm
1759}
1760
1761fn warn_core_bare_worktree_conflict(git_dir: &Path) {
1762 if env::var("GIT_WORK_TREE")
1763 .ok()
1764 .filter(|s| !s.trim().is_empty())
1765 .is_some()
1766 {
1767 return;
1768 }
1769 static WARNED_DIRS: Mutex<Option<HashSet<String>>> = Mutex::new(None);
1770 if let Ok((bare, wt)) = read_core_bare_and_worktree(git_dir) {
1771 if bare && wt.is_some() {
1772 let key = git_dir
1773 .canonicalize()
1774 .unwrap_or_else(|_| git_dir.to_path_buf())
1775 .to_string_lossy()
1776 .to_string();
1777 let mut guard = WARNED_DIRS.lock().unwrap_or_else(|e| e.into_inner());
1778 let set = guard.get_or_insert_with(HashSet::new);
1779 if set.insert(key) {
1780 eprintln!("warning: core.bare and core.worktree do not make sense");
1781 }
1782 }
1783 }
1784}
1785
1786fn read_core_bare_and_worktree(git_dir: &Path) -> Result<(bool, Option<String>)> {
1787 let Some(config_path) = repository_config_path(git_dir) else {
1788 return Ok((false, None));
1789 };
1790 let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
1791 let mut in_core = false;
1792 let mut bare = false;
1793 let mut worktree: Option<String> = None;
1794 for raw_line in content.lines() {
1795 let line = raw_line.trim();
1796 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
1797 continue;
1798 }
1799 if line.starts_with('[') {
1800 in_core = line.eq_ignore_ascii_case("[core]");
1801 continue;
1802 }
1803 if !in_core {
1804 continue;
1805 }
1806 if let Some((k, v)) = line.split_once('=') {
1807 let key = k.trim();
1808 let val = v.trim();
1809 if key.eq_ignore_ascii_case("bare") {
1810 bare = val.eq_ignore_ascii_case("true");
1811 } else if key.eq_ignore_ascii_case("worktree") {
1812 worktree = Some(val.to_owned());
1813 }
1814 }
1815 }
1816 Ok((bare, worktree))
1817}
1818
1819fn validate_git_work_tree_path(path: &Path) -> Result<()> {
1822 if !path.is_absolute() {
1823 return Ok(());
1824 }
1825 let mut cur = PathBuf::new();
1826 for comp in path.components() {
1827 match comp {
1828 Component::Prefix(p) => cur.push(p.as_os_str()),
1829 Component::RootDir => cur.push(comp.as_os_str()),
1830 Component::CurDir => {}
1831 Component::ParentDir => {
1832 let _ = cur.pop();
1833 }
1834 Component::Normal(seg) => {
1835 cur.push(seg);
1836 if !cur.exists() {
1837 return Err(Error::PathError(format!(
1838 "Invalid path '{}': No such file or directory",
1839 cur.display()
1840 )));
1841 }
1842 }
1843 }
1844 }
1845 Ok(())
1846}
1847
1848fn resolve_core_worktree_path(git_dir: &Path, raw: &str) -> Result<PathBuf> {
1849 let p = Path::new(raw);
1850 if p.is_absolute() {
1851 return Ok(p.canonicalize().unwrap_or_else(|_| p.to_path_buf()));
1852 }
1853 let old = env::current_dir().map_err(Error::Io)?;
1854 env::set_current_dir(git_dir).map_err(Error::Io)?;
1855 env::set_current_dir(raw).map_err(Error::Io)?;
1856 let resolved = env::current_dir().map_err(Error::Io)?;
1857 env::set_current_dir(&old).map_err(Error::Io)?;
1858 Ok(resolved.canonicalize().unwrap_or(resolved))
1859}
1860
1861fn resolve_git_dir_env_path(git_dir: &Path) -> Result<PathBuf> {
1863 if git_dir.is_file() {
1864 let content =
1865 fs::read_to_string(git_dir).map_err(|e| Error::NotARepository(e.to_string()))?;
1866 let base = git_dir
1867 .parent()
1868 .ok_or_else(|| Error::NotARepository(git_dir.display().to_string()))?;
1869 return parse_gitfile(&content, base);
1870 }
1871 Ok(git_dir.to_path_buf())
1872}
1873
1874pub fn resolve_dot_git(dot_git: &Path) -> Result<PathBuf> {
1880 if dot_git.is_dir() {
1881 return dot_git
1882 .canonicalize()
1883 .map_err(|_| Error::NotARepository(dot_git.display().to_string()));
1884 }
1885 if dot_git.is_file() {
1886 let content =
1887 fs::read_to_string(dot_git).map_err(|e| Error::NotARepository(e.to_string()))?;
1888 let base = dot_git
1889 .parent()
1890 .ok_or_else(|| Error::NotARepository(dot_git.display().to_string()))?;
1891 return parse_gitfile(&content, base);
1892 }
1893 Err(Error::NotARepository(dot_git.display().to_string()))
1894}
1895
1896fn parse_gitfile(content: &str, base: &Path) -> Result<PathBuf> {
1898 for line in content.lines() {
1899 if let Some(rest) = line.strip_prefix("gitdir:") {
1900 let rel = rest.trim();
1901 let path = if Path::new(rel).is_absolute() {
1902 PathBuf::from(rel)
1903 } else {
1904 base.join(rel)
1905 };
1906 if !path.exists() {
1907 return Err(Error::NotARepository(path.display().to_string()));
1908 }
1909 return Ok(path);
1910 }
1911 }
1912 Err(Error::NotARepository("invalid gitfile format".to_owned()))
1913}
1914
1915fn write_fresh_git_directory(
1932 git_dir: &Path,
1933 bare: bool,
1934 initial_branch: &str,
1935 template_dir: Option<&Path>,
1936 ref_storage: &str,
1937) -> Result<()> {
1938 let mut subs = vec![
1939 "objects",
1940 "objects/info",
1941 "objects/pack",
1942 "refs",
1943 "refs/heads",
1944 "refs/tags",
1945 ];
1946 if !bare {
1947 subs.push("info");
1948 subs.push("hooks");
1949 }
1950 for sub in subs {
1951 fs::create_dir_all(git_dir.join(sub))?;
1952 }
1953
1954 if ref_storage == "reftable" {
1955 let reftable_dir = git_dir.join("reftable");
1956 fs::create_dir_all(&reftable_dir)?;
1957 let tables_list = reftable_dir.join("tables.list");
1958 if !tables_list.exists() {
1959 fs::write(&tables_list, "")?;
1960 }
1961 }
1962
1963 if let Some(tmpl) = template_dir {
1964 if tmpl.is_dir() {
1965 copy_template(tmpl, git_dir)?;
1966 }
1967 }
1968
1969 let head_content = format!("ref: refs/heads/{initial_branch}\n");
1970 fs::write(git_dir.join("HEAD"), head_content)?;
1971
1972 let needs_extensions = ref_storage == "reftable";
1973 let repo_version = if needs_extensions { 1 } else { 0 };
1974
1975 let mut config_content = String::from("[core]\n");
1976 config_content.push_str(&format!("\trepositoryformatversion = {repo_version}\n"));
1977 config_content.push_str("\tfilemode = true\n");
1978 if bare {
1979 config_content.push_str("\tbare = true\n");
1980 } else {
1981 config_content.push_str("\tbare = false\n");
1982 config_content.push_str("\tlogallrefupdates = true\n");
1983 }
1984 if needs_extensions {
1985 config_content.push_str("[extensions]\n");
1986 config_content.push_str("\trefStorage = reftable\n");
1987 }
1988 fs::write(git_dir.join("config"), config_content)?;
1989
1990 if let Some(tmpl) = template_dir {
1992 if tmpl.is_dir() {
1993 let tmpl_config = tmpl.join("config");
1994 if tmpl_config.is_file() {
1995 let tmpl_text = fs::read_to_string(&tmpl_config)?;
1996 let tmpl_parsed = ConfigFile::parse(&tmpl_config, &tmpl_text, ConfigScope::Local)?;
1997 let dest_path = git_dir.join("config");
1998 let dest_text = fs::read_to_string(&dest_path)?;
1999 let mut dest_parsed =
2000 ConfigFile::parse(&dest_path, &dest_text, ConfigScope::Local)?;
2001 for e in &tmpl_parsed.entries {
2002 if e.key == "core.bare" {
2004 continue;
2005 }
2006 if let Some(v) = &e.value {
2007 let _ = dest_parsed.set(&e.key, v);
2008 } else {
2009 let _ = dest_parsed.set(&e.key, "true");
2010 }
2011 }
2012 dest_parsed.write()?;
2013 }
2014 }
2015 }
2016
2017 fs::write(
2018 git_dir.join("description"),
2019 "Unnamed repository; edit this file 'description' to name the repository.\n",
2020 )?;
2021 Ok(())
2022}
2023
2024pub fn init_repository_separate_git_dir(
2033 work_tree: &Path,
2034 git_dir: &Path,
2035 initial_branch: &str,
2036 template_dir: Option<&Path>,
2037 ref_storage: &str,
2038) -> Result<Repository> {
2039 fs::create_dir_all(work_tree)?;
2040 fs::create_dir_all(git_dir)?;
2041 write_fresh_git_directory(git_dir, false, initial_branch, template_dir, ref_storage)?;
2042
2043 let git_dir_abs = git_dir
2044 .canonicalize()
2045 .unwrap_or_else(|_| git_dir.to_path_buf());
2046 let gitfile = work_tree.join(".git");
2047 fs::write(gitfile, format!("gitdir: {}\n", git_dir_abs.display()))?;
2048
2049 Repository::open(git_dir, Some(work_tree))
2050}
2051
2052pub fn init_bare_clone_minimal(
2066 git_dir: &Path,
2067 initial_branch: &str,
2068 ref_storage: &str,
2069) -> Result<()> {
2070 for sub in &[
2071 "objects",
2072 "objects/info",
2073 "objects/pack",
2074 "refs",
2075 "refs/heads",
2076 "refs/tags",
2077 ] {
2078 fs::create_dir_all(git_dir.join(sub))?;
2079 }
2080
2081 if ref_storage == "reftable" {
2082 let reftable_dir = git_dir.join("reftable");
2083 fs::create_dir_all(&reftable_dir)?;
2084 let tables_list = reftable_dir.join("tables.list");
2085 if !tables_list.exists() {
2086 fs::write(&tables_list, "")?;
2087 }
2088 }
2089
2090 let head_content = format!("ref: refs/heads/{initial_branch}\n");
2091 fs::write(git_dir.join("HEAD"), head_content)?;
2092
2093 let needs_extensions = ref_storage == "reftable";
2094 let repo_version = if needs_extensions { 1 } else { 0 };
2095 let mut config_content = String::from("[core]\n");
2096 config_content.push_str(&format!("\trepositoryformatversion = {repo_version}\n"));
2097 config_content.push_str("\tfilemode = true\n");
2098 config_content.push_str("\tbare = true\n");
2099 if needs_extensions {
2100 config_content.push_str("[extensions]\n");
2101 config_content.push_str("\trefStorage = reftable\n");
2102 }
2103 fs::write(git_dir.join("config"), config_content)?;
2104
2105 fs::write(
2106 git_dir.join("packed-refs"),
2107 "# pack-refs with: peeled fully-peeled sorted\n",
2108 )?;
2109 Ok(())
2110}
2111
2112pub fn init_repository(
2113 path: &Path,
2114 bare: bool,
2115 initial_branch: &str,
2116 template_dir: Option<&Path>,
2117 ref_storage: &str,
2118) -> Result<Repository> {
2119 let git_dir = if bare {
2120 path.to_path_buf()
2121 } else {
2122 path.join(".git")
2123 };
2124
2125 if !bare {
2126 fs::create_dir_all(path)?;
2127 }
2128 fs::create_dir_all(&git_dir)?;
2129 write_fresh_git_directory(&git_dir, bare, initial_branch, template_dir, ref_storage)?;
2130
2131 let work_tree = if bare { None } else { Some(path) };
2132 Repository::open(&git_dir, work_tree)
2133}
2134
2135pub fn init_repository_separate(
2140 work_tree: &Path,
2141 git_dir: &Path,
2142 initial_branch: &str,
2143 template_dir: Option<&Path>,
2144) -> Result<Repository> {
2145 fs::create_dir_all(work_tree)?;
2146 if git_dir.exists() {
2147 return Err(Error::PathError(format!(
2148 "git directory '{}' already exists",
2149 git_dir.display()
2150 )));
2151 }
2152
2153 for sub in &[
2154 "objects",
2155 "objects/info",
2156 "objects/pack",
2157 "refs",
2158 "refs/heads",
2159 "refs/tags",
2160 "info",
2161 "hooks",
2162 ] {
2163 fs::create_dir_all(git_dir.join(sub))?;
2164 }
2165
2166 if let Some(tmpl) = template_dir {
2167 if tmpl.is_dir() {
2168 copy_template(tmpl, git_dir)?;
2169 }
2170 }
2171
2172 fs::write(
2173 git_dir.join("HEAD"),
2174 format!("ref: refs/heads/{initial_branch}\n"),
2175 )?;
2176
2177 let work_tree_abs = fs::canonicalize(work_tree).unwrap_or_else(|_| work_tree.to_path_buf());
2178 let git_dir_abs = fs::canonicalize(git_dir).unwrap_or_else(|_| git_dir.to_path_buf());
2179 let config_content = format!(
2180 "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n\tlogallrefupdates = true\n\tworktree = {}\n",
2181 work_tree_abs.display()
2182 );
2183 fs::write(git_dir.join("config"), config_content)?;
2184 fs::write(
2185 git_dir.join("description"),
2186 "Unnamed repository; edit this file 'description' to name the repository.\n",
2187 )?;
2188
2189 let gitfile = work_tree.join(".git");
2190 fs::write(&gitfile, format!("gitdir: {}\n", git_dir_abs.display()))?;
2191
2192 Repository::open(git_dir, Some(work_tree))
2193}
2194
2195fn copy_template(src: &Path, dst: &Path) -> Result<()> {
2197 for entry in fs::read_dir(src)? {
2198 let entry = entry?;
2199 let src_path = entry.path();
2200 let dst_path = dst.join(entry.file_name());
2201 if src_path.is_dir() {
2202 fs::create_dir_all(&dst_path)?;
2203 copy_template(&src_path, &dst_path)?;
2204 } else {
2205 fs::copy(&src_path, &dst_path)?;
2206 }
2207 }
2208 Ok(())
2209}
2210
2211fn parse_ceiling_directories() -> Vec<PathBuf> {
2216 let raw = match env::var("GIT_CEILING_DIRECTORIES") {
2217 Ok(val) => val,
2218 Err(_) => return Vec::new(),
2219 };
2220 if raw.is_empty() {
2221 return Vec::new();
2222 }
2223 raw.split(':')
2224 .filter(|s| !s.is_empty())
2225 .filter_map(|s| {
2226 let p = PathBuf::from(s);
2227 if !p.is_absolute() {
2228 return None;
2229 }
2230 Some(p.canonicalize().unwrap_or_else(|_| {
2233 let s = s.trim_end_matches('/');
2235 PathBuf::from(s)
2236 }))
2237 })
2238 .collect()
2239}
2240
2241pub fn validate_repo_config(config_text: &str) -> std::result::Result<(), String> {
2244 let mut version: u32 = 0;
2245 let mut in_core = false;
2246 for line in config_text.lines() {
2247 let trimmed = line.trim();
2248 if trimmed.starts_with('[') {
2249 in_core = trimmed.to_lowercase().starts_with("[core");
2250 continue;
2251 }
2252 if in_core {
2253 if let Some(rest) = trimmed.strip_prefix("repositoryformatversion") {
2254 let val = rest.trim_start_matches([' ', '=']).trim();
2255 if let Ok(v) = val.parse::<u32>() {
2256 version = v;
2257 }
2258 }
2259 }
2260 }
2261 if version >= 2 {
2262 return Err(format!("unknown repository format version: {version}"));
2263 }
2264 Ok(())
2265}