1use super::{
4 DEFAULT_SCAN_TIMEOUT, FsError, FsResult, GIT_DIRECTORY_POINTER_MAX_BYTES,
5 check_registered_worktree,
6};
7use projectatlas_core::{IndexCancellation, IndexWorkControl, IndexWorkStage};
8use std::collections::BTreeMap;
9use std::fs;
10use std::io::{self, Read};
11use std::path::{Path, PathBuf};
12
13#[derive(Clone, Debug, Eq, PartialEq)]
15pub enum RepositoryStructure {
16 NonGit {
18 selected_root: PathBuf,
20 },
21 Git(GitRepositoryStructure),
23 InvalidGit {
25 selected_root: PathBuf,
27 issue: GitStructureIssue,
29 },
30}
31
32#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct GitRepositoryStructure {
35 pub common_directory: PathBuf,
37 pub selection: GitRepositorySelection,
39 pub worktrees: Vec<GitWorktreeEntry>,
41}
42
43#[derive(Clone, Debug, Eq, PartialEq)]
45pub enum GitRepositorySelection {
46 Worktree {
48 root: PathBuf,
50 role: GitWorktreeRole,
52 administrative_directory: PathBuf,
54 },
55 CommonManager {
57 source_selection: GitManagerSourceSelection,
59 },
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
64pub enum GitManagerSourceSelection {
65 None,
67 Unambiguous {
69 root: PathBuf,
71 },
72 Ambiguous {
74 worktree_count: usize,
76 },
77}
78
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub enum GitWorktreeRole {
82 Primary,
84 Linked,
86}
87
88#[derive(Clone, Debug, Eq, PartialEq)]
90pub struct GitWorktreeEntry {
91 pub role: GitWorktreeRole,
93 pub administrative_directory: PathBuf,
99 pub state: GitWorktreeState,
101}
102
103#[derive(Clone, Debug, Eq, PartialEq)]
105pub enum GitWorktreeState {
106 Active {
108 root: PathBuf,
110 git_control_path: PathBuf,
112 },
113 Missing {
115 git_control_path: PathBuf,
117 },
118 Invalid {
120 issue: GitStructureIssue,
122 },
123}
124
125#[derive(Clone, Debug, Eq, PartialEq)]
127pub struct GitStructureIssue {
128 pub path: PathBuf,
130 pub kind: GitStructureIssueKind,
132}
133
134#[derive(Clone, Debug, Eq, PartialEq)]
136pub enum GitStructureIssueKind {
137 SymbolicLink,
139 UnsupportedPathType,
141 FilesystemUnavailable {
143 error_kind: io::ErrorKind,
145 },
146 PointerTooLarge {
148 limit_bytes: u64,
150 observed_bytes: u64,
152 },
153 PointerNotUtf8,
155 MalformedPointer,
157 MissingPointerTarget,
159 InvalidCommonDirectory,
161 UnsupportedSourceConfiguration,
163 RegistrationOutsideCommonDirectory,
165 MissingRegistrationPointer,
167 ReciprocalControlMismatch {
169 expected: PathBuf,
171 observed: PathBuf,
173 },
174 CommonDirectoryMismatch {
176 expected: PathBuf,
178 observed: PathBuf,
180 },
181}
182
183impl GitStructureIssue {
184 pub(super) fn into_io_error(self) -> (PathBuf, io::Error) {
186 let error_kind = match &self.kind {
187 GitStructureIssueKind::FilesystemUnavailable { error_kind } => *error_kind,
188 _ => io::ErrorKind::InvalidData,
189 };
190 let message = format!("{:?}", self.kind);
191 (self.path, io::Error::new(error_kind, message))
192 }
193}
194
195pub fn discover_repository_structure(path: &Path) -> FsResult<RepositoryStructure> {
206 let control = IndexWorkControl::new(IndexCancellation::new(), Some(DEFAULT_SCAN_TIMEOUT));
207 discover_repository_structure_controlled(path, &control)
208}
209
210pub fn discover_repository_structure_controlled(
218 path: &Path,
219 control: &IndexWorkControl,
220) -> FsResult<RepositoryStructure> {
221 control.check(IndexWorkStage::RepositoryTraversal)?;
222 if !path.is_dir() {
223 return Err(FsError::RootNotDirectory(path.to_path_buf()));
224 }
225 let selected_root = canonicalize(path, path)?;
226
227 for ancestor in selected_root.ancestors() {
228 control.check(IndexWorkStage::RepositoryTraversal)?;
229 let git_control_path = ancestor.join(".git");
230 match fs::symlink_metadata(&git_control_path) {
231 Ok(_) => {
232 return match inspect_worktree(ancestor) {
233 Ok(selected) => {
234 let selection_kind = if selected.role == GitWorktreeRole::Primary
236 && paths_equal(&selected.git_control_path, &selected.common_directory)
237 && (selected.common_directory_bare_setting == Some(true)
238 || !selected.source_root_selected_exactly)
239 {
240 GitRepositorySelectionKind::Manager
241 } else {
242 GitRepositorySelectionKind::Worktree
243 };
244 build_git_structure(selected, selection_kind, control)
245 .map(RepositoryStructure::Git)
246 }
247 Err(issue) => Ok(RepositoryStructure::InvalidGit {
248 selected_root: ancestor.to_path_buf(),
249 issue,
250 }),
251 };
252 }
253 Err(source) if source.kind() == io::ErrorKind::NotFound => {}
254 Err(source) => {
255 return Err(FsError::RepositoryBoundary {
256 path: git_control_path,
257 source,
258 });
259 }
260 }
261
262 if has_git_control_markers(ancestor)? {
263 return match inspect_common_directory(ancestor) {
264 Ok(common_directory) => build_git_structure(
265 SelectedWorktree {
266 root: ancestor.to_path_buf(),
267 git_control_path: common_directory.path.clone(),
268 administrative_directory: common_directory.path.clone(),
269 common_directory: common_directory.path,
270 common_directory_bare_setting: common_directory.bare_setting,
271 common_directory_source_root_inference_safe: common_directory
272 .source_root_inference_safe,
273 source_root_selected_exactly: false,
274 role: GitWorktreeRole::Primary,
275 },
276 GitRepositorySelectionKind::Manager,
277 control,
278 )
279 .map(RepositoryStructure::Git),
280 Err(issue) => Ok(RepositoryStructure::InvalidGit {
281 selected_root: ancestor.to_path_buf(),
282 issue,
283 }),
284 };
285 }
286 }
287
288 Ok(RepositoryStructure::NonGit { selected_root })
289}
290
291#[derive(Clone, Debug)]
293struct SelectedWorktree {
294 root: PathBuf,
296 git_control_path: PathBuf,
298 administrative_directory: PathBuf,
300 common_directory: PathBuf,
302 common_directory_bare_setting: Option<bool>,
304 common_directory_source_root_inference_safe: bool,
306 source_root_selected_exactly: bool,
308 role: GitWorktreeRole,
310}
311
312#[derive(Clone, Debug)]
314struct InspectedCommonDirectory {
315 path: PathBuf,
317 bare_setting: Option<bool>,
319 source_root_inference_safe: bool,
321}
322
323#[derive(Clone, Debug, Eq, PartialEq)]
325struct GitLocalConfigPolicy {
326 bare_setting: Option<bool>,
328 source_root_inference_safe: bool,
330 worktree_config_enabled: bool,
332 worktree_setting: Option<PathBuf>,
334 source_selection_policy_complete: bool,
336}
337
338#[derive(Clone, Copy, Debug, Eq, PartialEq)]
340enum GitRepositorySelectionKind {
341 Worktree,
343 Manager,
345}
346
347fn build_git_structure(
349 selected: SelectedWorktree,
350 selection_kind: GitRepositorySelectionKind,
351 control: &IndexWorkControl,
352) -> FsResult<GitRepositoryStructure> {
353 let common_directory = canonicalize(&selected.common_directory, &selected.common_directory)?;
354 let primary_root = primary_worktree_root(
355 &common_directory,
356 selected.common_directory_bare_setting,
357 selected.common_directory_source_root_inference_safe,
358 )?;
359 let primary_may_be_unlisted = primary_root.is_none()
360 && (selected.common_directory_bare_setting.is_none()
361 || !selected.common_directory_source_root_inference_safe)
362 && common_directory.file_name().and_then(|name| name.to_str()) == Some(".git");
363 let selected_primary = (selection_kind == GitRepositorySelectionKind::Worktree
364 && selected.role == GitWorktreeRole::Primary)
365 .then_some(selected.clone());
366 let worktrees = worktree_inventory(
367 &common_directory,
368 primary_root.as_deref(),
369 selected_primary.as_ref(),
370 control,
371 )?;
372
373 let selection = match selection_kind {
374 GitRepositorySelectionKind::Worktree => GitRepositorySelection::Worktree {
375 root: selected.root,
376 role: selected.role,
377 administrative_directory: selected.administrative_directory,
378 },
379 GitRepositorySelectionKind::Manager => GitRepositorySelection::CommonManager {
380 source_selection: manager_source_selection(&worktrees, primary_may_be_unlisted),
381 },
382 };
383
384 Ok(GitRepositoryStructure {
385 common_directory,
386 selection,
387 worktrees,
388 })
389}
390
391fn inspect_worktree(root: &Path) -> Result<SelectedWorktree, GitStructureIssue> {
393 let root = canonicalize_issue(root, root)?;
394 let git_control_path = root.join(".git");
395 let metadata = structural_metadata(&git_control_path)?;
396 if metadata.is_dir() {
397 let common_directory = inspect_common_directory(&git_control_path)?;
398 let source_root_selected_exactly = common_directory.bare_setting != Some(true)
399 && (common_directory.source_root_inference_safe
400 || validate_pointer_source_configuration(
401 &root,
402 &common_directory.path,
403 &common_directory.path,
404 false,
405 )
406 .is_ok());
407 return Ok(SelectedWorktree {
408 root,
409 git_control_path: common_directory.path.clone(),
410 administrative_directory: common_directory.path.clone(),
411 common_directory: common_directory.path,
412 common_directory_bare_setting: common_directory.bare_setting,
413 common_directory_source_root_inference_safe: common_directory
414 .source_root_inference_safe,
415 source_root_selected_exactly,
416 role: GitWorktreeRole::Primary,
417 });
418 }
419 if !metadata.is_file() {
420 return Err(issue(
421 git_control_path,
422 GitStructureIssueKind::UnsupportedPathType,
423 ));
424 }
425
426 let administrative_pointer = read_prefixed_pointer(&git_control_path, "gitdir:")?;
427 let administrative_directory =
428 resolve_existing_directory(&git_control_path, &root, &administrative_pointer)?;
429 let common_pointer_path = administrative_directory.join("commondir");
430 match fs::symlink_metadata(&common_pointer_path) {
431 Ok(_) => {
432 let common_pointer = read_plain_pointer(&common_pointer_path)?;
433 let common_directory_path = resolve_existing_directory(
434 &common_pointer_path,
435 &administrative_directory,
436 &common_pointer,
437 )?;
438 let common_directory = inspect_common_directory(&common_directory_path)?;
439 validate_linked_administrative_directory(
440 &administrative_directory,
441 &common_directory.path,
442 )?;
443 validate_reciprocal_control(
444 &administrative_directory,
445 &git_control_path,
446 &common_directory.path,
447 )?;
448 validate_pointer_source_configuration(
449 &root,
450 &common_directory.path,
451 &administrative_directory,
452 true,
453 )?;
454 Ok(SelectedWorktree {
455 root,
456 git_control_path: canonicalize_issue(&git_control_path, &git_control_path)?,
457 administrative_directory,
458 common_directory: common_directory.path,
459 common_directory_bare_setting: common_directory.bare_setting,
460 common_directory_source_root_inference_safe: common_directory
461 .source_root_inference_safe,
462 source_root_selected_exactly: true,
463 role: GitWorktreeRole::Linked,
464 })
465 }
466 Err(source) if source.kind() == io::ErrorKind::NotFound => {
467 let common_directory = inspect_common_directory(&administrative_directory)?;
468 validate_pointer_source_configuration(
469 &root,
470 &common_directory.path,
471 &administrative_directory,
472 false,
473 )?;
474 Ok(SelectedWorktree {
475 root,
476 git_control_path: canonicalize_issue(&git_control_path, &git_control_path)?,
477 administrative_directory,
478 common_directory: common_directory.path,
479 common_directory_bare_setting: common_directory.bare_setting,
480 common_directory_source_root_inference_safe: common_directory
481 .source_root_inference_safe,
482 source_root_selected_exactly: true,
483 role: GitWorktreeRole::Primary,
484 })
485 }
486 Err(_) => Err(issue(
487 common_pointer_path,
488 GitStructureIssueKind::UnsupportedPathType,
489 )),
490 }
491}
492
493fn inspect_common_directory(path: &Path) -> Result<InspectedCommonDirectory, GitStructureIssue> {
495 let common_directory = canonicalize_issue(path, path)?;
496 for (name, directory) in [("HEAD", false), ("objects", true), ("refs", true)] {
497 let marker = common_directory.join(name);
498 let metadata = structural_metadata(&marker)?;
499 if metadata.is_dir() != directory || metadata.is_file() == directory {
500 return Err(issue(
501 common_directory,
502 GitStructureIssueKind::InvalidCommonDirectory,
503 ));
504 }
505 }
506 let config = common_directory.join("config");
507 let mut config_policy = match fs::symlink_metadata(&config) {
508 Ok(_) => local_config_policy(&config)?,
509 Err(source) if source.kind() == io::ErrorKind::NotFound => GitLocalConfigPolicy {
510 bare_setting: None,
511 source_root_inference_safe: true,
512 worktree_config_enabled: false,
513 worktree_setting: None,
514 source_selection_policy_complete: true,
515 },
516 Err(source) => {
517 return Err(issue(
518 config,
519 GitStructureIssueKind::FilesystemUnavailable {
520 error_kind: source.kind(),
521 },
522 ));
523 }
524 };
525 if config_policy.worktree_config_enabled {
526 let worktree_config = common_directory.join("config.worktree");
527 match fs::symlink_metadata(&worktree_config) {
528 Ok(_) => {
529 let worktree_policy = local_config_policy(&worktree_config)?;
530 config_policy.bare_setting = if worktree_policy.source_root_inference_safe {
531 worktree_policy.bare_setting.or(config_policy.bare_setting)
532 } else {
533 None
534 };
535 config_policy.source_root_inference_safe &=
536 worktree_policy.source_root_inference_safe;
537 }
538 Err(source) if source.kind() == io::ErrorKind::NotFound => {}
539 Err(source) => {
540 return Err(issue(
541 worktree_config,
542 GitStructureIssueKind::FilesystemUnavailable {
543 error_kind: source.kind(),
544 },
545 ));
546 }
547 }
548 }
549 let registrations = common_directory.join("worktrees");
550 match fs::symlink_metadata(®istrations) {
551 Ok(_) => {
552 let metadata = structural_metadata(®istrations)?;
553 if !metadata.is_dir() {
554 return Err(issue(
555 registrations,
556 GitStructureIssueKind::UnsupportedPathType,
557 ));
558 }
559 }
560 Err(source) if source.kind() == io::ErrorKind::NotFound => {}
561 Err(source) => {
562 return Err(issue(
563 registrations,
564 GitStructureIssueKind::FilesystemUnavailable {
565 error_kind: source.kind(),
566 },
567 ));
568 }
569 }
570 Ok(InspectedCommonDirectory {
571 path: common_directory,
572 bare_setting: config_policy.bare_setting,
573 source_root_inference_safe: config_policy.source_root_inference_safe,
574 })
575}
576
577fn has_git_control_markers(path: &Path) -> FsResult<bool> {
579 let head = path.join("HEAD");
580 let objects = path.join("objects");
581 let refs = path.join("refs");
582 Ok(path_is_present(&head)? && path_is_present(&objects)? && path_is_present(&refs)?)
583}
584
585fn local_config_policy(path: &Path) -> Result<GitLocalConfigPolicy, GitStructureIssue> {
588 let text = read_bounded_text(path, GIT_DIRECTORY_POINTER_MAX_BYTES)?;
589 let text = text.strip_prefix('\u{feff}').unwrap_or(&text);
590 let mut in_core = false;
591 let mut in_extensions = false;
592 let mut has_include = false;
593 let mut bare_setting = None;
594 let mut source_root_inference_safe = true;
595 let mut worktree_config_enabled = false;
596 let mut worktree_setting = None;
597 let mut source_selection_policy_complete = true;
598 let mut repository_format_version = Some(0_u64);
599 let mut repository_extensions = BTreeMap::new();
600 for raw_line in text.lines() {
601 let mut line = trim_git_config_whitespace(raw_line);
602 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
603 continue;
604 }
605 if line.starts_with('[') {
606 let Some((section_name, has_subsection, remainder)) = git_config_section(line) else {
607 return Ok(GitLocalConfigPolicy {
608 bare_setting: None,
609 source_root_inference_safe: false,
610 worktree_config_enabled: false,
611 worktree_setting: None,
612 source_selection_policy_complete: false,
613 });
614 };
615 has_include |= section_name.eq_ignore_ascii_case("include")
616 || section_name.eq_ignore_ascii_case("includeif");
617 in_core = !has_subsection && section_name.eq_ignore_ascii_case("core");
618 in_extensions = !has_subsection && section_name.eq_ignore_ascii_case("extensions");
619 if remainder.is_empty() || remainder.starts_with(['#', ';']) {
620 continue;
621 }
622 line = remainder;
623 }
624 let (key, raw_value) = line
625 .split_once('=')
626 .map_or((line, "true"), |(key, value)| (key, value));
627 let key = trim_git_config_whitespace(key);
628 let mut key_bytes = key.bytes();
629 if !key_bytes
630 .next()
631 .is_some_and(|byte| byte.is_ascii_alphabetic())
632 || !key_bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
633 {
634 return Ok(GitLocalConfigPolicy {
635 bare_setting: None,
636 source_root_inference_safe: false,
637 worktree_config_enabled: false,
638 worktree_setting: None,
639 source_selection_policy_complete: false,
640 });
641 }
642 let Some(value) = git_config_value(raw_value) else {
643 return Ok(GitLocalConfigPolicy {
644 bare_setting: None,
645 source_root_inference_safe: false,
646 worktree_config_enabled: false,
647 worktree_setting: None,
648 source_selection_policy_complete: false,
649 });
650 };
651 if !in_core && !in_extensions {
652 continue;
653 }
654 if in_extensions {
655 repository_extensions.insert(key.to_ascii_lowercase(), value);
656 continue;
657 }
658 if !in_core {
659 continue;
660 }
661 if key.eq_ignore_ascii_case("repositoryformatversion") {
662 repository_format_version = value.parse::<u64>().ok().filter(|version| *version <= 1);
663 continue;
664 }
665 if key.eq_ignore_ascii_case("worktree") {
666 if value.contains('\\') {
667 source_root_inference_safe = false;
668 source_selection_policy_complete = false;
669 continue;
670 }
671 worktree_setting = (!value.is_empty()).then(|| PathBuf::from(value));
672 source_root_inference_safe = false;
673 source_selection_policy_complete &= worktree_setting.is_some();
674 continue;
675 }
676 if !key.eq_ignore_ascii_case("bare") {
677 continue;
678 }
679 if let Some(bare) = parse_git_boolean(&value) {
680 bare_setting = Some(bare);
681 } else {
682 bare_setting = None;
683 source_root_inference_safe = false;
684 source_selection_policy_complete = false;
685 }
686 }
687 let repository_format_version = if let Some(version) = repository_format_version {
688 version
689 } else {
690 source_root_inference_safe = false;
691 source_selection_policy_complete = false;
692 0
693 };
694 let mut has_format_one_extension = false;
695 let mut has_unsupported_repository_extension = false;
696 let mut object_format = None;
697 let mut compatibility_object_format = None;
698 for (key, value) in repository_extensions {
699 match key.as_str() {
700 "worktreeconfig" => {
701 if let Some(enabled) = parse_git_boolean(&value) {
702 worktree_config_enabled = enabled;
703 } else {
704 source_root_inference_safe = false;
705 worktree_config_enabled = false;
706 source_selection_policy_complete = false;
707 }
708 }
709 "noop" | "partialclone" => {}
710 "preciousobjects" => {
711 if parse_git_boolean(&value).is_none() {
712 source_root_inference_safe = false;
713 source_selection_policy_complete = false;
714 }
715 }
716 "compatobjectformat" => {
717 has_format_one_extension = true;
718 has_unsupported_repository_extension |=
719 !matches!(value.as_str(), "sha1" | "sha256");
720 compatibility_object_format = Some(value);
721 }
722 "noop-v1" => has_format_one_extension = true,
723 "objectformat" => {
724 has_format_one_extension = true;
725 has_unsupported_repository_extension |=
726 !matches!(value.as_str(), "sha1" | "sha256");
727 object_format = Some(value);
728 }
729 "refstorage" => {
730 has_format_one_extension = true;
731 let format = value
732 .split_once("://")
733 .map_or(value.as_str(), |(format, _)| format);
734 has_unsupported_repository_extension |= !matches!(format, "files" | "reftable");
735 }
736 "relativeworktrees" | "submodulepathconfig" => {
737 has_format_one_extension = true;
738 has_unsupported_repository_extension |= parse_git_boolean(&value).is_none();
739 }
740 _ => has_unsupported_repository_extension = true,
741 }
742 }
743 if compatibility_object_format
744 .as_deref()
745 .is_some_and(|compatibility| compatibility == object_format.as_deref().unwrap_or("sha1"))
746 {
747 has_unsupported_repository_extension = true;
748 }
749 if (repository_format_version == 1 && has_unsupported_repository_extension)
750 || (repository_format_version != 1 && has_format_one_extension)
751 {
752 source_root_inference_safe = false;
753 source_selection_policy_complete = false;
754 }
755 source_selection_policy_complete &= !has_include;
756 Ok(GitLocalConfigPolicy {
757 bare_setting: (!has_include).then_some(bare_setting).flatten(),
758 source_root_inference_safe: source_root_inference_safe && !has_include,
759 worktree_config_enabled,
760 worktree_setting,
761 source_selection_policy_complete,
762 })
763}
764
765fn trim_git_config_whitespace(value: &str) -> &str {
767 value.trim_matches([' ', '\t'])
768}
769
770fn git_config_section(line: &str) -> Option<(&str, bool, &str)> {
772 let mut quoted = false;
773 let mut escaped = false;
774 let mut header_end = None;
775 for (index, character) in line.char_indices() {
776 if escaped {
777 escaped = false;
778 continue;
779 }
780 match character {
781 '\\' if quoted => escaped = true,
782 '"' => quoted = !quoted,
783 ']' if !quoted => {
784 header_end = Some(index);
785 break;
786 }
787 _ => {}
788 }
789 }
790 if quoted || escaped {
791 return None;
792 }
793 let header_end = header_end?;
794 let section = trim_git_config_whitespace(&line[1..header_end]);
795 if section.is_empty() || section.contains(['[', ']']) {
796 return None;
797 }
798 let name_end = section
799 .find(|character: char| character.is_ascii_whitespace())
800 .unwrap_or(section.len());
801 let name = §ion[..name_end];
802 if !name
803 .bytes()
804 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.'))
805 {
806 return None;
807 }
808 let subsection = trim_git_config_whitespace(§ion[name_end..]);
809 let remainder = trim_git_config_whitespace(&line[header_end + 1..]);
810 if subsection.is_empty() {
811 return Some((name, false, remainder));
812 }
813 let subsection = subsection.strip_prefix('"')?.strip_suffix('"')?;
814 let mut escaped = false;
815 for character in subsection.chars() {
816 if escaped {
817 escaped = false;
818 } else if character == '\\' {
819 escaped = true;
820 } else if character == '"' {
821 return None;
822 }
823 }
824 (!escaped).then_some((name, true, remainder))
825}
826
827fn git_config_value(raw_value: &str) -> Option<String> {
832 let mut quoted = false;
833 let mut escaped = false;
834 let mut value_end = raw_value.len();
835 for (index, character) in raw_value.char_indices() {
836 if escaped {
837 if !matches!(character, '\\' | '"' | 'n' | 't' | 'b') {
838 return None;
839 }
840 escaped = false;
841 continue;
842 }
843 match character {
844 '\\' => escaped = true,
845 '"' => quoted = !quoted,
846 '#' | ';' if !quoted => {
847 value_end = index;
848 break;
849 }
850 _ => {}
851 }
852 }
853 if quoted || escaped {
854 return None;
855 }
856 let value = trim_git_config_whitespace(&raw_value[..value_end]);
857 let mut normalized = String::with_capacity(value.len());
858 let mut escaped = false;
859 for character in value.chars() {
860 if escaped {
861 normalized.push(character);
862 escaped = false;
863 } else if character == '\\' {
864 normalized.push(character);
865 escaped = true;
866 } else if character != '"' {
867 normalized.push(character);
868 }
869 }
870 Some(normalized)
871}
872
873fn parse_git_boolean(value: &str) -> Option<bool> {
875 match value.to_ascii_lowercase().as_str() {
876 "" | "false" | "no" | "off" | "0" => Some(false),
877 "true" | "yes" | "on" | "1" => Some(true),
878 _ => value.parse::<i64>().ok().map(|value| value != 0),
879 }
880}
881
882fn validate_pointer_source_configuration(
884 root: &Path,
885 common_directory: &Path,
886 administrative_directory: &Path,
887 common_manager_may_be_bare: bool,
888) -> Result<(), GitStructureIssue> {
889 let common_config = common_directory.join("config");
890 let common_policy = match fs::symlink_metadata(&common_config) {
891 Ok(_) => local_config_policy(&common_config)?,
892 Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(()),
893 Err(source) => {
894 return Err(issue(
895 common_config,
896 GitStructureIssueKind::FilesystemUnavailable {
897 error_kind: source.kind(),
898 },
899 ));
900 }
901 };
902 validate_pointer_config_policy(
903 &common_config,
904 administrative_directory,
905 root,
906 &common_policy,
907 common_manager_may_be_bare,
908 )?;
909 if !common_policy.worktree_config_enabled {
910 return Ok(());
911 }
912
913 let worktree_config = administrative_directory.join("config.worktree");
914 let worktree_policy = match fs::symlink_metadata(&worktree_config) {
915 Ok(_) => local_config_policy(&worktree_config)?,
916 Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(()),
917 Err(source) => {
918 return Err(issue(
919 worktree_config,
920 GitStructureIssueKind::FilesystemUnavailable {
921 error_kind: source.kind(),
922 },
923 ));
924 }
925 };
926 validate_pointer_config_policy(
927 &worktree_config,
928 administrative_directory,
929 root,
930 &worktree_policy,
931 false,
932 )
933}
934
935fn validate_pointer_config_policy(
937 config_path: &Path,
938 administrative_directory: &Path,
939 root: &Path,
940 policy: &GitLocalConfigPolicy,
941 bare_allowed: bool,
942) -> Result<(), GitStructureIssue> {
943 if !policy.source_selection_policy_complete
944 || !bare_allowed && policy.bare_setting == Some(true)
945 {
946 return Err(issue(
947 config_path.to_path_buf(),
948 GitStructureIssueKind::UnsupportedSourceConfiguration,
949 ));
950 }
951 let Some(setting) = &policy.worktree_setting else {
952 return Ok(());
953 };
954 let configured_root =
955 resolve_existing_directory(config_path, administrative_directory, setting)?;
956 if paths_equal(&configured_root, root) {
957 Ok(())
958 } else {
959 Err(issue(
960 config_path.to_path_buf(),
961 GitStructureIssueKind::UnsupportedSourceConfiguration,
962 ))
963 }
964}
965
966fn worktree_inventory(
968 common_directory: &Path,
969 primary_root: Option<&Path>,
970 selected_primary: Option<&SelectedWorktree>,
971 control: &IndexWorkControl,
972) -> FsResult<Vec<GitWorktreeEntry>> {
973 let mut worktrees = Vec::new();
974 if let Some(root) = primary_root {
975 worktrees.push(primary_entry(root, common_directory)?);
976 } else if let Some(selected) = selected_primary {
977 worktrees.push(active_entry(selected));
978 }
979
980 let registrations = common_directory.join("worktrees");
981 let entries = match fs::read_dir(®istrations) {
982 Ok(entries) => entries,
983 Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(worktrees),
984 Err(source) => {
985 return Err(FsError::RepositoryBoundary {
986 path: registrations,
987 source,
988 });
989 }
990 };
991 let mut registration_paths = Vec::new();
992 for (index, entry) in entries.enumerate() {
993 check_registered_worktree(control, index)?;
994 let entry = entry.map_err(|source| FsError::RepositoryBoundary {
995 path: registrations.clone(),
996 source,
997 })?;
998 registration_paths.push(entry.path());
999 }
1000 registration_paths.sort();
1001 for registration in registration_paths {
1002 control.check(IndexWorkStage::RepositoryTraversal)?;
1003 worktrees.push(inspect_registration(®istration, common_directory)?);
1004 }
1005 Ok(worktrees)
1006}
1007
1008fn primary_entry(root: &Path, common_directory: &Path) -> FsResult<GitWorktreeEntry> {
1010 let root = canonicalize(root, root)?;
1011 let git_control_path = canonicalize(&root.join(".git"), &root.join(".git"))?;
1012 Ok(GitWorktreeEntry {
1013 role: GitWorktreeRole::Primary,
1014 administrative_directory: common_directory.to_path_buf(),
1015 state: GitWorktreeState::Active {
1016 root,
1017 git_control_path,
1018 },
1019 })
1020}
1021
1022fn active_entry(selected: &SelectedWorktree) -> GitWorktreeEntry {
1024 GitWorktreeEntry {
1025 role: selected.role,
1026 administrative_directory: selected.administrative_directory.clone(),
1027 state: GitWorktreeState::Active {
1028 root: selected.root.clone(),
1029 git_control_path: selected.git_control_path.clone(),
1030 },
1031 }
1032}
1033
1034fn inspect_registration(
1036 registration: &Path,
1037 common_directory: &Path,
1038) -> FsResult<GitWorktreeEntry> {
1039 let administrative_directory = match structural_metadata(registration) {
1040 Ok(metadata) if metadata.is_dir() => canonicalize(registration, registration)?,
1041 Ok(_) => {
1042 return Ok(invalid_entry(
1043 registration.to_path_buf(),
1044 issue(
1045 registration.to_path_buf(),
1046 GitStructureIssueKind::UnsupportedPathType,
1047 ),
1048 ));
1049 }
1050 Err(issue) => return Ok(invalid_entry(registration.to_path_buf(), issue)),
1051 };
1052 let gitdir_path = administrative_directory.join("gitdir");
1053 let pointer = match fs::symlink_metadata(&gitdir_path) {
1054 Ok(_) => match read_plain_pointer(&gitdir_path) {
1055 Ok(pointer) => pointer,
1056 Err(issue) => return Ok(invalid_entry(administrative_directory, issue)),
1057 },
1058 Err(source) if source.kind() == io::ErrorKind::NotFound => {
1059 return Ok(invalid_entry(
1060 administrative_directory,
1061 issue(
1062 gitdir_path,
1063 GitStructureIssueKind::MissingRegistrationPointer,
1064 ),
1065 ));
1066 }
1067 Err(source) => {
1068 return Err(FsError::RepositoryBoundary {
1069 path: gitdir_path,
1070 source,
1071 });
1072 }
1073 };
1074 let git_control_path = resolve_pointer(&administrative_directory, &pointer);
1075 match fs::symlink_metadata(&git_control_path) {
1076 Err(source) if source.kind() == io::ErrorKind::NotFound => {
1077 return Ok(GitWorktreeEntry {
1078 role: GitWorktreeRole::Linked,
1079 administrative_directory,
1080 state: GitWorktreeState::Missing { git_control_path },
1081 });
1082 }
1083 Err(source) => {
1084 return Err(FsError::RepositoryBoundary {
1085 path: git_control_path,
1086 source,
1087 });
1088 }
1089 Ok(_) => {}
1090 }
1091 let git_control_path = match canonicalize_issue(&git_control_path, &git_control_path) {
1092 Ok(path) => path,
1093 Err(issue) => return Ok(invalid_entry(administrative_directory, issue)),
1094 };
1095 if git_control_path.file_name().and_then(|name| name.to_str()) != Some(".git") {
1096 return Ok(invalid_entry(
1097 administrative_directory,
1098 issue(git_control_path, GitStructureIssueKind::UnsupportedPathType),
1099 ));
1100 }
1101 let Some(root) = git_control_path.parent() else {
1102 return Ok(invalid_entry(
1103 administrative_directory,
1104 issue(git_control_path, GitStructureIssueKind::UnsupportedPathType),
1105 ));
1106 };
1107 let selected = match inspect_worktree(root) {
1108 Ok(selected) => selected,
1109 Err(issue) => return Ok(invalid_entry(administrative_directory, issue)),
1110 };
1111 if selected.role != GitWorktreeRole::Linked
1112 || !paths_equal(
1113 &selected.administrative_directory,
1114 &administrative_directory,
1115 )
1116 {
1117 return Ok(invalid_entry(
1118 administrative_directory.clone(),
1119 issue(
1120 git_control_path,
1121 GitStructureIssueKind::ReciprocalControlMismatch {
1122 expected: administrative_directory,
1123 observed: selected.administrative_directory,
1124 },
1125 ),
1126 ));
1127 }
1128 if !paths_equal(&selected.common_directory, common_directory) {
1129 return Ok(invalid_entry(
1130 administrative_directory,
1131 issue(
1132 git_control_path,
1133 GitStructureIssueKind::CommonDirectoryMismatch {
1134 expected: common_directory.to_path_buf(),
1135 observed: selected.common_directory,
1136 },
1137 ),
1138 ));
1139 }
1140 Ok(active_entry(&selected))
1141}
1142
1143fn invalid_entry(administrative_directory: PathBuf, issue: GitStructureIssue) -> GitWorktreeEntry {
1145 GitWorktreeEntry {
1146 role: GitWorktreeRole::Linked,
1147 administrative_directory,
1148 state: GitWorktreeState::Invalid { issue },
1149 }
1150}
1151
1152fn validate_linked_administrative_directory(
1154 administrative_directory: &Path,
1155 common_directory: &Path,
1156) -> Result<(), GitStructureIssue> {
1157 let registrations = common_directory.join("worktrees");
1158 let metadata = structural_metadata(®istrations)?;
1159 if !metadata.is_dir() {
1160 return Err(issue(
1161 registrations,
1162 GitStructureIssueKind::UnsupportedPathType,
1163 ));
1164 }
1165 if administrative_directory
1166 .parent()
1167 .is_some_and(|parent| paths_equal(parent, ®istrations))
1168 {
1169 Ok(())
1170 } else {
1171 Err(issue(
1172 administrative_directory.to_path_buf(),
1173 GitStructureIssueKind::RegistrationOutsideCommonDirectory,
1174 ))
1175 }
1176}
1177
1178fn validate_reciprocal_control(
1180 administrative_directory: &Path,
1181 expected_git_control_path: &Path,
1182 expected_common_directory: &Path,
1183) -> Result<(), GitStructureIssue> {
1184 let gitdir_path = administrative_directory.join("gitdir");
1185 let pointer = read_plain_pointer(&gitdir_path)?;
1186 let observed = resolve_existing_file(&gitdir_path, administrative_directory, &pointer)?;
1187 let expected = canonicalize_issue(expected_git_control_path, expected_git_control_path)?;
1188 if !paths_equal(&expected, &observed) {
1189 return Err(issue(
1190 gitdir_path,
1191 GitStructureIssueKind::ReciprocalControlMismatch { expected, observed },
1192 ));
1193 }
1194
1195 let common_pointer_path = administrative_directory.join("commondir");
1196 let common_pointer = read_plain_pointer(&common_pointer_path)?;
1197 let observed_common = resolve_existing_directory(
1198 &common_pointer_path,
1199 administrative_directory,
1200 &common_pointer,
1201 )?;
1202 let expected_common = canonicalize_issue(expected_common_directory, expected_common_directory)?;
1203 if paths_equal(&expected_common, &observed_common) {
1204 Ok(())
1205 } else {
1206 Err(issue(
1207 common_pointer_path,
1208 GitStructureIssueKind::CommonDirectoryMismatch {
1209 expected: expected_common,
1210 observed: observed_common,
1211 },
1212 ))
1213 }
1214}
1215
1216fn primary_worktree_root(
1218 common_directory: &Path,
1219 common_directory_bare_setting: Option<bool>,
1220 source_root_inference_safe: bool,
1221) -> FsResult<Option<PathBuf>> {
1222 if !source_root_inference_safe
1223 || common_directory_bare_setting != Some(false)
1224 || common_directory.file_name().and_then(|name| name.to_str()) != Some(".git")
1225 {
1226 return Ok(None);
1227 }
1228 let Some(parent) = common_directory.parent() else {
1229 return Ok(None);
1230 };
1231 let marker = parent.join(".git");
1232 let metadata = match fs::symlink_metadata(&marker) {
1233 Ok(metadata) => metadata,
1234 Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(None),
1235 Err(source) => {
1236 return Err(FsError::RepositoryBoundary {
1237 path: marker,
1238 source,
1239 });
1240 }
1241 };
1242 if metadata.file_type().is_symlink() || !metadata.is_dir() {
1243 return Ok(None);
1244 }
1245 let marker = canonicalize(&marker, &marker)?;
1246 if paths_equal(&marker, common_directory) {
1247 canonicalize(parent, parent).map(Some)
1248 } else {
1249 Ok(None)
1250 }
1251}
1252
1253fn manager_source_selection(
1255 worktrees: &[GitWorktreeEntry],
1256 primary_may_be_unlisted: bool,
1257) -> GitManagerSourceSelection {
1258 let mut count = 0_usize;
1259 let mut only_root = None;
1260 for entry in worktrees {
1261 if let GitWorktreeState::Active { root, .. } = &entry.state {
1262 count = count.saturating_add(1);
1263 if count == 1 {
1264 only_root = Some(root.clone());
1265 }
1266 }
1267 }
1268 if primary_may_be_unlisted && count == 1 {
1269 return GitManagerSourceSelection::Ambiguous { worktree_count: 2 };
1270 }
1271 match (count, only_root) {
1272 (0, _) => GitManagerSourceSelection::None,
1273 (1, Some(root)) => GitManagerSourceSelection::Unambiguous { root },
1274 (worktree_count, _) => GitManagerSourceSelection::Ambiguous { worktree_count },
1275 }
1276}
1277
1278pub fn git_administrative_identity(path: &Path) -> FsResult<String> {
1289 let metadata = fs::symlink_metadata(path).map_err(|source| FsError::RepositoryBoundary {
1290 path: path.to_path_buf(),
1291 source,
1292 })?;
1293 if metadata_is_indirect(&metadata) || !metadata.is_dir() {
1294 return Err(FsError::RepositoryBoundary {
1295 path: path.to_path_buf(),
1296 source: io::Error::new(
1297 io::ErrorKind::InvalidData,
1298 "Git administrative identity requires a direct directory",
1299 ),
1300 });
1301 }
1302
1303 let mut identity = blake3::Hasher::new();
1304 identity.update(b"projectatlas-git-administrative-identity-v1\0");
1305 #[cfg(unix)]
1306 {
1307 use std::os::unix::fs::MetadataExt;
1308
1309 identity.update(b"unix\0");
1310 identity.update(&metadata.dev().to_le_bytes());
1311 identity.update(&metadata.ino().to_le_bytes());
1312 identity.update(&required_creation_nanos(path, metadata.created())?.to_le_bytes());
1313 }
1314 #[cfg(windows)]
1315 {
1316 let windows = windows_file_identity::read(path)?;
1317 identity.update(b"windows\0");
1318 identity.update(&windows.creation_time.to_le_bytes());
1319 identity.update(&windows.volume_serial_number.to_le_bytes());
1320 identity.update(&windows.file_id);
1321 }
1322 #[cfg(not(any(unix, windows)))]
1323 {
1324 identity.update(b"portable\0");
1325 identity.update(&required_creation_nanos(path, metadata.created())?.to_le_bytes());
1326 }
1327 Ok(identity.finalize().to_hex().to_string())
1328}
1329
1330pub fn git_worktree_lifecycle_matches(
1337 root: &Path,
1338 common_directory: &Path,
1339 administrative_directory: &Path,
1340 administrative_identity: &str,
1341) -> FsResult<bool> {
1342 let Ok(common_directory) = canonicalize_issue(common_directory, common_directory) else {
1343 return Ok(false);
1344 };
1345 let Ok(administrative_directory) =
1346 canonicalize_issue(administrative_directory, administrative_directory)
1347 else {
1348 return Ok(false);
1349 };
1350 let Ok(selected) = inspect_worktree(root) else {
1351 return Ok(false);
1352 };
1353 if !selected.source_root_selected_exactly
1354 || !paths_equal(&selected.common_directory, &common_directory)
1355 || !paths_equal(
1356 &selected.administrative_directory,
1357 &administrative_directory,
1358 )
1359 {
1360 return Ok(false);
1361 }
1362 Ok(git_administrative_identity(&selected.administrative_directory)? == administrative_identity)
1363}
1364
1365#[cfg(not(windows))]
1367fn required_creation_nanos(
1368 path: &Path,
1369 created: io::Result<std::time::SystemTime>,
1370) -> FsResult<u128> {
1371 let created = created.map_err(|source| FsError::RepositoryBoundary {
1372 path: path.to_path_buf(),
1373 source,
1374 })?;
1375 created
1376 .duration_since(std::time::UNIX_EPOCH)
1377 .map(|duration| duration.as_nanos())
1378 .map_err(|source| FsError::RepositoryBoundary {
1379 path: path.to_path_buf(),
1380 source: io::Error::new(io::ErrorKind::InvalidData, source),
1381 })
1382}
1383
1384#[cfg(windows)]
1386#[expect(
1387 unsafe_code,
1388 reason = "the stable standard library does not expose Windows volume and 128-bit file identity; this bounded native query avoids a release dependency"
1389)]
1390mod windows_file_identity {
1391 use super::{FsError, FsResult, metadata_is_indirect};
1392 use std::ffi::c_void;
1393 use std::fs::OpenOptions;
1394 use std::io;
1395 use std::mem::size_of;
1396 use std::os::windows::fs::{MetadataExt, OpenOptionsExt};
1397 use std::os::windows::io::{AsRawHandle, RawHandle};
1398 use std::path::Path;
1399
1400 const FILE_SHARE_READ: u32 = 0x0000_0001;
1402 const FILE_SHARE_WRITE: u32 = 0x0000_0002;
1404 const FILE_SHARE_DELETE: u32 = 0x0000_0004;
1406 const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
1408 const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
1410 const FILE_ID_INFO_CLASS: i32 = 18;
1412
1413 #[repr(C)]
1415 #[derive(Default)]
1416 struct NativeFileIdInfo {
1417 volume_serial_number: u64,
1419 file_id: [u8; 16],
1421 }
1422
1423 #[link(name = "Kernel32")]
1424 unsafe extern "system" {
1425 fn GetFileInformationByHandleEx(
1426 file: RawHandle,
1427 information_class: i32,
1428 information: *mut c_void,
1429 information_bytes: u32,
1430 ) -> i32;
1431 }
1432
1433 pub(super) struct Identity {
1435 pub(super) creation_time: u64,
1437 pub(super) volume_serial_number: u64,
1439 pub(super) file_id: [u8; 16],
1441 }
1442
1443 pub(super) fn read(path: &Path) -> FsResult<Identity> {
1445 let directory = OpenOptions::new()
1446 .read(true)
1447 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
1448 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
1449 .open(path)
1450 .map_err(|source| FsError::RepositoryBoundary {
1451 path: path.to_path_buf(),
1452 source,
1453 })?;
1454 let metadata = directory
1455 .metadata()
1456 .map_err(|source| FsError::RepositoryBoundary {
1457 path: path.to_path_buf(),
1458 source,
1459 })?;
1460 if metadata_is_indirect(&metadata) || !metadata.is_dir() {
1461 return Err(FsError::RepositoryBoundary {
1462 path: path.to_path_buf(),
1463 source: io::Error::new(
1464 io::ErrorKind::InvalidData,
1465 "Git administrative identity requires a direct directory handle",
1466 ),
1467 });
1468 }
1469
1470 let mut native = NativeFileIdInfo::default();
1471 let information_bytes =
1472 u32::try_from(size_of::<NativeFileIdInfo>()).map_err(|_source| {
1473 FsError::RepositoryBoundary {
1474 path: path.to_path_buf(),
1475 source: io::Error::other("Windows file identity structure exceeds DWORD size"),
1476 }
1477 })?;
1478 let succeeded = unsafe {
1481 GetFileInformationByHandleEx(
1482 directory.as_raw_handle(),
1483 FILE_ID_INFO_CLASS,
1484 (&raw mut native).cast(),
1485 information_bytes,
1486 )
1487 };
1488 if succeeded == 0 {
1489 return Err(FsError::RepositoryBoundary {
1490 path: path.to_path_buf(),
1491 source: io::Error::last_os_error(),
1492 });
1493 }
1494 Ok(Identity {
1495 creation_time: metadata.creation_time(),
1496 volume_serial_number: native.volume_serial_number,
1497 file_id: native.file_id,
1498 })
1499 }
1500}
1501
1502pub(super) fn read_prefixed_pointer(
1504 path: &Path,
1505 prefix: &str,
1506) -> Result<PathBuf, GitStructureIssue> {
1507 let bytes = read_bounded_bytes(path, GIT_DIRECTORY_POINTER_MAX_BYTES)?;
1508 let value = single_pointer_line_bytes(path, &bytes)?;
1509 let Some(value) = value
1510 .strip_prefix(prefix.as_bytes())
1511 .and_then(|value| value.strip_prefix(b" "))
1512 else {
1513 return Err(issue(
1514 path.to_path_buf(),
1515 if std::str::from_utf8(value).is_err() {
1516 GitStructureIssueKind::PointerNotUtf8
1517 } else {
1518 GitStructureIssueKind::MalformedPointer
1519 },
1520 ));
1521 };
1522 path_value_bytes(path, value)
1523}
1524
1525pub(super) fn read_plain_pointer(path: &Path) -> Result<PathBuf, GitStructureIssue> {
1527 let bytes = read_bounded_bytes(path, GIT_DIRECTORY_POINTER_MAX_BYTES)?;
1528 path_value_bytes(path, single_pointer_line_bytes(path, &bytes)?)
1529}
1530
1531fn read_bounded_text(path: &Path, limit: u64) -> Result<String, GitStructureIssue> {
1533 let bytes = read_bounded_bytes(path, limit)?;
1534 String::from_utf8(bytes)
1535 .map_err(|_source| issue(path.to_path_buf(), GitStructureIssueKind::PointerNotUtf8))
1536}
1537
1538fn read_bounded_bytes(path: &Path, limit: u64) -> Result<Vec<u8>, GitStructureIssue> {
1540 let metadata = structural_metadata(path)?;
1541 if !metadata.is_file() {
1542 return Err(issue(
1543 path.to_path_buf(),
1544 GitStructureIssueKind::UnsupportedPathType,
1545 ));
1546 }
1547 if metadata.len() > limit {
1548 return Err(issue(
1549 path.to_path_buf(),
1550 GitStructureIssueKind::PointerTooLarge {
1551 limit_bytes: limit,
1552 observed_bytes: metadata.len(),
1553 },
1554 ));
1555 }
1556 let file = fs::File::open(path).map_err(|source| {
1557 issue(
1558 path.to_path_buf(),
1559 GitStructureIssueKind::FilesystemUnavailable {
1560 error_kind: source.kind(),
1561 },
1562 )
1563 })?;
1564 let mut bytes =
1565 Vec::with_capacity(usize::try_from(metadata.len().min(limit)).unwrap_or(usize::MAX));
1566 file.take(limit.saturating_add(1))
1567 .read_to_end(&mut bytes)
1568 .map_err(|source| {
1569 issue(
1570 path.to_path_buf(),
1571 GitStructureIssueKind::FilesystemUnavailable {
1572 error_kind: source.kind(),
1573 },
1574 )
1575 })?;
1576 if bytes.len() as u64 > limit {
1577 return Err(issue(
1578 path.to_path_buf(),
1579 GitStructureIssueKind::PointerTooLarge {
1580 limit_bytes: limit,
1581 observed_bytes: bytes.len() as u64,
1582 },
1583 ));
1584 }
1585 Ok(bytes)
1586}
1587
1588fn single_pointer_line_bytes<'a>(
1590 path: &Path,
1591 bytes: &'a [u8],
1592) -> Result<&'a [u8], GitStructureIssue> {
1593 let mut value = bytes;
1594 while let Some((b'\r' | b'\n', rest)) = value.split_last() {
1596 value = rest;
1597 }
1598 if value.is_empty() || value.contains(&b'\n') || value.contains(&0) {
1599 return Err(issue(
1600 path.to_path_buf(),
1601 GitStructureIssueKind::MalformedPointer,
1602 ));
1603 }
1604 Ok(value)
1605}
1606
1607fn path_value_bytes(path: &Path, value: &[u8]) -> Result<PathBuf, GitStructureIssue> {
1609 if value.is_empty() {
1610 return Err(issue(
1611 path.to_path_buf(),
1612 GitStructureIssueKind::MalformedPointer,
1613 ));
1614 }
1615
1616 #[cfg(unix)]
1617 {
1618 use std::ffi::OsString;
1619 use std::os::unix::ffi::OsStringExt;
1620 Ok(PathBuf::from(OsString::from_vec(value.to_vec())))
1621 }
1622 #[cfg(not(unix))]
1623 {
1624 String::from_utf8(value.to_vec())
1625 .map(PathBuf::from)
1626 .map_err(|_source| issue(path.to_path_buf(), GitStructureIssueKind::PointerNotUtf8))
1627 }
1628}
1629
1630fn resolve_pointer(base: &Path, pointer: &Path) -> PathBuf {
1632 if pointer.is_absolute() {
1633 pointer.to_path_buf()
1634 } else {
1635 base.join(pointer)
1636 }
1637}
1638
1639fn resolve_existing_directory(
1641 pointer_path: &Path,
1642 base: &Path,
1643 pointer: &Path,
1644) -> Result<PathBuf, GitStructureIssue> {
1645 let target = resolve_pointer(base, pointer);
1646 let metadata = structural_metadata(&target).map_err(|issue| match issue.kind {
1647 GitStructureIssueKind::MissingPointerTarget => issue,
1648 _ => GitStructureIssue {
1649 path: pointer_path.to_path_buf(),
1650 kind: issue.kind,
1651 },
1652 })?;
1653 if !metadata.is_dir() {
1654 return Err(issue(
1655 pointer_path.to_path_buf(),
1656 GitStructureIssueKind::UnsupportedPathType,
1657 ));
1658 }
1659 canonicalize_issue(&target, pointer_path)
1660}
1661
1662fn resolve_existing_file(
1664 pointer_path: &Path,
1665 base: &Path,
1666 pointer: &Path,
1667) -> Result<PathBuf, GitStructureIssue> {
1668 let target = resolve_pointer(base, pointer);
1669 let metadata = structural_metadata(&target).map_err(|issue| GitStructureIssue {
1670 path: pointer_path.to_path_buf(),
1671 kind: issue.kind,
1672 })?;
1673 if !metadata.is_file() {
1674 return Err(issue(
1675 pointer_path.to_path_buf(),
1676 GitStructureIssueKind::UnsupportedPathType,
1677 ));
1678 }
1679 canonicalize_issue(&target, pointer_path)
1680}
1681
1682fn structural_metadata(path: &Path) -> Result<fs::Metadata, GitStructureIssue> {
1684 match fs::symlink_metadata(path) {
1685 Ok(metadata) if metadata_is_indirect(&metadata) => Err(issue(
1686 path.to_path_buf(),
1687 GitStructureIssueKind::SymbolicLink,
1688 )),
1689 Ok(metadata) => Ok(metadata),
1690 Err(source) if source.kind() == io::ErrorKind::NotFound => Err(issue(
1691 path.to_path_buf(),
1692 GitStructureIssueKind::MissingPointerTarget,
1693 )),
1694 Err(source) => Err(issue(
1695 path.to_path_buf(),
1696 GitStructureIssueKind::FilesystemUnavailable {
1697 error_kind: source.kind(),
1698 },
1699 )),
1700 }
1701}
1702
1703fn metadata_is_indirect(metadata: &fs::Metadata) -> bool {
1705 if metadata.file_type().is_symlink() {
1706 return true;
1707 }
1708 #[cfg(windows)]
1709 {
1710 use std::os::windows::fs::MetadataExt;
1711
1712 const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0400;
1713 metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
1714 }
1715 #[cfg(not(windows))]
1716 {
1717 false
1718 }
1719}
1720
1721fn path_is_present(path: &Path) -> FsResult<bool> {
1723 match fs::symlink_metadata(path) {
1724 Ok(_) => Ok(true),
1725 Err(source) if source.kind() == io::ErrorKind::NotFound => Ok(false),
1726 Err(source) => Err(FsError::RepositoryBoundary {
1727 path: path.to_path_buf(),
1728 source,
1729 }),
1730 }
1731}
1732
1733fn canonicalize(path: &Path, evidence_path: &Path) -> FsResult<PathBuf> {
1735 path.canonicalize()
1736 .map_err(|source| FsError::RepositoryBoundary {
1737 path: evidence_path.to_path_buf(),
1738 source,
1739 })
1740}
1741
1742fn canonicalize_issue(path: &Path, evidence_path: &Path) -> Result<PathBuf, GitStructureIssue> {
1744 path.canonicalize().map_err(|source| {
1745 issue(
1746 evidence_path.to_path_buf(),
1747 if source.kind() == io::ErrorKind::NotFound {
1748 GitStructureIssueKind::MissingPointerTarget
1749 } else {
1750 GitStructureIssueKind::UnsupportedPathType
1751 },
1752 )
1753 })
1754}
1755
1756fn paths_equal(left: &Path, right: &Path) -> bool {
1758 #[cfg(windows)]
1759 {
1760 left == right
1761 || left
1762 .to_str()
1763 .zip(right.to_str())
1764 .is_some_and(|(left, right)| left.eq_ignore_ascii_case(right))
1765 }
1766 #[cfg(not(windows))]
1767 {
1768 left == right
1769 }
1770}
1771
1772fn issue(path: PathBuf, kind: GitStructureIssueKind) -> GitStructureIssue {
1774 GitStructureIssue { path, kind }
1775}
1776
1777#[cfg(test)]
1778mod tests {
1779 use super::*;
1780 use projectatlas_core::{IndexWorkFailure, IndexWorkResource};
1781 use std::error::Error;
1782 use std::ffi::OsStr;
1783 use std::process::Command;
1784
1785 #[test]
1786 fn structural_discovery_covers_real_git_worktree_lifecycle_matrix() -> Result<(), Box<dyn Error>>
1787 {
1788 let temp = tempfile::tempdir()?;
1789 let primary = temp.path().join("primary checkout");
1790 fs::create_dir(&primary)?;
1791 run_git(&primary, ["init"])?;
1792 run_git(&primary, ["config", "user.name", "ProjectAtlas Test"])?;
1793 run_git(
1794 &primary,
1795 ["config", "user.email", "projectatlas@example.invalid"],
1796 )?;
1797 fs::create_dir(primary.join("src"))?;
1798 fs::write(primary.join("src").join("main.rs"), "fn main() {}\n")?;
1799 run_git(&primary, ["add", "."])?;
1800 run_git(&primary, ["commit", "-m", "fixture"])?;
1801
1802 let sha256 = temp.path().join("sha256 checkout");
1803 run_command(
1804 Command::new("git")
1805 .args(["init", "--object-format=sha256"])
1806 .arg(&sha256),
1807 )?;
1808 let sha256_root = Command::new("git")
1809 .current_dir(&sha256)
1810 .args(["rev-parse", "--show-toplevel"])
1811 .output()?;
1812 require(
1813 sha256_root.status.success()
1814 && Path::new(String::from_utf8(sha256_root.stdout)?.trim()).canonicalize()?
1815 == sha256.canonicalize()?,
1816 "Git fixture did not accept the SHA-256 repository",
1817 )?;
1818 let sha256_structure = require_git(discover_repository_structure(&sha256)?)?;
1819 require_worktree_selection(
1820 &sha256_structure,
1821 &sha256.canonicalize()?,
1822 GitWorktreeRole::Primary,
1823 )?;
1824
1825 let reftable = temp.path().join("reftable checkout");
1826 run_command(
1827 Command::new("git")
1828 .args(["init", "--ref-format=reftable"])
1829 .arg(&reftable),
1830 )?;
1831 let reftable_root = Command::new("git")
1832 .current_dir(&reftable)
1833 .args(["rev-parse", "--show-toplevel"])
1834 .output()?;
1835 require(
1836 reftable_root.status.success()
1837 && Path::new(String::from_utf8(reftable_root.stdout)?.trim()).canonicalize()?
1838 == reftable.canonicalize()?,
1839 "Git fixture did not accept the reftable repository",
1840 )?;
1841 let reftable_structure = require_git(discover_repository_structure(&reftable)?)?;
1842 require_worktree_selection(
1843 &reftable_structure,
1844 &reftable.canonicalize()?,
1845 GitWorktreeRole::Primary,
1846 )?;
1847 let reftable_manager = require_git(discover_repository_structure(&reftable.join(".git"))?)?;
1848 require(
1849 reftable_manager.common_directory == reftable.join(".git").canonicalize()?,
1850 "reftable manager discovery did not retain its common directory",
1851 )?;
1852
1853 let config_path = primary.join(".git").join("config");
1854 let config = fs::read(&config_path)?;
1855 fs::remove_file(&config_path)?;
1856 let configless = require_git(discover_repository_structure(&primary.join("src"))?)?;
1857 require_worktree_selection(
1858 &configless,
1859 &primary.canonicalize()?,
1860 GitWorktreeRole::Primary,
1861 )?;
1862 let configless_manager =
1863 require_git(discover_repository_structure(&primary.join(".git"))?)?;
1864 require(
1865 configless_manager.selection
1866 == GitRepositorySelection::CommonManager {
1867 source_selection: GitManagerSourceSelection::None,
1868 },
1869 "configless manager inferred a primary checkout without positive non-bare evidence",
1870 )?;
1871 fs::write(&config_path, &config)?;
1872
1873 let mut bom_config = b"\xef\xbb\xbf".to_vec();
1874 bom_config.extend_from_slice(&config);
1875 fs::write(&config_path, &bom_config)?;
1876 let bom_bare = Command::new("git")
1877 .arg("--git-dir")
1878 .arg(primary.join(".git"))
1879 .args(["config", "--bool", "core.bare"])
1880 .output()?;
1881 require(
1882 bom_bare.status.success() && String::from_utf8(bom_bare.stdout)?.trim() == "false",
1883 "Git fixture did not accept a UTF-8 BOM in local config",
1884 )?;
1885 let bom_checkout = require_git(discover_repository_structure(&primary.join("src"))?)?;
1886 require_worktree_selection(
1887 &bom_checkout,
1888 &primary.canonicalize()?,
1889 GitWorktreeRole::Primary,
1890 )?;
1891 fs::write(&config_path, &config)?;
1892
1893 run_git(&primary, ["config", "core.bare", ""])?;
1894 let effective_bare = Command::new("git")
1895 .arg("--git-dir")
1896 .arg(primary.join(".git"))
1897 .args(["config", "--bool", "core.bare"])
1898 .output()?;
1899 require(
1900 effective_bare.status.success()
1901 && String::from_utf8(effective_bare.stdout)?.trim() == "false",
1902 "Git fixture did not interpret an empty core.bare value as false",
1903 )?;
1904 let empty_bare_manager =
1905 require_git(discover_repository_structure(&primary.join(".git"))?)?;
1906 require(
1907 empty_bare_manager.selection
1908 == GitRepositorySelection::CommonManager {
1909 source_selection: GitManagerSourceSelection::Unambiguous {
1910 root: primary.canonicalize()?,
1911 },
1912 },
1913 "empty core.bare value hid the valid primary checkout",
1914 )?;
1915 fs::write(&config_path, &config)?;
1916
1917 let configured_worktree = temp.path().join("configured external worktree");
1918 fs::create_dir(&configured_worktree)?;
1919 run_command(
1920 Command::new("git")
1921 .current_dir(&primary)
1922 .args(["config", "core.worktree"])
1923 .arg(&configured_worktree),
1924 )?;
1925 let effective_worktree = Command::new("git")
1926 .arg("--git-dir")
1927 .arg(primary.join(".git"))
1928 .args(["rev-parse", "--show-toplevel"])
1929 .output()?;
1930 require(
1931 effective_worktree.status.success()
1932 && paths_equal(
1933 &PathBuf::from(String::from_utf8(effective_worktree.stdout)?.trim())
1934 .canonicalize()?,
1935 &configured_worktree.canonicalize()?,
1936 ),
1937 "Git fixture did not relocate its configured worktree",
1938 )?;
1939 for selected in [&primary, &primary.join(".git")] {
1940 let relocated = require_git(discover_repository_structure(selected)?)?;
1941 require(
1942 relocated.selection
1943 == GitRepositorySelection::CommonManager {
1944 source_selection: GitManagerSourceSelection::None,
1945 },
1946 "core.worktree inferred the common-directory parent as source",
1947 )?;
1948 }
1949 run_git(&primary, ["config", "--unset", "core.worktree"])?;
1950
1951 run_git(&primary, ["config", "core.worktree", ".."])?;
1952 let matching_worktree = Command::new("git")
1953 .arg("-C")
1954 .arg(&primary)
1955 .args(["rev-parse", "--show-toplevel"])
1956 .output()?;
1957 require(
1958 matching_worktree.status.success()
1959 && paths_equal(
1960 &PathBuf::from(String::from_utf8(matching_worktree.stdout)?.trim())
1961 .canonicalize()?,
1962 &primary.canonicalize()?,
1963 ),
1964 "Git fixture did not resolve relative core.worktree back to its checkout",
1965 )?;
1966 let matching = require_git(discover_repository_structure(&primary)?)?;
1967 require_worktree_selection(
1968 &matching,
1969 &primary.canonicalize()?,
1970 GitWorktreeRole::Primary,
1971 )?;
1972 run_git(&primary, ["config", "--unset", "core.worktree"])?;
1973
1974 run_git(&primary, ["config", "extensions.worktreeConfig", "true"])?;
1975 run_command(
1976 Command::new("git")
1977 .current_dir(&primary)
1978 .args(["config", "--worktree", "core.worktree"])
1979 .arg(&configured_worktree),
1980 )?;
1981 let worktree_config_path = primary.join(".git").join("config.worktree");
1982 require(
1983 worktree_config_path.is_file(),
1984 "Git fixture did not create config.worktree",
1985 )?;
1986 let effective_worktree = Command::new("git")
1987 .arg("--git-dir")
1988 .arg(primary.join(".git"))
1989 .args(["rev-parse", "--show-toplevel"])
1990 .output()?;
1991 require(
1992 effective_worktree.status.success()
1993 && Path::new(String::from_utf8(effective_worktree.stdout)?.trim())
1994 .canonicalize()?
1995 == configured_worktree.canonicalize()?,
1996 "Git fixture did not honor config.worktree core.worktree",
1997 )?;
1998 for selected in [&primary, &primary.join(".git")] {
1999 let relocated = require_git(discover_repository_structure(selected)?)?;
2000 require(
2001 relocated.selection
2002 == GitRepositorySelection::CommonManager {
2003 source_selection: GitManagerSourceSelection::None,
2004 },
2005 "config.worktree core.worktree inferred the common-directory parent as source",
2006 )?;
2007 }
2008 run_git(
2009 &primary,
2010 ["config", "--worktree", "--unset", "core.worktree"],
2011 )?;
2012 run_git(&primary, ["config", "--worktree", "core.bare", "true"])?;
2013 let effective_bare = Command::new("git")
2014 .arg("--git-dir")
2015 .arg(primary.join(".git"))
2016 .args(["config", "--bool", "core.bare"])
2017 .output()?;
2018 require(
2019 effective_bare.status.success()
2020 && String::from_utf8(effective_bare.stdout)?.trim() == "true",
2021 "Git fixture did not honor config.worktree core.bare",
2022 )?;
2023 for selected in [&primary, &primary.join(".git")] {
2024 let per_worktree_bare = require_git(discover_repository_structure(selected)?)?;
2025 require(
2026 per_worktree_bare.selection
2027 == GitRepositorySelection::CommonManager {
2028 source_selection: GitManagerSourceSelection::None,
2029 },
2030 "config.worktree core.bare invented the common-directory parent as source",
2031 )?;
2032 }
2033 run_git(&primary, ["config", "--worktree", "--unset", "core.bare"])?;
2034 run_git(&primary, ["config", "--unset", "extensions.worktreeConfig"])?;
2035
2036 let submodule_source = temp.path().join("submodule source");
2037 fs::create_dir(&submodule_source)?;
2038 run_git(&submodule_source, ["init"])?;
2039 run_git(
2040 &submodule_source,
2041 ["config", "user.name", "ProjectAtlas Test"],
2042 )?;
2043 run_git(
2044 &submodule_source,
2045 ["config", "user.email", "projectatlas@example.invalid"],
2046 )?;
2047 fs::write(submodule_source.join("lib.rs"), "pub fn submodule() {}\n")?;
2048 run_git(&submodule_source, ["add", "."])?;
2049 run_git(&submodule_source, ["commit", "-m", "submodule fixture"])?;
2050 let submodule = primary.join("vendor").join("submodule");
2051 run_command(
2052 Command::new("git")
2053 .current_dir(&primary)
2054 .args(["-c", "protocol.file.allow=always", "submodule", "add"])
2055 .arg(&submodule_source)
2056 .arg("vendor/submodule"),
2057 )?;
2058 let submodule_structure = require_git(discover_repository_structure(&submodule)?)?;
2059 require_worktree_selection(
2060 &submodule_structure,
2061 &submodule.canonicalize()?,
2062 GitWorktreeRole::Primary,
2063 )?;
2064 let submodule_administrative_directory =
2065 active_entry_for_root(&submodule_structure, &submodule.canonicalize()?)?
2066 .administrative_directory
2067 .clone();
2068 let submodule_config_path = submodule_administrative_directory.join("config");
2069 let submodule_config = fs::read(&submodule_config_path)?;
2070 let quoted_pointer_worktree = submodule.with_file_name("submodule#external;worktree");
2071 fs::create_dir("ed_pointer_worktree)?;
2072 run_command(
2073 Command::new("git")
2074 .current_dir(&submodule)
2075 .args(["config", "core.worktree"])
2076 .arg("ed_pointer_worktree),
2077 )?;
2078 let effective_pointer_worktree = Command::new("git")
2079 .current_dir(&submodule)
2080 .args(["rev-parse", "--show-toplevel"])
2081 .output()?;
2082 require(
2083 effective_pointer_worktree.status.success()
2084 && paths_equal(
2085 &PathBuf::from(String::from_utf8(effective_pointer_worktree.stdout)?.trim())
2086 .canonicalize()?,
2087 "ed_pointer_worktree.canonicalize()?,
2088 ),
2089 "Git fixture did not preserve the quoted core.worktree comment marker",
2090 )?;
2091 require_invalid_kind(
2092 discover_repository_structure(&submodule)?,
2093 |kind| matches!(kind, GitStructureIssueKind::UnsupportedSourceConfiguration),
2094 "primary pointer core.worktree was admitted as pointer-owned source",
2095 )?;
2096 fs::write(&submodule_config_path, submodule_config)?;
2097 run_git(&primary, ["add", ".gitmodules", "vendor/submodule"])?;
2098 run_git(&primary, ["commit", "-m", "submodule checkout fixture"])?;
2099
2100 let lookalike = primary.join("src").join("application metadata");
2101 fs::create_dir(&lookalike)?;
2102 fs::write(lookalike.join("HEAD"), "ordinary application data\n")?;
2103 fs::write(lookalike.join("config"), "ordinary application data\n")?;
2104 let lookalike_structure = require_git(discover_repository_structure(&lookalike)?)?;
2105 require_worktree_selection(
2106 &lookalike_structure,
2107 &primary.canonicalize()?,
2108 GitWorktreeRole::Primary,
2109 )?;
2110
2111 let nested_linked = primary.join("arbitrary container").join("linked checkout");
2112 add_worktree(&primary, "nested-linked", &nested_linked)?;
2113 let config = fs::read(&config_path)?;
2114 fs::remove_file(&config_path)?;
2115 let configless_mixed_manager =
2116 require_git(discover_repository_structure(&primary.join(".git"))?)?;
2117 require(
2118 configless_mixed_manager.selection
2119 == GitRepositorySelection::CommonManager {
2120 source_selection: GitManagerSourceSelection::Ambiguous { worktree_count: 2 },
2121 },
2122 "configless mixed manager routed to its sole inventoried linked checkout",
2123 )?;
2124 fs::write(&config_path, config)?;
2125 let outside_linked = temp
2126 .path()
2127 .join("outside arbitrary å·¥ä½œæ ‘")
2128 .join("linked chëckout");
2129 add_worktree(&primary, "outside-linked", &outside_linked)?;
2130 let nested_cwd = outside_linked.join("deep").join("cwd");
2131 fs::create_dir_all(&nested_cwd)?;
2132
2133 run_git(&primary, ["config", "extensions.worktreeConfig", "true"])?;
2134 run_command(
2135 Command::new("git")
2136 .current_dir(&nested_linked)
2137 .args(["config", "--worktree", "core.worktree"])
2138 .arg(&configured_worktree),
2139 )?;
2140 let linked_effective_root = Command::new("git")
2141 .current_dir(&nested_linked)
2142 .args(["rev-parse", "--show-toplevel"])
2143 .output()?;
2144 require(
2145 linked_effective_root.status.success()
2146 && paths_equal(
2147 &PathBuf::from(String::from_utf8(linked_effective_root.stdout)?.trim())
2148 .canonicalize()?,
2149 &configured_worktree.canonicalize()?,
2150 ),
2151 "Git fixture did not honor linked config.worktree core.worktree",
2152 )?;
2153 require_invalid_kind(
2154 discover_repository_structure(&nested_linked)?,
2155 |kind| matches!(kind, GitStructureIssueKind::UnsupportedSourceConfiguration),
2156 "linked config.worktree core.worktree was admitted as pointer-owned source",
2157 )?;
2158 run_git(
2159 &nested_linked,
2160 ["config", "--worktree", "--unset", "core.worktree"],
2161 )?;
2162 run_git(
2163 &nested_linked,
2164 ["config", "--worktree", "core.bare", "true"],
2165 )?;
2166 require_invalid_kind(
2167 discover_repository_structure(&nested_linked)?,
2168 |kind| matches!(kind, GitStructureIssueKind::UnsupportedSourceConfiguration),
2169 "linked config.worktree core.bare was admitted as checked-out source",
2170 )?;
2171 run_git(
2172 &nested_linked,
2173 ["config", "--worktree", "--unset", "core.bare"],
2174 )?;
2175 run_git(&primary, ["config", "--unset", "extensions.worktreeConfig"])?;
2176
2177 let primary_structure = require_git(discover_repository_structure(&primary.join("src"))?)?;
2178 require_worktree_selection(
2179 &primary_structure,
2180 &primary.canonicalize()?,
2181 GitWorktreeRole::Primary,
2182 )?;
2183 require_active_roots(
2184 &primary_structure,
2185 [&primary, &nested_linked, &outside_linked],
2186 )?;
2187
2188 let linked_structure = require_git(discover_repository_structure(&nested_cwd)?)?;
2189 require_worktree_selection(
2190 &linked_structure,
2191 &outside_linked.canonicalize()?,
2192 GitWorktreeRole::Linked,
2193 )?;
2194 require(
2195 linked_structure.common_directory == primary.join(".git").canonicalize()?,
2196 "linked checkout did not resolve the primary common directory",
2197 )?;
2198
2199 let manager = require_git(discover_repository_structure(&primary.join(".git"))?)?;
2200 require(
2201 manager.selection
2202 == GitRepositorySelection::CommonManager {
2203 source_selection: GitManagerSourceSelection::Ambiguous { worktree_count: 3 },
2204 },
2205 "multi-worktree manager guessed or omitted its ambiguous selection",
2206 )?;
2207
2208 let copied_root = temp.path().join("copied registration");
2209 fs::create_dir(&copied_root)?;
2210 fs::copy(nested_linked.join(".git"), copied_root.join(".git"))?;
2211 let copied = discover_repository_structure(&copied_root)?;
2212 require(
2213 matches!(
2214 copied,
2215 RepositoryStructure::InvalidGit {
2216 issue: GitStructureIssue {
2217 kind: GitStructureIssueKind::ReciprocalControlMismatch { .. },
2218 ..
2219 },
2220 ..
2221 }
2222 ),
2223 "a copied one-way .git control file was admitted as reciprocal identity",
2224 )?;
2225
2226 let before_move = active_entry_for_root(&manager, &outside_linked.canonicalize()?)?
2227 .administrative_directory
2228 .clone();
2229 let relocated = temp
2230 .path()
2231 .join("relocated arbitrary container")
2232 .join("checkout");
2233 fs::create_dir_all(
2234 relocated
2235 .parent()
2236 .ok_or_else(|| io::Error::other("relocated fixture path has no parent"))?,
2237 )?;
2238 move_worktree(&primary, &outside_linked, &relocated)?;
2239 fs::create_dir_all(relocated.join("nested"))?;
2240 let relocated_structure =
2241 require_git(discover_repository_structure(&relocated.join("nested"))?)?;
2242 let relocated_root = relocated.canonicalize()?;
2243 require_worktree_selection(
2244 &relocated_structure,
2245 &relocated_root,
2246 GitWorktreeRole::Linked,
2247 )?;
2248 require(
2249 active_entry_for_root(&relocated_structure, &relocated_root)?.administrative_directory
2250 == before_move,
2251 "Git-managed relocation did not retain its administrative identity evidence",
2252 )?;
2253
2254 fs::remove_dir_all(&relocated)?;
2255 let after_removal = require_git(discover_repository_structure(&primary.join(".git"))?)?;
2256 require(
2257 after_removal.worktrees.iter().any(|entry| {
2258 entry.administrative_directory == before_move
2259 && matches!(entry.state, GitWorktreeState::Missing { .. })
2260 }),
2261 "externally removed worktree was not retained as a typed missing registration",
2262 )?;
2263
2264 let bare = temp.path().join("bare manager.git");
2265 clone_bare(&primary, &bare)?;
2266 let bare_structure = require_git(discover_repository_structure(&bare)?)?;
2267 require(
2268 bare_structure.selection
2269 == GitRepositorySelection::CommonManager {
2270 source_selection: GitManagerSourceSelection::None,
2271 },
2272 "bare manager without worktrees exposed a source selection",
2273 )?;
2274 let bare_linked = temp.path().join("bare manager checkout");
2275 add_bare_worktree(&bare, &bare_linked)?;
2276 let bare_with_source = require_git(discover_repository_structure(&bare)?)?;
2277 require(
2278 bare_with_source.selection
2279 == GitRepositorySelection::CommonManager {
2280 source_selection: GitManagerSourceSelection::Unambiguous {
2281 root: bare_linked.canonicalize()?,
2282 },
2283 },
2284 "bare manager did not expose its one unambiguous registered worktree",
2285 )?;
2286
2287 let dot_git_container = temp.path().join("bare dot-git container");
2288 fs::create_dir(&dot_git_container)?;
2289 let bare_dot_git = dot_git_container.join(".git");
2290 clone_bare(&primary, &bare_dot_git)?;
2291 for selected in [&bare_dot_git, &dot_git_container] {
2292 let structure = require_git(discover_repository_structure(selected)?)?;
2293 require(
2294 structure.selection
2295 == GitRepositorySelection::CommonManager {
2296 source_selection: GitManagerSourceSelection::None,
2297 },
2298 "bare repository named .git invented its unrelated parent as source",
2299 )?;
2300 }
2301 let bare_config_path = bare_dot_git.join("config");
2302 let bare_config = fs::read(&bare_config_path)?;
2303 run_command(
2304 Command::new("git")
2305 .arg("--git-dir")
2306 .arg(&bare_dot_git)
2307 .args(["config", "extensions.bare", "false"]),
2308 )?;
2309 let effective_bare = Command::new("git")
2310 .arg("--git-dir")
2311 .arg(&bare_dot_git)
2312 .args(["config", "--bool", "core.bare"])
2313 .output()?;
2314 require(
2315 effective_bare.status.success()
2316 && String::from_utf8(effective_bare.stdout)?.trim() == "true",
2317 "Git fixture let a non-core bare key override core.bare",
2318 )?;
2319 let non_core_bare = require_git(discover_repository_structure(&bare_dot_git)?)?;
2320 require(
2321 non_core_bare.selection
2322 == GitRepositorySelection::CommonManager {
2323 source_selection: GitManagerSourceSelection::None,
2324 },
2325 "non-core bare key invented the manager parent as source",
2326 )?;
2327 fs::write(&bare_config_path, &bare_config)?;
2328
2329 for supported_config in [
2330 "[core]\n repositoryFormatVersion = 1\n bare = false\n[extensions]\n preciousObjects = true\n",
2331 "[core]\n repositoryFormatVersion = 1\n bare = false\n[extensions]\n partialClone = origin\n",
2332 "[core]\n repositoryFormatVersion = 99\n repositoryFormatVersion = 0\n bare = false\n",
2333 ] {
2334 fs::write(&bare_config_path, supported_config)?;
2335 let accepted_config = Command::new("git")
2336 .arg("--git-dir")
2337 .arg(&bare_dot_git)
2338 .args(["rev-parse", "--is-bare-repository"])
2339 .output()?;
2340 require(
2341 accepted_config.status.success(),
2342 "Git fixture rejected a supported effective repository format",
2343 )?;
2344 let accepted_config = require_git(discover_repository_structure(&bare_dot_git)?)?;
2345 require(
2346 accepted_config.selection
2347 == GitRepositorySelection::CommonManager {
2348 source_selection: GitManagerSourceSelection::Unambiguous {
2349 root: dot_git_container.canonicalize()?,
2350 },
2351 },
2352 "supported effective repository format hid the exact manager source",
2353 )?;
2354 }
2355 fs::write(&bare_config_path, &bare_config)?;
2356
2357 for (repository_format_version, unsupported_extension) in [
2358 (1, "madeup = true"),
2359 (1, "objectformat = sha512"),
2360 (0, "objectformat = sha256"),
2361 ] {
2362 fs::write(
2363 &bare_config_path,
2364 format!(
2365 "[core]\n repositoryFormatVersion = {repository_format_version}\n bare = false\n[extensions]\n {unsupported_extension}\n"
2366 ),
2367 )?;
2368 let rejected_extension = Command::new("git")
2369 .arg("--git-dir")
2370 .arg(&bare_dot_git)
2371 .args(["rev-parse", "--is-bare-repository"])
2372 .output()?;
2373 require(
2374 !rejected_extension.status.success(),
2375 "Git fixture accepted a repository extension outside its supported format",
2376 )?;
2377 let rejected_extension = require_git(discover_repository_structure(&bare_dot_git)?)?;
2378 require(
2379 rejected_extension.selection
2380 == GitRepositorySelection::CommonManager {
2381 source_selection: GitManagerSourceSelection::None,
2382 },
2383 "repository extension outside its supported format invented the manager parent as source",
2384 )?;
2385 }
2386 fs::write(&bare_config_path, &bare_config)?;
2387
2388 run_command(
2389 Command::new("git")
2390 .arg("--git-dir")
2391 .arg(&bare_dot_git)
2392 .args(["config", "extensions.worktreeConfig", ""]),
2393 )?;
2394 fs::write(
2395 bare_dot_git.join("config.worktree"),
2396 "[core]\n bare = false\n",
2397 )?;
2398 let effective_worktree_config = Command::new("git")
2399 .arg("--git-dir")
2400 .arg(&bare_dot_git)
2401 .args(["config", "--bool", "extensions.worktreeConfig"])
2402 .output()?;
2403 require(
2404 effective_worktree_config.status.success()
2405 && String::from_utf8(effective_worktree_config.stdout)?.trim() == "false",
2406 "Git fixture did not interpret an empty extensions.worktreeConfig value as false",
2407 )?;
2408 let empty_worktree_config = require_git(discover_repository_structure(&bare_dot_git)?)?;
2409 require(
2410 empty_worktree_config.selection
2411 == GitRepositorySelection::CommonManager {
2412 source_selection: GitManagerSourceSelection::None,
2413 },
2414 "empty extensions.worktreeConfig enabled config.worktree and invented a source",
2415 )?;
2416 fs::remove_file(bare_dot_git.join("config.worktree"))?;
2417 fs::write(&bare_config_path, &bare_config)?;
2418
2419 fs::write(
2420 &bare_config_path,
2421 "[core]\n bare = true\n[extensions]\n worktreeConfig = 2\n",
2422 )?;
2423 fs::write(
2424 bare_dot_git.join("config.worktree"),
2425 "[core]\n bare = false\n",
2426 )?;
2427 let numeric_worktree_config = Command::new("git")
2428 .arg("--git-dir")
2429 .arg(&bare_dot_git)
2430 .args(["config", "--bool", "extensions.worktreeConfig"])
2431 .output()?;
2432 require(
2433 numeric_worktree_config.status.success()
2434 && String::from_utf8(numeric_worktree_config.stdout)?.trim() == "true",
2435 "Git fixture did not accept a nonzero decimal worktreeConfig boolean",
2436 )?;
2437 let numeric_worktree_config = require_git(discover_repository_structure(&bare_dot_git)?)?;
2438 require(
2439 numeric_worktree_config.selection
2440 == GitRepositorySelection::CommonManager {
2441 source_selection: GitManagerSourceSelection::Unambiguous {
2442 root: dot_git_container.canonicalize()?,
2443 },
2444 },
2445 "nonzero decimal worktreeConfig hid the exact configured source",
2446 )?;
2447 fs::remove_file(bare_dot_git.join("config.worktree"))?;
2448 fs::write(&bare_config_path, &bare_config)?;
2449
2450 fs::write(
2451 &bare_config_path,
2452 "[core]\n bare = true\n[extensions]\n worktreeConfig = maybe\n",
2453 )?;
2454 fs::write(
2455 bare_dot_git.join("config.worktree"),
2456 "[core]\n bare = false\n",
2457 )?;
2458 let invalid_worktree_config = Command::new("git")
2459 .arg("--git-dir")
2460 .arg(&bare_dot_git)
2461 .args(["config", "--bool", "extensions.worktreeConfig"])
2462 .output()?;
2463 require(
2464 !invalid_worktree_config.status.success(),
2465 "Git fixture accepted an invalid extensions.worktreeConfig boolean",
2466 )?;
2467 let invalid_worktree_config = require_git(discover_repository_structure(&bare_dot_git)?)?;
2468 require(
2469 invalid_worktree_config.selection
2470 == GitRepositorySelection::CommonManager {
2471 source_selection: GitManagerSourceSelection::None,
2472 },
2473 "invalid extensions.worktreeConfig enabled config.worktree and invented a source",
2474 )?;
2475 fs::remove_file(bare_dot_git.join("config.worktree"))?;
2476 fs::write(&bare_config_path, &bare_config)?;
2477
2478 fs::write(&bare_config_path, "[core] bare = false\n")?;
2479 let inline_section_assignment = Command::new("git")
2480 .arg("--git-dir")
2481 .arg(&bare_dot_git)
2482 .args(["config", "--bool", "core.bare"])
2483 .output()?;
2484 require(
2485 inline_section_assignment.status.success()
2486 && String::from_utf8(inline_section_assignment.stdout)?.trim() == "false",
2487 "Git fixture did not accept an assignment after a section header",
2488 )?;
2489 let inline_section_assignment = require_git(discover_repository_structure(&bare_dot_git)?)?;
2490 require(
2491 inline_section_assignment.selection
2492 == GitRepositorySelection::CommonManager {
2493 source_selection: GitManagerSourceSelection::Unambiguous {
2494 root: dot_git_container.canonicalize()?,
2495 },
2496 },
2497 "inline section assignment hid the exact configured source",
2498 )?;
2499 fs::write(&bare_config_path, &bare_config)?;
2500
2501 for malformed_config in [
2502 "[core\n bare = false\n",
2503 "[core] trailing garbage\n bare = false\n",
2504 "[core]\n bare = true\n bad line\n bare = false\n",
2505 ] {
2506 fs::write(&bare_config_path, malformed_config)?;
2507 let malformed_bare = Command::new("git")
2508 .arg("--git-dir")
2509 .arg(&bare_dot_git)
2510 .args(["config", "--bool", "core.bare"])
2511 .output()?;
2512 require(
2513 !malformed_bare.status.success(),
2514 "Git fixture accepted malformed local config",
2515 )?;
2516 let malformed_bare = require_git(discover_repository_structure(&bare_dot_git)?)?;
2517 require(
2518 malformed_bare.selection
2519 == GitRepositorySelection::CommonManager {
2520 source_selection: GitManagerSourceSelection::None,
2521 },
2522 "malformed local config invented the manager parent as source",
2523 )?;
2524 }
2525 fs::write(&bare_config_path, &bare_config)?;
2526
2527 for invalid_spacing in ['\u{000b}', '\u{000c}', '\r', '\u{00a0}'] {
2528 fs::write(
2529 &bare_config_path,
2530 format!("[other]\n value{invalid_spacing} = accepted\n[core]\n bare = false\n"),
2531 )?;
2532 let malformed_spacing = Command::new("git")
2533 .arg("--git-dir")
2534 .arg(&bare_dot_git)
2535 .args(["config", "--bool", "core.bare"])
2536 .output()?;
2537 require(
2538 !malformed_spacing.status.success(),
2539 "Git fixture accepted invalid variable-name whitespace",
2540 )?;
2541 let malformed_spacing = require_git(discover_repository_structure(&bare_dot_git)?)?;
2542 require(
2543 malformed_spacing.selection
2544 == GitRepositorySelection::CommonManager {
2545 source_selection: GitManagerSourceSelection::None,
2546 },
2547 "invalid variable-name whitespace invented the manager parent as source",
2548 )?;
2549 }
2550 fs::write(&bare_config_path, &bare_config)?;
2551
2552 fs::write(
2553 &bare_config_path,
2554 "[other]\n value = foo\"bar baz\"qux\n[core]\n bare = false\n",
2555 )?;
2556 let mixed_quoted_value = Command::new("git")
2557 .arg("--git-dir")
2558 .arg(&bare_dot_git)
2559 .args(["config", "--get", "other.value"])
2560 .output()?;
2561 require(
2562 mixed_quoted_value.status.success()
2563 && String::from_utf8(mixed_quoted_value.stdout)?.trim_end_matches(['\r', '\n'])
2564 == "foobar bazqux",
2565 "Git fixture did not concatenate mixed quoted value segments",
2566 )?;
2567 let mixed_quoted_value = require_git(discover_repository_structure(&bare_dot_git)?)?;
2568 require(
2569 mixed_quoted_value.selection
2570 == GitRepositorySelection::CommonManager {
2571 source_selection: GitManagerSourceSelection::Unambiguous {
2572 root: dot_git_container.canonicalize()?,
2573 },
2574 },
2575 "valid mixed quoted value in an unrelated section hid the exact manager source",
2576 )?;
2577 fs::write(&bare_config_path, &bare_config)?;
2578
2579 fs::write(
2580 &bare_config_path,
2581 "[other]\n value = foo\\\\\n[core]\n bare = false\n",
2582 )?;
2583 let escaped_backslash = Command::new("git")
2584 .arg("--git-dir")
2585 .arg(&bare_dot_git)
2586 .args(["config", "--get", "other.value"])
2587 .output()?;
2588 require(
2589 escaped_backslash.status.success()
2590 && String::from_utf8(escaped_backslash.stdout)?.trim_end_matches(['\r', '\n'])
2591 == "foo\\",
2592 "Git fixture did not decode the escaped terminal backslash",
2593 )?;
2594 let escaped_backslash = require_git(discover_repository_structure(&bare_dot_git)?)?;
2595 require(
2596 escaped_backslash.selection
2597 == GitRepositorySelection::CommonManager {
2598 source_selection: GitManagerSourceSelection::Unambiguous {
2599 root: dot_git_container.canonicalize()?,
2600 },
2601 },
2602 "escaped terminal backslash in an unrelated section hid the exact manager source",
2603 )?;
2604 fs::write(&bare_config_path, &bare_config)?;
2605
2606 for malformed_value in [
2607 "\"unterminated",
2608 r#"foo"unterminated"#,
2609 r"invalid\q",
2610 r#"foo"invalid\q"bar"#,
2611 ] {
2612 fs::write(
2613 &bare_config_path,
2614 format!("[other]\n value = {malformed_value}\n[core]\n bare = false\n"),
2615 )?;
2616 let malformed_unrelated_value = Command::new("git")
2617 .arg("--git-dir")
2618 .arg(&bare_dot_git)
2619 .args(["config", "--bool", "core.bare"])
2620 .output()?;
2621 require(
2622 !malformed_unrelated_value.status.success(),
2623 "Git fixture accepted a malformed value in an unrelated section",
2624 )?;
2625 let malformed_unrelated_value =
2626 require_git(discover_repository_structure(&bare_dot_git)?)?;
2627 require(
2628 malformed_unrelated_value.selection
2629 == GitRepositorySelection::CommonManager {
2630 source_selection: GitManagerSourceSelection::None,
2631 },
2632 "malformed unrelated config value invented the manager parent as source",
2633 )?;
2634 }
2635
2636 fs::write(
2637 &bare_config_path,
2638 "[core]\n bare = false # ignored Windows path C:\\\n",
2639 )?;
2640 let commented_backslash = Command::new("git")
2641 .arg("--git-dir")
2642 .arg(&bare_dot_git)
2643 .args(["config", "--bool", "core.bare"])
2644 .output()?;
2645 require(
2646 commented_backslash.status.success()
2647 && String::from_utf8(commented_backslash.stdout)?.trim() == "false",
2648 "Git fixture treated a trailing backslash in a comment as continuation",
2649 )?;
2650 let commented_backslash = require_git(discover_repository_structure(&bare_dot_git)?)?;
2651 require(
2652 commented_backslash.selection
2653 == GitRepositorySelection::CommonManager {
2654 source_selection: GitManagerSourceSelection::Unambiguous {
2655 root: dot_git_container.canonicalize()?,
2656 },
2657 },
2658 "trailing backslash inside a comment hid the exact manager source",
2659 )?;
2660 fs::write(&bare_config_path, &bare_config)?;
2661
2662 fs::write(
2663 &bare_config_path,
2664 "[core]\n bare = true\n[extensions]\n worktreeConfig = fals\\\ne\n",
2665 )?;
2666 fs::write(
2667 bare_dot_git.join("config.worktree"),
2668 "[core]\n bare = false\n",
2669 )?;
2670 let effective_worktree_config = Command::new("git")
2671 .arg("--git-dir")
2672 .arg(&bare_dot_git)
2673 .args(["config", "--bool", "extensions.worktreeConfig"])
2674 .output()?;
2675 require(
2676 effective_worktree_config.status.success()
2677 && String::from_utf8(effective_worktree_config.stdout)?.trim() == "false",
2678 "Git fixture did not join the continued extensions.worktreeConfig value",
2679 )?;
2680 let continued_worktree_config = require_git(discover_repository_structure(&bare_dot_git)?)?;
2681 require(
2682 continued_worktree_config.selection
2683 == GitRepositorySelection::CommonManager {
2684 source_selection: GitManagerSourceSelection::None,
2685 },
2686 "continued extensions.worktreeConfig value invented the manager parent as source",
2687 )?;
2688 fs::remove_file(bare_dot_git.join("config.worktree"))?;
2689 fs::write(&bare_config_path, &bare_config)?;
2690
2691 fs::remove_file(&bare_config_path)?;
2692 let configless_bare_manager = require_git(discover_repository_structure(&bare_dot_git)?)?;
2693 require(
2694 configless_bare_manager.selection
2695 == GitRepositorySelection::CommonManager {
2696 source_selection: GitManagerSourceSelection::None,
2697 },
2698 "configless .git manager inferred a primary checkout without non-bare evidence",
2699 )?;
2700 fs::write(&bare_config_path, bare_config)?;
2701
2702 let included_config = temp.path().join("included bare config");
2703 fs::write(&included_config, "[core]\n bare = true\n")?;
2704 let included_path = included_config
2705 .to_string_lossy()
2706 .replace('\\', "/")
2707 .replace('"', "\\\"");
2708 fs::write(
2709 bare_dot_git.join("config"),
2710 format!("[core]\n bare = false\n[include]\n path = \"{included_path}\"\n"),
2711 )?;
2712 let effective_bare = Command::new("git")
2713 .arg("--git-dir")
2714 .arg(&bare_dot_git)
2715 .args(["config", "--bool", "core.bare"])
2716 .output()?;
2717 require(
2718 effective_bare.status.success()
2719 && String::from_utf8(effective_bare.stdout)?.trim() == "true",
2720 "Git fixture include did not override the local core.bare value",
2721 )?;
2722 let included_bare_manager = require_git(discover_repository_structure(&bare_dot_git)?)?;
2723 require(
2724 included_bare_manager.selection
2725 == GitRepositorySelection::CommonManager {
2726 source_selection: GitManagerSourceSelection::None,
2727 },
2728 "unresolved config include invented the manager parent as source",
2729 )?;
2730
2731 let non_git = temp.path().join("plain directory");
2732 let non_git_nested = non_git.join("nested").join("cwd");
2733 fs::create_dir_all(non_git.join(".projectatlas"))?;
2734 fs::create_dir_all(&non_git_nested)?;
2735 require(
2736 discover_repository_structure(&non_git_nested)?
2737 == RepositoryStructure::NonGit {
2738 selected_root: non_git_nested.canonicalize()?,
2739 },
2740 "plain directory did not preserve the exact caller-selected non-Git root",
2741 )?;
2742 Ok(())
2743 }
2744
2745 #[test]
2746 fn native_git_pointer_records_preserve_path_whitespace() -> Result<(), Box<dyn Error>> {
2747 let temp = tempfile::tempdir()?;
2748 let pointer = temp.path().join("pointer");
2749 for value in [" relative path ", "\tleading and trailing tabs\t"] {
2750 for ending in ["", "\n", "\r\n", "\n\n"] {
2751 fs::write(&pointer, format!("gitdir: {value}{ending}"))?;
2752 require(
2753 read_prefixed_pointer(&pointer, "gitdir:")
2754 .is_ok_and(|parsed| parsed == Path::new(value)),
2755 "prefixed Git pointer changed native path whitespace",
2756 )?;
2757 fs::write(&pointer, format!("{value}{ending}"))?;
2758 require(
2759 read_plain_pointer(&pointer).is_ok_and(|parsed| parsed == Path::new(value)),
2760 "plain Git pointer changed native path whitespace",
2761 )?;
2762 }
2763 }
2764 for malformed in [b"".as_slice(), b"\r\n", b"first\nsecond", b"path\0suffix"] {
2765 fs::write(&pointer, malformed)?;
2766 require(
2767 read_plain_pointer(&pointer).is_err(),
2768 "malformed plain Git pointer was accepted",
2769 )?;
2770 }
2771 for malformed in [
2772 "gitdir:",
2773 "gitdir: ",
2774 "gitdir:path",
2775 " gitdir: path",
2776 "\ngitdir: path",
2777 ] {
2778 fs::write(&pointer, malformed)?;
2779 require(
2780 read_prefixed_pointer(&pointer, "gitdir:").is_err(),
2781 "malformed prefixed Git pointer was accepted",
2782 )?;
2783 }
2784 Ok(())
2785 }
2786
2787 #[test]
2788 fn structural_discovery_is_git_process_independent_and_rejects_unsafe_control_files()
2789 -> Result<(), Box<dyn Error>> {
2790 let temp = tempfile::tempdir()?;
2791 let handwritten = temp.path().join("handwritten checkout");
2792 write_structural_primary(&handwritten)?;
2793 let structure = require_git(discover_repository_structure(&handwritten)?)?;
2794 require_worktree_selection(
2795 &structure,
2796 &handwritten.canonicalize()?,
2797 GitWorktreeRole::Primary,
2798 )?;
2799
2800 let malformed = temp.path().join("malformed pointer");
2801 fs::create_dir(&malformed)?;
2802 fs::write(malformed.join(".git"), "gitdir: first\ngitdir: second\n")?;
2803 require_invalid_kind(
2804 discover_repository_structure(&malformed)?,
2805 |kind| matches!(kind, GitStructureIssueKind::MalformedPointer),
2806 "ambiguous multi-record .git pointer was not rejected",
2807 )?;
2808
2809 let oversized = temp.path().join("oversized pointer");
2810 fs::create_dir(&oversized)?;
2811 fs::write(
2812 oversized.join(".git"),
2813 vec![b'x'; GIT_DIRECTORY_POINTER_MAX_BYTES as usize + 1],
2814 )?;
2815 require_invalid_kind(
2816 discover_repository_structure(&oversized)?,
2817 |kind| matches!(kind, GitStructureIssueKind::PointerTooLarge { .. }),
2818 "oversized .git pointer was not rejected at the byte bound",
2819 )?;
2820
2821 let non_utf8 = temp.path().join("non utf8 pointer");
2822 fs::create_dir(&non_utf8)?;
2823 fs::write(non_utf8.join(".git"), [0xff, 0xfe])?;
2824 require_invalid_kind(
2825 discover_repository_structure(&non_utf8)?,
2826 |kind| matches!(kind, GitStructureIssueKind::PointerNotUtf8),
2827 "non-UTF-8 .git pointer was not rejected",
2828 )?;
2829
2830 let symlinked = temp.path().join("symlinked control");
2831 fs::create_dir(&symlinked)?;
2832 create_control_symlink(&handwritten.join(".git"), &symlinked.join(".git"))?;
2833 require_invalid_kind(
2834 discover_repository_structure(&symlinked)?,
2835 |kind| matches!(kind, GitStructureIssueKind::SymbolicLink),
2836 "symbolic-link Git control metadata was followed as identity evidence",
2837 )?;
2838
2839 let common_directory = handwritten.join(".git");
2840 let external_registrations = temp.path().join("external registrations");
2841 fs::create_dir(&external_registrations)?;
2842 let registrations = common_directory.join("worktrees");
2843 create_control_symlink(&external_registrations, ®istrations)?;
2844 require_invalid_kind(
2845 discover_repository_structure(&handwritten)?,
2846 |kind| matches!(kind, GitStructureIssueKind::SymbolicLink),
2847 "indirect worktree registration container was followed outside the common root",
2848 )?;
2849 remove_control_symlink(®istrations)?;
2850
2851 fs::create_dir(®istrations)?;
2852 let external_registration = temp.path().join("external registration");
2853 fs::create_dir(&external_registration)?;
2854 let indirect_registration = registrations.join("indirect registration");
2855 create_control_symlink(&external_registration, &indirect_registration)?;
2856 let structure = require_git(discover_repository_structure(&handwritten)?)?;
2857 require(
2858 structure.worktrees.iter().any(|entry| {
2859 matches!(
2860 &entry.state,
2861 GitWorktreeState::Invalid {
2862 issue: GitStructureIssue {
2863 kind: GitStructureIssueKind::SymbolicLink,
2864 ..
2865 }
2866 }
2867 )
2868 }),
2869 "indirect registration entry was not retained as typed invalid evidence",
2870 )?;
2871 remove_control_symlink(&indirect_registration)?;
2872
2873 let cancellation = IndexCancellation::new();
2874 cancellation.cancel();
2875 let canceled = discover_repository_structure_controlled(
2876 &handwritten,
2877 &IndexWorkControl::new(cancellation, None),
2878 );
2879 require(
2880 matches!(
2881 canceled,
2882 Err(FsError::IndexWork(IndexWorkFailure::Cancelled {
2883 stage: IndexWorkStage::RepositoryTraversal
2884 }))
2885 ),
2886 "pre-canceled structural discovery did not stop before filesystem traversal",
2887 )?;
2888 Ok(())
2889 }
2890
2891 #[test]
2892 fn structural_discovery_enforces_the_registration_count_bound_without_partial_state()
2893 -> Result<(), Box<dyn Error>> {
2894 let temp = tempfile::tempdir()?;
2895 let repo = temp.path().join("bounded checkout");
2896 write_structural_primary(&repo)?;
2897 let registrations = repo.join(".git").join("worktrees");
2898 fs::create_dir(®istrations)?;
2899 for index in 0..=projectatlas_core::MAX_GIT_WORKTREE_REGISTRATIONS {
2900 fs::create_dir(registrations.join(format!("registration-{index:04}")))?;
2901 }
2902
2903 let result = discover_repository_structure(&repo);
2904 require(
2905 matches!(
2906 result,
2907 Err(FsError::IndexWork(
2908 IndexWorkFailure::ResourceLimitExceeded {
2909 stage: IndexWorkStage::RepositoryTraversal,
2910 resource: IndexWorkResource::Entries,
2911 limit,
2912 observed,
2913 }
2914 )) if limit == projectatlas_core::MAX_GIT_WORKTREE_REGISTRATIONS as u64
2915 && observed == projectatlas_core::MAX_GIT_WORKTREE_REGISTRATIONS as u64 + 1
2916 ),
2917 "registration overflow returned partial or untyped repository state",
2918 )?;
2919 Ok(())
2920 }
2921
2922 #[test]
2923 fn exact_worktree_lifecycle_check_does_not_inventory_sibling_registrations()
2924 -> Result<(), Box<dyn Error>> {
2925 let temp = tempfile::tempdir()?;
2926 let root = temp.path().join("exact checkout");
2927 write_structural_primary(&root)?;
2928 let common = root.join(".git").canonicalize()?;
2929 let identity = git_administrative_identity(&common)?;
2930 let registrations = common.join("worktrees");
2931 fs::create_dir(®istrations)?;
2932 for index in 0..=projectatlas_core::MAX_GIT_WORKTREE_REGISTRATIONS {
2933 fs::create_dir(registrations.join(format!("unrelated-{index:04}")))?;
2934 }
2935
2936 require(
2937 git_worktree_lifecycle_matches(&root, &common, &common, &identity)?,
2938 "exact lifecycle validation enumerated unrelated worktree registrations",
2939 )
2940 }
2941
2942 #[cfg(unix)]
2943 #[test]
2944 fn real_git_worktree_preserves_non_utf8_native_paths() -> Result<(), Box<dyn Error>> {
2945 use std::ffi::OsString;
2946 use std::os::unix::ffi::OsStringExt;
2947
2948 let temp = tempfile::tempdir()?;
2949 let primary = temp
2950 .path()
2951 .join(OsString::from_vec(b"primary-\xff".to_vec()));
2952 fs::create_dir(&primary)?;
2953 run_git(&primary, ["init"])?;
2954 run_git(&primary, ["config", "user.name", "ProjectAtlas Test"])?;
2955 run_git(
2956 &primary,
2957 ["config", "user.email", "projectatlas@example.invalid"],
2958 )?;
2959 fs::write(primary.join("README.md"), "native worktree identity\n")?;
2960 run_git(&primary, ["add", "."])?;
2961 run_git(&primary, ["commit", "-m", "fixture"])?;
2962
2963 let linked = temp
2964 .path()
2965 .join(OsString::from_vec(b"linked-\xfe".to_vec()));
2966 add_worktree(&primary, "native-bytes", &linked)?;
2967
2968 let primary = primary.canonicalize()?;
2969 let linked = linked.canonicalize()?;
2970 let structure = require_git(discover_repository_structure(&linked)?)?;
2971 let entry = structure
2972 .worktrees
2973 .iter()
2974 .find(|entry| {
2975 matches!(
2976 &entry.state,
2977 GitWorktreeState::Active { root, .. } if root == &linked
2978 )
2979 })
2980 .ok_or_else(|| io::Error::other("native linked worktree was not discovered"))?;
2981
2982 require(
2983 primary.to_str().is_none()
2984 && linked.to_str().is_none()
2985 && structure.common_directory.to_str().is_none()
2986 && entry.administrative_directory.to_str().is_none(),
2987 "native invalid-byte worktree paths unexpectedly became UTF-8",
2988 )?;
2989 require(
2990 structure.common_directory == primary.join(".git"),
2991 "native linked worktree changed its common directory bytes",
2992 )?;
2993 let administrative_identity = git_administrative_identity(&entry.administrative_directory)?;
2994 require(
2995 git_worktree_lifecycle_matches(
2996 &linked,
2997 &structure.common_directory,
2998 &entry.administrative_directory,
2999 &administrative_identity,
3000 )?,
3001 "native linked worktree lifecycle did not round-trip through Git evidence",
3002 )?;
3003 Ok(())
3004 }
3005
3006 #[test]
3007 fn git_config_values_accept_mixed_quotes_without_weakening_syntax_checks() {
3008 for (raw, expected) in [
3009 (r#" foo"bar baz"qux "#, "foobar bazqux"),
3010 (
3011 r#"pre"quoted #; value"post ; ignored comment"#,
3012 "prequoted #; valuepost",
3013 ),
3014 (r#"escaped\"quote"#, r#"escaped\"quote"#),
3015 (" false # ignored trailing backslash \\", "false"),
3016 ] {
3017 assert_eq!(git_config_value(raw).as_deref(), Some(expected));
3018 }
3019 for malformed in [r#"foo"unterminated"#, r#"foo"invalid\q"bar"#, "trailing\\"] {
3020 assert_eq!(git_config_value(malformed), None);
3021 }
3022 }
3023
3024 #[test]
3025 fn git_repository_extension_policy_covers_defined_keys() -> Result<(), Box<dyn Error>> {
3026 let temp = tempfile::tempdir()?;
3027 let config = temp.path().join("config");
3028 for (repository_format_version, extension, accepted) in [
3029 (1, "compatObjectFormat = sha256", true),
3030 (0, "noop = true", true),
3031 (1, "noop-v1 = true", true),
3032 (1, "objectFormat = sha256", true),
3033 (0, "partialClone = origin", true),
3034 (0, "preciousObjects = true", true),
3035 (1, "refStorage = files", true),
3036 (1, "refStorage = reftable", true),
3037 (1, "relativeWorktrees = true", true),
3038 (1, "submodulePathConfig = true", true),
3039 (0, "worktreeConfig = true", true),
3040 (1, "compatObjectFormat = sha1", false),
3041 (0, "partialClone =", true),
3042 (0, "preciousObjects = invalid", false),
3043 (1, "refStorage = unknown", false),
3044 (1, "relativeWorktrees = invalid", false),
3045 (1, "submodulePathConfig = invalid", false),
3046 (0, "worktreeConfig = invalid", false),
3047 ] {
3048 fs::write(
3049 &config,
3050 format!(
3051 "[core]\n repositoryFormatVersion = {repository_format_version}\n[extensions]\n {extension}\n"
3052 ),
3053 )?;
3054 let policy = match local_config_policy(&config) {
3055 Ok(policy) => policy,
3056 Err(issue) => {
3057 return Err(io::Error::other(format!(
3058 "defined repository extension config was unreadable: {issue:?}"
3059 ))
3060 .into());
3061 }
3062 };
3063 require(
3064 policy.source_root_inference_safe == accepted
3065 && policy.source_selection_policy_complete == accepted,
3066 &format!("repository extension policy disagreed for: {extension}"),
3067 )?;
3068 }
3069 Ok(())
3070 }
3071
3072 #[cfg(unix)]
3073 #[test]
3074 fn unquoted_git_config_escapes_cannot_change_pointer_owner() -> Result<(), Box<dyn Error>> {
3075 let temp = tempfile::tempdir()?;
3076 let pointer_owner = temp.path().join(r"owner\\root");
3077 fs::create_dir(&pointer_owner)?;
3078 write_structural_primary(&pointer_owner)?;
3079 fs::write(
3080 pointer_owner.join(".git").join("config"),
3081 "[core]\n bare = false\n worktree = ../../owner\\\\root\n",
3082 )?;
3083
3084 let effective_setting = Command::new("git")
3085 .arg("--git-dir")
3086 .arg(pointer_owner.join(".git"))
3087 .args(["config", "--get", "core.worktree"])
3088 .output()?;
3089 require(
3090 effective_setting.status.success()
3091 && String::from_utf8(effective_setting.stdout)?.trim_end_matches(['\r', '\n'])
3092 == r"../../owner\root",
3093 "Git fixture did not decode the unquoted backslash escape",
3094 )?;
3095 let escaped_pointer = require_git(discover_repository_structure(&pointer_owner)?)?;
3096 require(
3097 escaped_pointer.selection
3098 == GitRepositorySelection::CommonManager {
3099 source_selection: GitManagerSourceSelection::None,
3100 },
3101 "unquoted backslash escape admitted a pointer owner Git did not select",
3102 )?;
3103 Ok(())
3104 }
3105
3106 #[cfg(unix)]
3107 #[test]
3108 fn lifecycle_identity_requires_a_creation_timestamp() {
3109 let result = required_creation_nanos(
3110 Path::new("administrative-directory"),
3111 Err(io::Error::new(
3112 io::ErrorKind::Unsupported,
3113 "creation time unavailable",
3114 )),
3115 );
3116 assert!(matches!(
3117 result,
3118 Err(FsError::RepositoryBoundary { source, .. })
3119 if source.kind() == io::ErrorKind::Unsupported
3120 ));
3121 }
3122
3123 #[cfg(windows)]
3124 #[test]
3125 fn windows_lifecycle_identity_includes_stable_volume_and_file_identity()
3126 -> Result<(), Box<dyn Error>> {
3127 let temp = tempfile::tempdir()?;
3128 let administrative_directory = temp.path().join("administrative directory");
3129 fs::create_dir(&administrative_directory)?;
3130 let first_native = windows_file_identity::read(&administrative_directory)?;
3131 let first = git_administrative_identity(&administrative_directory)?;
3132 let stable_native = windows_file_identity::read(&administrative_directory)?;
3133 let stable = git_administrative_identity(&administrative_directory)?;
3134 require(
3135 first_native.volume_serial_number == stable_native.volume_serial_number
3136 && first_native.file_id == stable_native.file_id
3137 && first == stable,
3138 "Windows directory identity changed within one lifecycle",
3139 )?;
3140
3141 fs::remove_dir(&administrative_directory)?;
3142 fs::create_dir(&administrative_directory)?;
3143 let replacement_native = windows_file_identity::read(&administrative_directory)?;
3144 let replacement = git_administrative_identity(&administrative_directory)?;
3145 require(
3146 first_native.volume_serial_number != replacement_native.volume_serial_number
3147 || first_native.file_id != replacement_native.file_id,
3148 "Windows replacement reused the original volume and file identity",
3149 )?;
3150 require(
3151 first != replacement,
3152 "Windows replacement reused the original lifecycle hash",
3153 )?;
3154 Ok(())
3155 }
3156
3157 fn run_git<const N: usize>(repo: &Path, arguments: [&str; N]) -> Result<(), Box<dyn Error>> {
3159 run_command(Command::new("git").current_dir(repo).args(arguments))
3160 }
3161
3162 fn add_worktree(repo: &Path, branch: &str, path: &Path) -> Result<(), Box<dyn Error>> {
3164 run_command(
3165 Command::new("git")
3166 .current_dir(repo)
3167 .args(["worktree", "add", "-b", branch])
3168 .arg(path),
3169 )
3170 }
3171
3172 fn move_worktree(repo: &Path, from: &Path, to: &Path) -> Result<(), Box<dyn Error>> {
3174 run_command(
3175 Command::new("git")
3176 .current_dir(repo)
3177 .args([OsStr::new("worktree"), OsStr::new("move")])
3178 .arg(from)
3179 .arg(to),
3180 )
3181 }
3182
3183 fn clone_bare(repo: &Path, bare: &Path) -> Result<(), Box<dyn Error>> {
3185 let parent = repo
3186 .parent()
3187 .ok_or_else(|| io::Error::other("fixture repository has no parent"))?;
3188 run_command(
3189 Command::new("git")
3190 .current_dir(parent)
3191 .args([OsStr::new("clone"), OsStr::new("--bare")])
3192 .arg(repo)
3193 .arg(bare),
3194 )
3195 }
3196
3197 fn add_bare_worktree(bare: &Path, path: &Path) -> Result<(), Box<dyn Error>> {
3199 run_command(
3200 Command::new("git")
3201 .arg("--git-dir")
3202 .arg(bare)
3203 .args(["worktree", "add", "--detach"])
3204 .arg(path)
3205 .arg("HEAD"),
3206 )
3207 }
3208
3209 fn run_command(command: &mut Command) -> Result<(), Box<dyn Error>> {
3211 let output = command.output()?;
3212 if output.status.success() {
3213 Ok(())
3214 } else {
3215 Err(io::Error::other(format!(
3216 "fixture Git command failed: {}{}",
3217 String::from_utf8_lossy(&output.stdout),
3218 String::from_utf8_lossy(&output.stderr),
3219 ))
3220 .into())
3221 }
3222 }
3223
3224 fn write_structural_primary(root: &Path) -> Result<(), Box<dyn Error>> {
3226 let common = root.join(".git");
3227 write_structural_common(&common)
3228 }
3229
3230 fn write_structural_common(common: &Path) -> Result<(), Box<dyn Error>> {
3232 fs::create_dir_all(common.join("objects"))?;
3233 fs::create_dir(common.join("refs"))?;
3234 fs::write(common.join("HEAD"), "ref: refs/heads/main\n")?;
3235 fs::write(common.join("config"), "[core]\n bare = false\n")?;
3236 Ok(())
3237 }
3238
3239 #[cfg(unix)]
3241 fn create_control_symlink(target: &Path, link: &Path) -> Result<(), Box<dyn Error>> {
3242 std::os::unix::fs::symlink(target, link)?;
3243 Ok(())
3244 }
3245
3246 #[cfg(windows)]
3248 fn create_control_symlink(target: &Path, link: &Path) -> Result<(), Box<dyn Error>> {
3249 match std::os::windows::fs::symlink_dir(target, link) {
3250 Ok(()) => Ok(()),
3251 Err(error)
3252 if error.kind() == io::ErrorKind::PermissionDenied
3253 || error.raw_os_error() == Some(1314) =>
3254 {
3255 run_command(
3256 Command::new("cmd.exe")
3257 .args(["/D", "/C", "mklink", "/J"])
3258 .arg(link)
3259 .arg(target),
3260 )?;
3261 Ok(())
3262 }
3263 Err(error) => Err(error.into()),
3264 }
3265 }
3266
3267 #[cfg(unix)]
3269 fn remove_control_symlink(link: &Path) -> Result<(), Box<dyn Error>> {
3270 fs::remove_file(link)?;
3271 Ok(())
3272 }
3273
3274 #[cfg(windows)]
3276 fn remove_control_symlink(link: &Path) -> Result<(), Box<dyn Error>> {
3277 fs::remove_dir(link)?;
3278 Ok(())
3279 }
3280
3281 fn require_git(
3283 structure: RepositoryStructure,
3284 ) -> Result<GitRepositoryStructure, Box<dyn Error>> {
3285 match structure {
3286 RepositoryStructure::Git(structure) => Ok(structure),
3287 other => {
3288 Err(io::Error::other(format!("expected Git structure, found {other:?}")).into())
3289 }
3290 }
3291 }
3292
3293 fn require_worktree_selection(
3295 structure: &GitRepositoryStructure,
3296 expected_root: &Path,
3297 expected_role: GitWorktreeRole,
3298 ) -> Result<(), Box<dyn Error>> {
3299 require(
3300 matches!(
3301 &structure.selection,
3302 GitRepositorySelection::Worktree { root, role, .. }
3303 if paths_equal(root, expected_root) && *role == expected_role
3304 ),
3305 "repository selection did not identify the expected exact worktree",
3306 )
3307 }
3308
3309 fn require_active_roots<const N: usize>(
3311 structure: &GitRepositoryStructure,
3312 expected: [&Path; N],
3313 ) -> Result<(), Box<dyn Error>> {
3314 for root in expected {
3315 let root = root.canonicalize()?;
3316 let _ = active_entry_for_root(structure, &root)?;
3317 }
3318 Ok(())
3319 }
3320
3321 fn active_entry_for_root<'a>(
3323 structure: &'a GitRepositoryStructure,
3324 expected_root: &Path,
3325 ) -> Result<&'a GitWorktreeEntry, Box<dyn Error>> {
3326 structure
3327 .worktrees
3328 .iter()
3329 .find(|entry| {
3330 matches!(
3331 &entry.state,
3332 GitWorktreeState::Active { root, .. } if paths_equal(root, expected_root)
3333 )
3334 })
3335 .ok_or_else(|| {
3336 io::Error::other(format!(
3337 "missing active structural worktree {}",
3338 expected_root.display()
3339 ))
3340 .into()
3341 })
3342 }
3343
3344 fn require_invalid_kind(
3346 structure: RepositoryStructure,
3347 matches_kind: impl FnOnce(&GitStructureIssueKind) -> bool,
3348 message: &str,
3349 ) -> Result<(), Box<dyn Error>> {
3350 let matches = match structure {
3351 RepositoryStructure::InvalidGit { issue, .. } => matches_kind(&issue.kind),
3352 RepositoryStructure::NonGit { .. } | RepositoryStructure::Git(_) => false,
3353 };
3354 require(matches, message)
3355 }
3356
3357 fn require(condition: bool, message: &str) -> Result<(), Box<dyn Error>> {
3359 if condition {
3360 Ok(())
3361 } else {
3362 Err(io::Error::other(message).into())
3363 }
3364 }
3365}