1use std::collections::BTreeSet;
19use std::env;
20use std::fs;
21use std::fs::OpenOptions;
22use std::io::Write;
23use std::path::{Path, PathBuf};
24
25use crate::error::{Error, Result};
26use crate::odb::Odb;
27
28#[derive(Debug)]
30pub struct Repository {
31 pub git_dir: PathBuf,
33 pub work_tree: Option<PathBuf>,
35 pub odb: Odb,
37 pub explicit_git_dir: bool,
41}
42
43impl Repository {
44 pub fn open(git_dir: &Path, work_tree: Option<&Path>) -> Result<Self> {
51 let git_dir = git_dir
52 .canonicalize()
53 .map_err(|_| Error::NotARepository(git_dir.display().to_string()))?;
54
55 validate_repository_format(&git_dir)?;
56
57 let head_path = git_dir.join("HEAD");
59 if !head_path.exists() && !head_path.is_symlink() {
60 return Err(Error::NotARepository(git_dir.display().to_string()));
61 }
62
63 let objects_dir = if git_dir.join("objects").exists() {
66 git_dir.join("objects")
67 } else if let Some(common_dir) = resolve_common_dir(&git_dir) {
68 common_dir.join("objects")
69 } else {
70 return Err(Error::NotARepository(git_dir.display().to_string()));
71 };
72
73 if !objects_dir.exists() {
74 return Err(Error::NotARepository(git_dir.display().to_string()));
75 }
76
77 let work_tree = match work_tree {
78 Some(p) => Some(
79 p.canonicalize()
80 .map_err(|_| Error::PathError(p.display().to_string()))?,
81 ),
82 None => None,
83 };
84
85 let odb = if let Some(ref wt) = work_tree {
86 Odb::with_work_tree(&objects_dir, wt)
87 } else {
88 Odb::new(&objects_dir)
89 };
90
91 Ok(Self {
92 git_dir,
93 work_tree,
94 odb,
95 explicit_git_dir: false,
96 })
97 }
98
99 pub fn discover(start: Option<&Path>) -> Result<Self> {
108 if let Ok(dir) = env::var("GIT_DIR") {
110 let git_dir = PathBuf::from(&dir);
111 let work_tree = env::var("GIT_WORK_TREE").ok().map(PathBuf::from);
112 if work_tree.is_some() {
113 let mut repo = Self::open(&git_dir, work_tree.as_deref())?;
114 repo.explicit_git_dir = true;
115 return Ok(repo);
116 }
117 let mut repo = Self::open(&git_dir, None)?;
120 if repo.work_tree.is_none() {
121 let config_path = repo.git_dir.join("config");
123 let is_bare = if config_path.exists() {
124 fs::read_to_string(&config_path)
125 .ok()
126 .and_then(|c| {
127 c.lines()
128 .find(|l| {
129 let trimmed = l.trim();
130 trimmed.starts_with("bare") && trimmed.contains("true")
131 })
132 .map(|_| true)
133 })
134 .unwrap_or(false)
135 } else {
136 false
137 };
138 if !is_bare {
139 let cwd = env::current_dir()?;
140 repo.work_tree = Some(cwd.canonicalize().unwrap_or(cwd));
141 }
142 }
143 repo.explicit_git_dir = true;
144 return Ok(repo);
145 }
146
147 let env_work_tree = env::var("GIT_WORK_TREE").ok().map(PathBuf::from);
150
151 let cwd = env::current_dir()?;
152 let start = start.unwrap_or(&cwd);
153 let start = if start.is_absolute() {
154 start.to_path_buf()
155 } else {
156 cwd.join(start)
157 };
158
159 let ceiling_dirs = parse_ceiling_directories();
162
163 let mut current = start.as_path();
164 let mut first = true;
165 loop {
166 if !first && is_ceiling_blocked(current, &ceiling_dirs) {
170 break;
171 }
172 first = false;
173
174 if let Some(mut repo) = try_open_at(current)? {
175 if let Some(ref wt) = env_work_tree {
177 repo.work_tree = Some(wt.canonicalize().unwrap_or_else(|_| wt.clone()));
178 }
179 repo.enforce_safe_directory()?;
180 return Ok(repo);
181 }
182 match current.parent() {
183 Some(p) => current = p,
184 None => break,
185 }
186 }
187
188 Err(Error::NotARepository(start.display().to_string()))
189 }
190
191 #[must_use]
193 pub fn index_path(&self) -> PathBuf {
194 self.git_dir.join("index")
195 }
196
197 #[must_use]
199 pub fn refs_dir(&self) -> PathBuf {
200 self.git_dir.join("refs")
201 }
202
203 #[must_use]
205 pub fn head_path(&self) -> PathBuf {
206 self.git_dir.join("HEAD")
207 }
208
209 #[must_use]
211 pub fn is_bare(&self) -> bool {
212 let config_path = self.git_dir.join("config");
214 if let Ok(content) = std::fs::read_to_string(&config_path) {
215 let mut in_core = false;
216 for line in content.lines() {
217 let t = line.trim();
218 if t.starts_with('[') {
219 in_core = t.eq_ignore_ascii_case("[core]");
220 continue;
221 }
222 if in_core {
223 if let Some((k, v)) = t.split_once('=') {
224 if k.trim().eq_ignore_ascii_case("bare") {
225 if v.trim().eq_ignore_ascii_case("true") {
226 return true;
227 } else if v.trim().eq_ignore_ascii_case("false") {
228 return false;
229 }
230 }
231 }
232 }
233 }
234 }
235 if self.work_tree.is_some() {
236 return false;
237 }
238 let config_path = self.git_dir.join("config");
241 if let Ok(content) = std::fs::read_to_string(&config_path) {
242 let mut in_core = false;
243 for line in content.lines() {
244 let t = line.trim();
245 if t.starts_with('[') {
246 in_core = t.eq_ignore_ascii_case("[core]");
247 continue;
248 }
249 if in_core {
250 if let Some((k, v)) = t.split_once('=') {
251 if k.trim().eq_ignore_ascii_case("bare") {
252 return v.trim().eq_ignore_ascii_case("true");
253 }
254 }
255 }
256 }
257 }
258 true
260 }
261
262 pub fn read_replaced(&self, oid: &crate::objects::ObjectId) -> Result<crate::objects::Object> {
269 if std::env::var_os("GIT_NO_REPLACE_OBJECTS").is_some() {
270 return self.odb.read(oid);
271 }
272 let replace_base = std::env::var("GIT_REPLACE_REF_BASE")
273 .ok()
274 .filter(|s| !s.is_empty())
275 .unwrap_or_else(|| "refs/replace/".to_owned());
276 let replace_base = if replace_base.ends_with('/') {
277 replace_base
278 } else {
279 format!("{replace_base}/")
280 };
281 let replace_ref = self
282 .git_dir
283 .join(format!("{}{}", replace_base, oid.to_hex()));
284 if replace_ref.is_file() {
285 if let Ok(content) = std::fs::read_to_string(&replace_ref) {
286 let hex = content.trim();
287 if let Ok(replacement_oid) = hex.parse::<crate::objects::ObjectId>() {
288 if let Ok(obj) = self.odb.read(&replacement_oid) {
289 return Ok(obj);
290 }
291 }
292 }
293 }
294 self.odb.read(oid)
295 }
296}
297
298fn resolve_common_dir(git_dir: &Path) -> Option<PathBuf> {
300 let common_raw = fs::read_to_string(git_dir.join("commondir")).ok()?;
301 let common_rel = common_raw.trim();
302 if common_rel.is_empty() {
303 return None;
304 }
305 let common_dir = if Path::new(common_rel).is_absolute() {
306 PathBuf::from(common_rel)
307 } else {
308 git_dir.join(common_rel)
309 };
310 Some(common_dir.canonicalize().unwrap_or(common_dir))
311}
312
313fn repository_config_path(git_dir: &Path) -> Option<PathBuf> {
315 let local = git_dir.join("config");
316 if local.exists() {
317 return Some(local);
318 }
319 let common = resolve_common_dir(git_dir)?;
320 let shared = common.join("config");
321 if shared.exists() {
322 Some(shared)
323 } else {
324 None
325 }
326}
327
328pub fn validate_repo_format(git_dir: &Path) -> Result<()> {
334 validate_repository_format(git_dir)
335}
336
337fn validate_repository_format(git_dir: &Path) -> Result<()> {
338 let Some(config_path) = repository_config_path(git_dir) else {
339 return Ok(());
340 };
341
342 let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
343 let mut in_core = false;
344 let mut in_extensions = false;
345 let mut repo_version = 0u32;
346 let mut extensions = BTreeSet::new();
347
348 for raw_line in content.lines() {
349 let mut line = raw_line.trim();
350 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
351 continue;
352 }
353
354 if line.starts_with('[') {
355 let Some(end_idx) = line.find(']') else {
356 return Err(Error::ConfigError(format!(
357 "invalid config in {}",
358 config_path.display()
359 )));
360 };
361
362 let section = line[1..end_idx].trim();
363 let section_name = section
364 .split_whitespace()
365 .next()
366 .unwrap_or_default()
367 .to_ascii_lowercase();
368 in_core = section_name == "core";
369 in_extensions = section_name == "extensions";
370
371 let remainder = line[end_idx + 1..].trim();
372 if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
373 continue;
374 }
375 line = remainder;
376 }
377
378 if in_core {
379 if let Some((key, value)) = line.split_once('=') {
380 if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
381 repo_version = value.trim().parse::<u32>().map_err(|_| {
382 Error::ConfigError(format!(
383 "invalid core.repositoryformatversion in {}",
384 config_path.display()
385 ))
386 })?;
387 }
388 }
389 }
390
391 if in_extensions {
392 let key = if let Some((key, _)) = line.split_once('=') {
393 key.trim()
394 } else {
395 line
396 };
397 if !key.is_empty() {
398 extensions.insert(key.to_ascii_lowercase());
399 }
400 }
401 }
402
403 if repo_version > 1 {
404 return Err(Error::UnsupportedRepositoryFormatVersion(repo_version));
405 }
406
407 for extension in extensions {
408 if repo_version == 0 {
409 if extension.ends_with("-v1") {
410 return Err(Error::UnsupportedRepositoryExtension(extension));
411 }
412 continue;
413 }
414
415 if matches!(
416 extension.as_str(),
417 "noop"
418 | "noop-v1"
419 | "preciousobjects"
420 | "partialclone"
421 | "worktreeconfig"
422 | "objectformat"
423 | "compatobjectformat"
424 | "refstorage"
425 ) {
426 continue;
427 }
428
429 return Err(Error::UnsupportedRepositoryExtension(extension));
430 }
431
432 Ok(())
433}
434
435fn try_open_at(dir: &Path) -> Result<Option<Repository>> {
440 let dot_git = dir.join(".git");
441
442 #[cfg(unix)]
445 {
446 use std::os::unix::fs::FileTypeExt;
447 if let Ok(meta) = fs::symlink_metadata(&dot_git) {
448 let ft = meta.file_type();
449 if ft.is_fifo() || ft.is_socket() || ft.is_block_device() || ft.is_char_device() {
450 return Err(Error::NotARepository(format!(
451 "invalid gitfile format: {} is not a regular file",
452 dot_git.display()
453 )));
454 }
455 if ft.is_symlink() {
456 if let Ok(target_meta) = fs::metadata(&dot_git) {
457 let tft = target_meta.file_type();
458 if tft.is_fifo()
459 || tft.is_socket()
460 || tft.is_block_device()
461 || tft.is_char_device()
462 {
463 return Err(Error::NotARepository(format!(
464 "invalid gitfile format: {} is not a regular file",
465 dot_git.display()
466 )));
467 }
468 }
469 }
470 }
471 }
472
473 if dot_git.is_file() {
474 let content =
476 fs::read_to_string(&dot_git).map_err(|e| Error::NotARepository(e.to_string()))?;
477 let git_dir = parse_gitfile(&content, dir)?;
478 let repo = Repository::open(&git_dir, Some(dir))?;
479 return Ok(Some(repo));
480 }
481
482 if dot_git.is_dir() {
483 let open_path = if dot_git.is_symlink() {
487 dot_git.read_link().unwrap_or_else(|_| dot_git.clone())
489 } else {
490 dot_git.clone()
491 };
492 match Repository::open(&open_path, Some(dir)) {
495 Ok(mut repo) => {
496 if dot_git.is_symlink() {
499 let abs_dot_git = if dot_git.is_absolute() {
500 dot_git
501 } else {
502 dir.join(".git")
503 };
504 repo.git_dir = abs_dot_git;
505 }
506 return Ok(Some(repo));
507 }
508 Err(Error::NotARepository(_)) => return Ok(None),
509 Err(e) => return Err(e),
510 }
511 }
512
513 if dir.join("HEAD").is_file() && dir.join("commondir").is_file() {
516 maybe_trace_implicit_bare_repository(dir);
517 let repo = Repository::open(dir, None)?;
518 return Ok(Some(repo));
519 }
520
521 if dir.join("objects").is_dir() && dir.join("HEAD").is_file() {
523 maybe_trace_implicit_bare_repository(dir);
524 if !is_inside_dot_git(dir) {
528 if let Ok(cfg) = crate::config::ConfigSet::load(None, true) {
529 if let Some(val) = cfg.get("safe.bareRepository") {
530 if val.eq_ignore_ascii_case("explicit") {
531 return Err(Error::ForbiddenBareRepository(dir.display().to_string()));
532 }
533 }
534 }
535 }
536 let repo = Repository::open(dir, None)?;
537 return Ok(Some(repo));
538 }
539
540 Ok(None)
541}
542
543fn is_inside_dot_git(path: &Path) -> bool {
544 path.components().any(|c| c.as_os_str() == ".git")
545}
546
547fn maybe_trace_implicit_bare_repository(dir: &Path) {
548 let path = match std::env::var("GIT_TRACE2_PERF") {
549 Ok(p) if !p.is_empty() => p,
550 _ => return,
551 };
552
553 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
554 let _ = writeln!(file, "setup: implicit-bare-repository:{}", dir.display());
555 }
556}
557
558impl Repository {
559 pub fn enforce_safe_directory(&self) -> Result<()> {
565 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
566 .ok()
567 .map(|v| {
568 let lower = v.to_ascii_lowercase();
569 v == "1" || lower == "true" || lower == "yes" || lower == "on"
570 })
571 .unwrap_or(false);
572 if !assume_different {
573 return Ok(());
574 }
575
576 if self.explicit_git_dir {
577 return Ok(());
578 }
579
580 let checked = if let Some(wt) = &self.work_tree {
584 let cwd = std::env::current_dir().ok();
585 if let Some(cwd) = cwd {
586 if cwd
587 .canonicalize()
588 .ok()
589 .is_some_and(|c| c.starts_with(&self.git_dir))
590 {
591 self.git_dir
592 .canonicalize()
593 .unwrap_or_else(|_| self.git_dir.clone())
594 } else {
595 wt.canonicalize().unwrap_or_else(|_| wt.clone())
596 }
597 } else {
598 wt.canonicalize().unwrap_or_else(|_| wt.clone())
599 }
600 } else {
601 self.git_dir
602 .canonicalize()
603 .unwrap_or_else(|_| self.git_dir.clone())
604 };
605
606 if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
607 eprintln!(
608 "debug-safe-directory checked={} git_dir={} work_tree={:?} cwd={:?}",
609 checked.display(),
610 self.git_dir.display(),
611 self.work_tree,
612 std::env::current_dir().ok()
613 );
614 }
615 self.enforce_safe_directory_checked(&checked)
616 }
617
618 pub fn enforce_safe_directory_git_dir(&self) -> Result<()> {
623 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
624 .ok()
625 .map(|v| {
626 let lower = v.to_ascii_lowercase();
627 v == "1" || lower == "true" || lower == "yes" || lower == "on"
628 })
629 .unwrap_or(false);
630 if !assume_different {
631 return Ok(());
632 }
633 let checked = self
634 .git_dir
635 .canonicalize()
636 .unwrap_or_else(|_| self.git_dir.clone());
637 if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
638 eprintln!(
639 "debug-safe-directory(gitdir) checked={} git_dir={} work_tree={:?}",
640 checked.display(),
641 self.git_dir.display(),
642 self.work_tree
643 );
644 }
645 self.enforce_safe_directory_checked(&checked)
646 }
647
648 pub fn enforce_safe_directory_git_dir_with_path(&self, checked: &Path) -> Result<()> {
650 let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
651 .ok()
652 .map(|v| {
653 let lower = v.to_ascii_lowercase();
654 v == "1" || lower == "true" || lower == "yes" || lower == "on"
655 })
656 .unwrap_or(false);
657 if !assume_different {
658 return Ok(());
659 }
660 self.enforce_safe_directory_checked(checked)
661 }
662
663 fn enforce_safe_directory_checked(&self, checked: &Path) -> Result<()> {
664 let cfg = crate::config::ConfigSet::load(Some(&self.git_dir), true)
665 .unwrap_or_else(|_| crate::config::ConfigSet::new());
666 let mut values: Vec<String> = Vec::new();
667 for e in cfg.entries() {
668 if e.key == "safe.directory"
669 && e.scope != crate::config::ConfigScope::Local
670 && e.scope != crate::config::ConfigScope::Worktree
671 {
672 values.push(e.value.clone().unwrap_or_else(|| "true".to_owned()));
673 }
674 }
675
676 let mut effective: Vec<String> = Vec::new();
678 for v in values {
679 if v.is_empty() {
680 effective.clear();
681 } else {
682 effective.push(v);
683 }
684 }
685
686 let checked_s = checked.to_string_lossy().to_string();
687 if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
688 eprintln!("debug-safe-directory values={:?}", effective);
689 }
690 if effective
691 .iter()
692 .any(|v| safe_directory_matches(v, &checked_s))
693 {
694 return Ok(());
695 }
696
697 Err(Error::DubiousOwnership(checked_s))
698 }
699}
700
701fn normalize_fs_path(raw: &str) -> String {
702 use std::path::Component;
703 let p = std::path::Path::new(raw);
704 let mut parts: Vec<String> = Vec::new();
705 let mut absolute = false;
706 for c in p.components() {
707 match c {
708 Component::RootDir => {
709 absolute = true;
710 parts.clear();
711 }
712 Component::CurDir => {}
713 Component::ParentDir => {
714 if !parts.is_empty() {
715 parts.pop();
716 }
717 }
718 Component::Normal(s) => parts.push(s.to_string_lossy().to_string()),
719 Component::Prefix(_) => {}
720 }
721 }
722 let mut out = if absolute {
723 String::from("/")
724 } else {
725 String::new()
726 };
727 out.push_str(&parts.join("/"));
728 out
729}
730
731fn safe_directory_matches(config_value: &str, checked: &str) -> bool {
732 if config_value == "*" {
733 return true;
734 }
735 if config_value == "." {
736 if let Ok(cwd) = std::env::current_dir() {
738 let cwd_s = normalize_fs_path(&cwd.to_string_lossy());
739 let checked_s = normalize_fs_path(checked);
740 return cwd_s == checked_s;
741 }
742 return false;
743 }
744
745 let canonicalize_or_normalize = |raw: &str| -> String {
746 let p = std::path::Path::new(raw);
747 if p.exists() {
748 p.canonicalize()
749 .map(|c| c.to_string_lossy().to_string())
750 .map(|s| normalize_fs_path(&s))
751 .unwrap_or_else(|_| normalize_fs_path(raw))
752 } else {
753 normalize_fs_path(raw)
754 }
755 };
756
757 let config_norm = canonicalize_or_normalize(config_value);
758 let checked_norm = normalize_fs_path(checked);
759
760 if config_norm.ends_with("/*") {
761 let prefix_raw = &config_norm[..config_norm.len() - 2];
762 let prefix_norm = canonicalize_or_normalize(prefix_raw);
763 let mut prefix = prefix_norm;
764 if !prefix.ends_with('/') {
765 prefix.push('/');
766 }
767 return checked_norm.starts_with(&prefix);
768 }
769
770 config_norm == checked_norm
771}
772
773fn parse_gitfile(content: &str, base: &Path) -> Result<PathBuf> {
775 for line in content.lines() {
776 if let Some(rest) = line.strip_prefix("gitdir:") {
777 let rel = rest.trim();
778 let path = if Path::new(rel).is_absolute() {
779 PathBuf::from(rel)
780 } else {
781 base.join(rel)
782 };
783 if !path.exists() {
784 return Err(Error::NotARepository(path.display().to_string()));
785 }
786 return Ok(path);
787 }
788 }
789 Err(Error::NotARepository("invalid gitfile format".to_owned()))
790}
791
792pub fn init_repository(
809 path: &Path,
810 bare: bool,
811 initial_branch: &str,
812 template_dir: Option<&Path>,
813) -> Result<Repository> {
814 let git_dir = if bare {
815 path.to_path_buf()
816 } else {
817 path.join(".git")
818 };
819
820 for sub in &[
822 "objects",
823 "objects/info",
824 "objects/pack",
825 "refs",
826 "refs/heads",
827 "refs/tags",
828 "info",
829 "hooks",
830 ] {
831 fs::create_dir_all(git_dir.join(sub))?;
832 }
833
834 if let Some(tmpl) = template_dir {
836 if tmpl.is_dir() {
837 copy_template(tmpl, &git_dir)?;
838 }
839 }
840
841 let head_content = format!("ref: refs/heads/{initial_branch}\n");
843 fs::write(git_dir.join("HEAD"), head_content)?;
844
845 let config_content = if bare {
847 "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = true\n"
848 } else {
849 "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n\tlogallrefupdates = true\n"
850 };
851 fs::write(git_dir.join("config"), config_content)?;
852
853 fs::write(
855 git_dir.join("description"),
856 "Unnamed repository; edit this file 'description' to name the repository.\n",
857 )?;
858
859 let work_tree = if bare { None } else { Some(path) };
860 Repository::open(&git_dir, work_tree)
861}
862
863pub fn init_repository_separate(
868 work_tree: &Path,
869 git_dir: &Path,
870 initial_branch: &str,
871 template_dir: Option<&Path>,
872) -> Result<Repository> {
873 fs::create_dir_all(work_tree)?;
874 if git_dir.exists() {
875 return Err(Error::PathError(format!(
876 "git directory '{}' already exists",
877 git_dir.display()
878 )));
879 }
880
881 for sub in &[
882 "objects",
883 "objects/info",
884 "objects/pack",
885 "refs",
886 "refs/heads",
887 "refs/tags",
888 "info",
889 "hooks",
890 ] {
891 fs::create_dir_all(git_dir.join(sub))?;
892 }
893
894 if let Some(tmpl) = template_dir {
895 if tmpl.is_dir() {
896 copy_template(tmpl, git_dir)?;
897 }
898 }
899
900 fs::write(
901 git_dir.join("HEAD"),
902 format!("ref: refs/heads/{initial_branch}\n"),
903 )?;
904
905 let work_tree_abs = fs::canonicalize(work_tree).unwrap_or_else(|_| work_tree.to_path_buf());
906 let git_dir_abs = fs::canonicalize(git_dir).unwrap_or_else(|_| git_dir.to_path_buf());
907 let config_content = format!(
908 "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n\tlogallrefupdates = true\n\tworktree = {}\n",
909 work_tree_abs.display()
910 );
911 fs::write(git_dir.join("config"), config_content)?;
912 fs::write(
913 git_dir.join("description"),
914 "Unnamed repository; edit this file 'description' to name the repository.\n",
915 )?;
916
917 let gitfile = work_tree.join(".git");
918 fs::write(&gitfile, format!("gitdir: {}\n", git_dir_abs.display()))?;
919
920 Repository::open(git_dir, Some(work_tree))
921}
922
923fn copy_template(src: &Path, dst: &Path) -> Result<()> {
925 for entry in fs::read_dir(src)? {
926 let entry = entry?;
927 let src_path = entry.path();
928 let dst_path = dst.join(entry.file_name());
929 if src_path.is_dir() {
930 fs::create_dir_all(&dst_path)?;
931 copy_template(&src_path, &dst_path)?;
932 } else {
933 fs::copy(&src_path, &dst_path)?;
934 }
935 }
936 Ok(())
937}
938
939fn parse_ceiling_directories() -> Vec<PathBuf> {
944 let raw = match env::var("GIT_CEILING_DIRECTORIES") {
945 Ok(val) => val,
946 Err(_) => return Vec::new(),
947 };
948 if raw.is_empty() {
949 return Vec::new();
950 }
951 raw.split(':')
952 .filter(|s| !s.is_empty())
953 .filter_map(|s| {
954 let p = PathBuf::from(s);
955 if !p.is_absolute() {
956 return None;
957 }
958 Some(p.canonicalize().unwrap_or_else(|_| {
961 let s = s.trim_end_matches('/');
963 PathBuf::from(s)
964 }))
965 })
966 .collect()
967}
968
969fn is_ceiling_blocked(dir: &Path, ceilings: &[PathBuf]) -> bool {
979 if ceilings.is_empty() {
980 return false;
981 }
982 let canon = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
985 for ceil in ceilings {
986 if canon == *ceil {
991 return true;
992 }
993 }
994 false
995}
996
997pub fn validate_repo_config(config_text: &str) -> std::result::Result<(), String> {
1000 let mut version: u32 = 0;
1001 let mut in_core = false;
1002 for line in config_text.lines() {
1003 let trimmed = line.trim();
1004 if trimmed.starts_with('[') {
1005 in_core = trimmed.to_lowercase().starts_with("[core");
1006 continue;
1007 }
1008 if in_core {
1009 if let Some(rest) = trimmed.strip_prefix("repositoryformatversion") {
1010 let val = rest.trim_start_matches([' ', '=']).trim();
1011 if let Ok(v) = val.parse::<u32>() {
1012 version = v;
1013 }
1014 }
1015 }
1016 }
1017 if version >= 2 {
1018 return Err(format!("unknown repository format version: {version}"));
1019 }
1020 Ok(())
1021}