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