diff --git a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py index b69c6b08..b0d7ff8a 100644 --- a/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py +++ b/sdk/typescript/_bundled_plugin/scripts/generate_in_scope_files.py @@ -4,16 +4,100 @@ from __future__ import annotations import argparse +import codecs +import io +import mmap +import os +import re +import stat import subprocess import sys import tempfile -from pathlib import Path +import unicodedata +from collections.abc import Iterator +from pathlib import Path, PurePosixPath + +IGNORE_FILE_NAMES = (".gitignore", ".ignore", ".rgignore") class InventoryError(ValueError): """Raised when the repository, scope, or inventory cannot be used safely.""" +def symbolic_metadata(metadata: os.stat_result) -> bool: + reparse_point = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0) + return stat.S_ISLNK(metadata.st_mode) or bool( + getattr(metadata, "st_file_attributes", 0) & reparse_point + ) + + +def filesystem_name_key(value: str) -> str: + return unicodedata.normalize("NFC", value).upper().casefold().rstrip(". ") + + +def split_index_backing(index: Path, hash_size: int) -> str | None: + with index.open("rb") as handle: + if os.fstat(handle.fileno()).st_size == 0: + return None + with mmap.mmap(handle.fileno(), 0, access=mmap.ACCESS_READ) as contents: + if len(contents) < 12 + hash_size or contents[:4] != b"DIRC": + return None + version = int.from_bytes(contents[4:8], "big") + if version not in (2, 3, 4): + return None + count = int.from_bytes(contents[8:12], "big") + position = 12 + limit = len(contents) - hash_size + for _ in range(count): + entry = position + position += 40 + hash_size + if position + 2 > limit: + return None + flags = int.from_bytes(contents[position : position + 2], "big") + position += 2 + (2 if flags & 0x4000 else 0) + if version == 4: + while position < limit: + byte = contents[position] + position += 1 + if not byte & 0x80: + break + else: + return None + end = contents.find(b"\0", position, limit) + if end < 0: + return None + position = end + 1 if version == 4 else entry + ((end - entry + 8) & ~7) + while position + 8 <= limit: + signature = contents[position : position + 4] + length = int.from_bytes(contents[position + 4 : position + 8], "big") + position += 8 + if position + length > limit: + return None + if signature == b"link": + if length < hash_size: + return None + return contents[position : position + hash_size].hex() + position += length + return None + + +def git_metadata_path(parent: Path, name: str) -> bool: + if name == ".git": + return True + if filesystem_name_key(name) != ".git": + return False + try: + candidate = (parent / name).stat(follow_symlinks=False) + if symbolic_metadata(candidate): + raise InventoryError("symbolic Git metadata paths are not supported") + metadata = (parent / ".git").stat(follow_symlinks=False) + if symbolic_metadata(metadata): + raise InventoryError("symbolic Git metadata paths are not supported") + return (candidate.st_dev, candidate.st_ino) == (metadata.st_dev, metadata.st_ino) + except OSError: + return False + + def resolve_repository(value: str) -> Path: """Resolve the repository once so every scope is bound to its real root.""" try: @@ -37,17 +121,44 @@ def resolve_scope(repository: Path, value: str) -> str: except (OSError, ValueError) as error: raise InventoryError(f"--scope: path does not exist: {value}") from error + repository_metadata = repository.stat(follow_symlinks=False) + repository_identity = (repository_metadata.st_dev, repository_metadata.st_ino) try: relative = resolved.relative_to(repository) except ValueError as error: - raise InventoryError(f"--scope: path must remain inside --repo: {value}") from error + ancestor = resolved + while True: + metadata = ancestor.stat(follow_symlinks=False) + if (metadata.st_dev, metadata.st_ino) == repository_identity: + relative = Path(*resolved.parts[len(ancestor.parts) :]) + break + if ancestor == ancestor.parent: + raise InventoryError( + f"--scope: path must remain inside --repo: {value}" + ) from error + ancestor = ancestor.parent + parent = repository + for component in relative.parts: + if git_metadata_path(parent, component): + raise InventoryError("--scope: Git metadata paths are not supported") + parent /= component + + current = scope + while True: + metadata = current.stat(follow_symlinks=False) + if symbolic_metadata(metadata): + raise InventoryError("--scope: symbolic links are not supported") + if (metadata.st_dev, metadata.st_ino) == repository_identity: + break + if current == current.parent: + raise InventoryError("--scope: symbolic links are not supported") + current = current.parent if not resolved.is_dir() and not resolved.is_file(): raise InventoryError(f"--scope: expected a file or directory: {value}") - if requested.is_absolute(): - return relative.as_posix() if relative.parts else "." - return value + canonical = relative.as_posix() if relative.parts else "." + return f"./{canonical}" if value.startswith("./") and canonical != "." else canonical def resolve_output(value: str) -> Path: @@ -67,40 +178,1553 @@ def resolve_output(value: str) -> Path: def generate_in_scope_files(repository: Path, scope: str, output: Path) -> int: - """Atomically write the exact ripgrep inventory sorted as ``LC_ALL=C``.""" + """Atomically inventory visible files and ignored files tracked by Git.""" + selected = (repository / scope).resolve(strict=True) + selected_directory = selected if selected.is_dir() else selected.parent + ancestors: list[Path] = [] + current = selected_directory + while True: + ancestors.append(current) + if current == repository: + break + current = current.parent + ancestors.reverse() + def reject_symbolic_ignore(directory: Path) -> None: + for name in IGNORE_FILE_NAMES: + try: + metadata = (directory / name).stat(follow_symlinks=False) + except FileNotFoundError: + continue + if not symbolic_metadata(metadata) and ( + stat.S_ISREG(metadata.st_mode) or stat.S_ISDIR(metadata.st_mode) + ): + continue + if symbolic_metadata(metadata): + raise InventoryError("symbolic ignore files are not supported") + raise InventoryError("non-regular ignore files are not supported") + + def directory_identity(path: Path) -> tuple[int, int]: + metadata = path.stat() + return metadata.st_dev, metadata.st_ino + + def same_filesystem_path(first: Path, second: Path) -> bool: + try: + return directory_identity(first) == directory_identity(second) and directory_identity( + first.parent + ) == directory_identity(second.parent) + except FileNotFoundError: + return False + + def nonsymbolic_directory(path: Path) -> bool: + try: + metadata = path.stat(follow_symlinks=False) + except OSError: + return False + return stat.S_ISDIR(metadata.st_mode) and not symbolic_metadata(metadata) + + gitdir_owners: dict[tuple[int, int], tuple[int, int]] = {} + validated_object_stores: set[tuple[int, int]] = set() + + def has_git_marker(directory: Path) -> bool: + marker = directory / ".git" + + def inspect_metadata( + path: Path, *, directory: bool | None = None + ) -> os.stat_result | None: + if ( + os.name == "nt" + and path.anchor.startswith("\\\\") + and os.path.normcase(path.anchor) != os.path.normcase(repository.anchor) + ): + raise InventoryError("network Git metadata paths are not supported") + try: + metadata = path.stat(follow_symlinks=False) + except FileNotFoundError: + return None + except (OSError, ValueError) as error: + raise InventoryError(f"could not inspect Git metadata: {directory}") from error + if symbolic_metadata(metadata): + raise InventoryError("symbolic Git metadata paths are not supported") + if directory is True and not stat.S_ISDIR(metadata.st_mode): + raise InventoryError("non-directory Git metadata paths are not supported") + if directory is False and not stat.S_ISREG(metadata.st_mode): + raise InventoryError("non-regular Git metadata files are not supported") + return metadata + + def inspect_metadata_path(path: Path, *, directory_path: bool | None) -> Path | None: + current = Path(path.anchor) + if inspect_metadata(current, directory=True) is None: + return None + components = path.parts[1:] + for index, component in enumerate(components): + if component == "..": + current = current.parent + continue + current /= component + if ( + inspect_metadata( + current, + directory=directory_path if index + 1 == len(components) else True, + ) + is None + ): + return None + return current + + def aliases_canonical_path(path: Path, canonical: str) -> bool: + try: + actual = path.stat(follow_symlinks=False) + expected = (path.parent / canonical).stat(follow_symlinks=False) + except FileNotFoundError: + return False + return (actual.st_dev, actual.st_ino) == (expected.st_dev, expected.st_ino) + + def inspect_object_store(objects: Path) -> None: + try: + identity = directory_identity(objects) + if identity in validated_object_stores: + return + entries = objects.iterdir() + for entry in entries: + canonical = filesystem_name_key(entry.name) + if canonical not in ("info", "pack") and not re.fullmatch( + r"[0-9a-f]{2}", canonical + ): + continue + if entry.name != canonical and not aliases_canonical_path(entry, canonical): + continue + inspect_metadata(entry, directory=True) + if canonical != "info": + for member in entry.iterdir(): + member_canonical = filesystem_name_key(member.name) + if canonical == "pack": + if member_canonical == "multi-pack-index.d": + if member.name != member_canonical and not aliases_canonical_path( + member, member_canonical + ): + continue + inspect_metadata(member, directory=True) + for layer in member.iterdir(): + layer_canonical = filesystem_name_key(layer.name) + if layer_canonical != "multi-pack-index-chain" and not re.fullmatch( + r"multi-pack-index-[0-9a-f]{40}(?:[0-9a-f]{24})?\.(?:midx|bitmap|rev)", + layer_canonical, + ): + continue + if layer.name != layer_canonical and not aliases_canonical_path( + layer, layer_canonical + ): + continue + inspect_metadata(layer, directory=False) + continue + if member_canonical == "multi-pack-index": + expected = member_canonical + elif not member_canonical.endswith( + (".pack", ".idx", ".rev", ".bitmap", ".keep", ".promisor", ".mtimes") + ): + continue + else: + stem, _, suffix = member.name.rpartition(".") + expected = f"{stem}.{filesystem_name_key(suffix)}" + else: + if not re.fullmatch( + r"(?:[0-9a-f]{38}|[0-9a-f]{62})", member_canonical + ): + continue + expected = member_canonical + if member.name != expected and not aliases_canonical_path(member, expected): + continue + inspect_metadata(member, directory=False) + validated_object_stores.add(identity) + except OSError as error: + raise InventoryError(f"could not inspect Git metadata: {directory}") from error + + try: + metadata = marker.stat(follow_symlinks=False) + except FileNotFoundError: + return False + except OSError as error: + raise InventoryError(f"could not inspect Git metadata: {directory}") from error + if symbolic_metadata(metadata): + raise InventoryError("symbolic Git metadata paths are not supported") + gitfile = stat.S_ISREG(metadata.st_mode) + backpointer_owned = False + if stat.S_ISDIR(metadata.st_mode): + gitdir = marker + elif gitfile: + try: + contents = marker.read_bytes() + except OSError as error: + raise InventoryError(f"could not inspect Git metadata: {directory}") from error + if not contents.startswith(b"gitdir: "): + return True + gitdir = Path(os.fsdecode(contents.removeprefix(b"gitdir: ").rstrip(b"\r\n"))) + if not gitdir.is_absolute(): + gitdir = directory / gitdir + inspected_gitdir = inspect_metadata_path(gitdir, directory_path=True) + if inspected_gitdir is None: + raise InventoryError("Git metadata directory does not own selected worktree") + gitdir = inspected_gitdir + internally_owned = len(gitdir.parts) >= len(repository.parts) + if internally_owned: + ancestor = gitdir + for _ in range(len(gitdir.parts) - len(repository.parts)): + ancestor = ancestor.parent + internally_owned = directory_identity(ancestor) == directory_identity(repository) + backpointer = gitdir / "gitdir" + if inspect_metadata(backpointer, directory=False) is not None: + backpointer_owned = True + try: + target = Path(os.fsdecode(backpointer.read_bytes().rstrip(b"\r\n"))) + except (OSError, ValueError) as error: + raise InventoryError(f"could not inspect Git metadata: {directory}") from error + if not target.is_absolute(): + target = gitdir / target + inspected_target = inspect_metadata_path(target, directory_path=None) + if inspected_target is None or not same_filesystem_path(inspected_target, marker): + raise InventoryError("Git metadata directory does not own selected worktree") + elif not internally_owned: + raise InventoryError("Git metadata directory does not own selected worktree") + else: + return False + + identity = directory_identity(gitdir) + owner = directory_identity(directory) + if identity in gitdir_owners: + if gitdir_owners[identity] != owner: + raise InventoryError("Git metadata directory does not own selected worktree") + return True + gitdir_owners[identity] = owner + + roots = [gitdir] + common_marker = gitdir / "commondir" + common_metadata = inspect_metadata(common_marker, directory=False) + if common_metadata is not None: + try: + common = Path(os.fsdecode(common_marker.read_bytes().rstrip(b"\r\n"))) + except (OSError, ValueError) as error: + raise InventoryError(f"could not inspect Git metadata: {directory}") from error + if not common.is_absolute(): + common = gitdir / common + inspected_common = inspect_metadata_path(common, directory_path=True) + if inspected_common is None: + raise InventoryError("Git common directory does not own selected worktree") + common = inspected_common + owner = inspect_metadata_path( + common / "worktrees" / gitdir.name, + directory_path=True, + ) + if owner is None or not same_filesystem_path(owner, gitdir): + raise InventoryError("Git common directory does not own selected worktree") + roots.append(common) + + def config_value(value: str | None) -> str | None: + if value is None: + return None + quoted = False + escaped = False + for index, character in enumerate(value): + if escaped: + escaped = False + elif character == "\\": + escaped = True + elif character == '"': + quoted = not quoted + elif character in "#;" and not quoted: + return value[:index].rstrip(" \t\r") + return value.rstrip(" \t\r") + + def join_config_lines(contents: bytes) -> bytes: + joined = bytearray() + quoted = False + comment = False + escaped = False + position = 0 + while position < len(contents): + character = contents[position] + if character == ord("\n"): + quoted = False + comment = False + escaped = False + elif not comment and not escaped and character == ord("\\"): + if contents[position + 1 : position + 2] == b"\n": + position += 2 + continue + if contents[position + 1 : position + 3] == b"\r\n": + position += 3 + continue + escaped = True + elif escaped: + escaped = False + elif not comment and character == ord('"'): + quoted = not quoted + elif not quoted and character in (ord("#"), ord(";")): + comment = True + joined.append(character) + position += 1 + return bytes(joined) + + def decode_config_value(value: str) -> tuple[bytes, bytes]: + decoded = bytearray() + normalized = bytearray() + quoted = False + escaped = False + replacements = {ord("n"): ord("\n"), ord("t"): ord("\t"), ord("b"): ord("\b")} + for character in os.fsencode(value): + if escaped: + if character not in (*replacements, ord('"'), ord("\\")): + raise InventoryError("invalid Git worktree path") + replacement = replacements.get(character, character) + decoded.append(replacement) + normalized.append(replacement) + escaped = False + elif character == ord("\\"): + escaped = True + elif character == ord('"'): + quoted = not quoted + else: + decoded.append(character) + normalized.append( + ord(" ") + if character in (ord("\t"), ord("\r")) and not quoted + else character + ) + if quoted or escaped: + raise InventoryError("invalid Git worktree path") + return bytes(decoded), bytes(normalized) + + def config_enabled(value: str | None) -> bool: + if value is None: + return True + normalized = os.fsdecode(decode_config_value(value)[0]).strip(" \t\r").casefold() + return normalized not in ("", "false", "no", "off") and re.fullmatch( + r"[+-]?(?:0+|0x0+)[kmg]?", normalized + ) is None + + options: dict[tuple[str, str], str | None] = {} + config_path = roots[-1] / "config" + worktree_config_enabled = False + config_includes = False + if inspect_metadata(config_path, directory=False) is not None: + try: + for candidate in (config_path, gitdir / "config.worktree"): + if candidate != config_path: + worktree_config_enabled = config_enabled( + options.get(("extensions", "worktreeconfig"), "false") + ) + if not worktree_config_enabled: + continue + if inspect_metadata(candidate, directory=False) is None: + continue + contents = candidate.read_bytes().removeprefix(codecs.BOM_UTF8) + contents = join_config_lines(contents) + section = None + for raw in os.fsdecode(contents).split("\n"): + line = raw.lstrip(" \t\r").rstrip("\r") + if not line or line.startswith(("#", ";")): + continue + while line.startswith("["): + match = re.match( + r'\[[ \t]*([a-z0-9-]*)' + r'(?:([.][^\]\r\n]*)|[ \t\r]+("(?:[^"\\]|\\.)*"))?' + r'[ \t]*\]', + line, + re.IGNORECASE, + ) + section = None + if match is None: + break + name = match.group(1).casefold() + if ( + name in ("core", "extensions", "include", "includeif") + and match.group(2) is None + and (match.group(3) is not None) == (name == "includeif") + ): + section = name + line = line[match.end() :].lstrip(" \t\r") + if section is None or not line or line.startswith(("#", ";")): + continue + assignment = re.match( + r"([a-z][a-z0-9-]*)(?:([ \t]*=)[ \t\r]*(.*))?", + line, + re.IGNORECASE, + ) + if assignment is None: + continue + if assignment.group(2) is None: + remainder = line[assignment.end() :].lstrip(" \t") + if remainder and not remainder.startswith(("#", ";")): + continue + key = assignment.group(1).casefold() + if section in ("include", "includeif") and key == "path": + config_includes = True + if (section, key) in ( + ("core", "worktree"), + ("core", "sparsecheckout"), + ("extensions", "objectformat"), + ("extensions", "worktreeconfig"), + ): + if (section, key) == ("extensions", "objectformat") and candidate != config_path: + continue + options[(section, key)] = ( + None + if assignment.group(2) is None + else config_value(assignment.group(3)) + ) + except (OSError, UnicodeError, ValueError) as error: + raise InventoryError(f"could not inspect Git metadata: {directory}") from error + if config_includes: + raise InventoryError("Git config includes are not supported") + sparse_checkout_enabled = config_enabled( + options.get(("core", "sparsecheckout"), "false") + ) + + configured_worktree = options.get(("core", "worktree")) + if gitfile: + if configured_worktree is None: + if not backpointer_owned: + raise InventoryError("Git metadata directory does not own selected worktree") + else: + decoded, normalized = decode_config_value(configured_worktree) + configured_worktree = os.fsdecode(decoded) + target = Path(configured_worktree) + if not target.is_absolute(): + target = gitdir / target + inspected_target = inspect_metadata_path(target, directory_path=True) + if inspected_target is None: + raise InventoryError("Git metadata directory does not own selected worktree") + if decoded != normalized: + normalized_target = Path(os.fsdecode(normalized)) + if not normalized_target.is_absolute(): + normalized_target = gitdir / normalized_target + inspected_normalized = inspect_metadata_path( + normalized_target, + directory_path=True, + ) + if inspected_normalized is None: + raise InventoryError( + "Git metadata directory does not own selected worktree" + ) + if directory_identity(inspected_normalized) != directory_identity(directory): + raise InventoryError( + "Git metadata directory does not own selected worktree" + ) + if not same_filesystem_path(inspected_target, directory): + raise InventoryError("Git metadata directory does not own selected worktree") + + for root in roots: + for relative in ( + "HEAD", + "index", + "packed-refs", + "refs", + "refs/heads", + "refs/tags", + "config", + "config.worktree", + "info", + "info/exclude", + "info/sparse-checkout", + "objects", + "objects/info", + "objects/info/alternates", + ): + effective_root = ( + gitdir + if relative in ("HEAD", "index", "config.worktree", "info/sparse-checkout") + else roots[-1] + ) + if ( + root != effective_root + or relative == "config.worktree" and not worktree_config_enabled + or relative == "info/sparse-checkout" and not sparse_checkout_enabled + ): + continue + if relative == "info/sparse-checkout" and root != roots[-1]: + inspect_metadata(root / "info", directory=True) + path = root / relative + metadata = inspect_metadata( + path, + directory=relative in ( + "refs", + "refs/heads", + "refs/tags", + "info", + "objects", + "objects/info", + ), + ) + if metadata is not None and relative == "objects": + inspect_object_store(path) + if metadata is not None and relative == "objects/info/alternates": + try: + contents = path.read_bytes() + except OSError as error: + raise InventoryError(f"could not inspect Git metadata: {directory}") from error + pending = [(root / "objects", contents)] + inspected = {directory_identity(root / "objects")} + while pending: + object_root, records = pending.pop() + lines = records.split(b"\n") + for index, record in enumerate(lines): + terminated = index + 1 < len(lines) + if not record or (terminated and record == b"\r"): + continue + if record.startswith(b'"'): + variants = (record.removesuffix(b"\r"),) + elif terminated and record.endswith(b"\r"): + variants = (record.removesuffix(b"\r"),) + if os.name != "nt": + variants += (record,) + else: + variants = (record,) + validated = False + for line in variants: + if line.startswith(b'"'): + if not line.endswith(b'"'): + raise InventoryError("invalid Git object alternate paths") + quoted = line[1:-1] + if not re.fullmatch( + rb'(?:[^"\\]|\\(?:["\\abfnrtv]|[0-3][0-7]{2}))*', quoted + ): + raise InventoryError("invalid Git object alternate paths") + try: + line = codecs.escape_decode(quoted)[0] + except (ValueError, UnicodeError) as error: + raise InventoryError( + "invalid Git object alternate paths" + ) from error + alternate = Path(os.fsdecode(line)) + if not alternate.is_absolute(): + alternate = object_root / alternate + owner = None + anchor = alternate + for candidate in (repository, *roots): + if len(alternate.parts) < len(candidate.parts): + continue + candidate_anchor = inspect_metadata_path( + Path(*alternate.parts[: len(candidate.parts)]), + directory_path=True, + ) + if candidate_anchor is None: + continue + if directory_identity(candidate_anchor) != directory_identity(candidate): + continue + owner = candidate + anchor = candidate_anchor + break + if owner is None: + raise InventoryError( + "external Git object alternates are not supported" + ) + current = anchor + depth = 0 + for component in alternate.parts[len(anchor.parts) :]: + if component == "..": + if depth == 0: + raise InventoryError( + "external Git object alternates are not supported" + ) + current = current.parent + depth -= 1 + continue + current /= component + if inspect_metadata(current, directory=True) is None: + break + depth += 1 + else: + validated = True + alternate = current + identity = directory_identity(alternate) + if identity in inspected: + continue + inspected.add(identity) + inspect_object_store(alternate) + info = alternate / "info" + if inspect_metadata(info, directory=True) is None: + continue + nested_alternates = info / "alternates" + if inspect_metadata(nested_alternates, directory=False) is None: + continue + try: + records = nested_alternates.read_bytes() + except OSError as error: + raise InventoryError( + f"could not inspect Git metadata: {directory}" + ) from error + pending.append((alternate, records)) + if not validated: + raise InventoryError( + "missing Git object alternates are not supported" + ) + if root != gitdir: + continue + try: + configured_format = options.get(("extensions", "objectformat"), "sha1") + object_format = ( + "sha1" + if configured_format is None + else os.fsdecode(decode_config_value(configured_format)[0]).strip(" \t\r").casefold() + ) + backing = split_index_backing(root / "index", 32 if object_format == "sha256" else 20) + if backing is not None: + inspect_metadata(root / f"sharedindex.{backing}", directory=False) + except FileNotFoundError: + continue + except OSError as error: + raise InventoryError(f"could not inspect Git metadata: {directory}") from error + return True + + discovered_roots: dict[tuple[int, int], Path] = {} + metadata_aliases: set[tuple[str, ...]] = set() + for ancestor in ancestors: + reject_symbolic_ignore(ancestor) + has_git_marker(ancestor) + command = [ "rg", + "--no-config", "--files", + "--null", "--hidden", - "--no-ignore", "--path-separator", "/", + "--no-require-git", + "--no-ignore-parent", + "--no-ignore-global", + "--glob", + "!.git", "--glob", "!.git/**", - "--", - scope, ] - with tempfile.TemporaryFile(mode="w+b") as inventory: + + def ripgrep_inventory( + directory: Path, requested_scope: str, *, directory_guard: bool = False + ) -> set[bytes]: + arguments = command.copy() + directory_parts = directory.relative_to(repository).parts + ignored_aliases = [] + for alias in sorted(metadata_aliases): + if alias[: len(directory_parts)] != directory_parts: + continue + relative_alias = "/".join(re.escape(part) for part in alias[len(directory_parts) :]) + if relative_alias and "\n" not in relative_alias and "\r" not in relative_alias: + ignored_aliases.append(relative_alias) + if directory_guard: + relative_scope = requested_scope.removeprefix("./") + arguments.extend(["--quiet", "--glob", f"/{re.escape(relative_scope)}/**"]) + for alias in ignored_aliases: + arguments.extend(["--glob", f"!/{alias}", "--glob", f"!/{alias}/**"]) + arguments.extend(["--", "." if directory_guard else requested_scope]) + with tempfile.TemporaryFile(mode="w+b") as inventory: + try: + result = subprocess.run( + arguments, + cwd=directory, + stdout=inventory, + stderr=subprocess.PIPE, + check=False, + ) + except OSError as error: + raise InventoryError(f"could not run ripgrep: {error}") from error + + if result.returncode not in (0, 1): + detail = result.stderr.decode("utf-8", errors="replace").strip() + message = f"ripgrep exited with status {result.returncode}" + if detail: + message = f"{message}: {detail}" + raise InventoryError(message) + + if directory_guard: + return {b""} if result.returncode == 0 else set() + inventory.seek(0) + rows = set() + + def inventory_paths() -> Iterator[bytes]: + remainder = b"" + while chunk := inventory.read(io.DEFAULT_BUFFER_SIZE): + paths = chunk.split(b"\0") + paths[0] = remainder + paths[0] + remainder = paths.pop() + yield from paths + if remainder: + raise InventoryError("ripgrep returned an unterminated inventory path") + + for path in inventory_paths(): + if not path: + continue + if b"\n" in path or b"\r" in path: + raise InventoryError("line separators are not supported in inventory paths") + row = path + b"\n" + if not metadata_aliases: + rows.add(row) + continue + parts = ( + *directory_parts, + *Path(os.fsdecode(row.removesuffix(b"\n"))).parts, + ) + if not any(parts[: len(alias)] == alias for alias in metadata_aliases): + rows.add(row) + return rows + + def normalized(path: bytes) -> bytes: + return path.replace(b"\\", b"/") if os.name == "nt" else path + + def visible_to_outer_ignores( + root: Path, + candidates: list[Path], + *, + directories_only: bool = False, + exempt_gitignores: tuple[tuple[Path, Path], ...] = (), + preserve_gitignore_descendants: bool = False, + ) -> set[bytes]: + requested = { + normalized(os.fsencode(candidate.relative_to(repository).as_posix())) + for candidate in candidates + } + if directories_only and any(b"\n" in path or b"\r" in path for path in requested): + raise InventoryError("line separators are not supported in inventory paths") + directories: list[Path] = [] + current = root.parent + while True: + directories.append(current) + if current == repository: + break + current = current.parent + for directory in directories: + reject_symbolic_ignore(directory) + ignore_files = [ + directory / name + for directory in directories + for name in IGNORE_FILE_NAMES + if name != ".gitignore" + or preserve_gitignore_descendants + or not any( + directory.is_relative_to(owner) + and gitlink.is_relative_to(directory) + and directory != gitlink + for owner, gitlink in exempt_gitignores + ) + if (directory / name).is_file() + ] + configured_excludes: dict[Path, bytes] = {} + for directory in directories: + if not has_git_marker(directory): + continue + location = run_git( + ["rev-parse", "--path-format=absolute", "--git-path", "info/exclude"], + directory=directory, + ) + if location.returncode == 0: + exclude = Path(os.fsdecode(location.stdout.rstrip(b"\r\n"))) + if exclude.is_file(): + contents = exclude.read_bytes() + if any( + line.strip() and not line.startswith(b"#") + for line in contents.splitlines() + ): + configured_excludes[directory] = contents + if not ignore_files and not configured_excludes: + return requested + + batches: list[tuple[dict[str, str], set[bytes]]] = [] + + for relative in requested: + parts = PurePosixPath(os.fsdecode(relative)).parts + prefixes = { + filesystem_name_key("/".join(parts[: index + 1])): "/".join(parts[: index + 1]) + for index in range(len(parts)) + } + for names, batch in batches: + if all(names.get(folded, spelling) == spelling for folded, spelling in prefixes.items()): + names.update(prefixes) + batch.add(relative) + break + else: + batches.append((prefixes, {relative})) + + visible: set[bytes] = set() + for _, batch in batches: + with tempfile.TemporaryDirectory() as temporary_directory: + temporary_root = Path(temporary_directory) + probe = temporary_root / "inventory" + probe.mkdir() + external_ignores: list[tuple[int, int, int, Path]] = [] + + def collides_with_candidates(relative: tuple[str, ...]) -> bool: + for candidate in batch: + pairs = tuple( + zip(PurePosixPath(os.fsdecode(candidate)).parts, relative) + ) + if all( + filesystem_name_key(actual) == filesystem_name_key(synthetic) + for actual, synthetic in pairs + ) and any(actual != synthetic for actual, synthetic in pairs): + return True + return False + + isolate_ignores = any( + collides_with_candidates( + (*ignore.parent.relative_to(repository).parts, ignore.name) + ) + for ignore in ignore_files + ) or any( + collides_with_candidates( + (*directory.relative_to(repository).parts, name) + ) + for directory in configured_excludes + for name in (".gitignore", ".ignore") + ) + + def install_ignore( + directory: Path, + name: str, + contents: bytes, + *, + prepend: bool = False, + ) -> None: + relative = (*directory.relative_to(repository).parts, name) + if isolate_ignores: + if directory != repository: + prefix = os.fsencode( + "/".join( + re.escape(part) + for part in directory.relative_to(repository).parts + ) + ) + rebased = [] + lines = contents.removeprefix(b"\xef\xbb\xbf").split(b"\n") + for index, line in enumerate(lines): + terminated = index < len(lines) - 1 + if terminated: + line = line.removesuffix(b"\r") + if not line or line.startswith(b"#"): + continue + negated = line.startswith(b"!") + pattern = line[1:] if negated else line + if not pattern.rstrip(b" ").strip(b"/"): + continue + if pattern.startswith(b"/"): + pattern = pattern[1:] + elif b"/" not in pattern.rstrip(b"/"): + pattern = b"**/" + pattern + rebased.append( + (b"!" if negated else b"") + + b"/" + + prefix + + b"/" + + pattern + + (b"\n" if terminated else b"") + ) + contents = b"".join(rebased) + position = len(external_ignores) + destination = temporary_root / f"ignore-{position}" + destination.write_bytes(contents) + priority = -1 if prepend else IGNORE_FILE_NAMES.index(name) + depth = len(directory.relative_to(repository).parts) + external_ignores.append((priority, depth, position, destination)) + return + + destination = probe.joinpath(*relative) + destination.parent.mkdir(parents=True, exist_ok=True) + if prepend and destination.exists(): + existing = destination.read_bytes() + separator = b"" if not contents or contents.endswith(b"\n") else b"\n" + contents += separator + existing + destination.write_bytes(contents) + + def admit_gitlink_directories(directory: Path, contents: bytes) -> bytes: + if not preserve_gitignore_descendants: + return contents + for owner, selected_root in exempt_gitignores: + if not ( + directory.is_relative_to(owner) + and selected_root.is_relative_to(directory) + and directory != selected_root + ): + continue + parts = selected_root.relative_to(directory).parts + for index in range(len(parts)): + if contents and not contents.endswith(b"\n"): + contents += b"\n" + admitted = "/".join(re.escape(part) for part in parts[: index + 1]) + contents += os.fsencode(f"!/{admitted}/\n") + return contents + + for ignore in ignore_files: + contents = ignore.read_bytes() + if ignore.name == ".gitignore": + contents = admit_gitlink_directories(ignore.parent, contents) + install_ignore(ignore.parent, ignore.name, contents) + for directory, contents in configured_excludes.items(): + contents = admit_gitlink_directories(directory, contents) + install_ignore(directory, ".gitignore", contents, prepend=True) + for relative in batch: + destination = probe / os.fsdecode(relative) + if directories_only: + destination.mkdir(parents=True, exist_ok=True) + else: + destination.parent.mkdir(parents=True, exist_ok=True) + destination.touch() + result = subprocess.run( + [ + *command, + *( + argument + for _, _, _, ignore in sorted(external_ignores) + for argument in ("--ignore-file", str(ignore)) + ), + *(["--debug"] if directories_only else []), + "--", + ".", + ], + cwd=probe, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + if result.returncode not in (0, 1): + detail = result.stderr.decode("utf-8", errors="replace").strip() + raise InventoryError(f"could not evaluate outer ignore rules: {detail}") + if directories_only: + ignored = { + normalized(match.group(1)).removeprefix(b"./") + for line in result.stderr.splitlines() + if (match := re.search(rb": ignoring (.+): Ignore\(", line)) is not None + } + visible.update( + relative + for relative in batch + if not any( + relative == excluded or relative.startswith(excluded + b"/") + for excluded in ignored + ) + ) + else: + visible.update( + normalized(relative).removeprefix(b"./") + for relative in result.stdout.split(b"\0") + if normalized(relative).removeprefix(b"./") in batch + ) + return visible + + environment = os.environ.copy() + for name in ( + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + "GIT_CEILING_DIRECTORIES", + "GIT_COMMON_DIR", + "GIT_DIR", + "GIT_DISCOVERY_ACROSS_FILESYSTEM", + "GIT_INDEX_FILE", + "GIT_ICASE_PATHSPECS", + "GIT_GLOB_PATHSPECS", + "GIT_NAMESPACE", + "GIT_NOGLOB_PATHSPECS", + "GIT_OBJECT_DIRECTORY", + "GIT_WORK_TREE", + ): + environment.pop(name, None) + environment["GIT_LITERAL_PATHSPECS"] = "1" + environment["GIT_NO_LAZY_FETCH"] = "1" + environment["GIT_NO_REPLACE_OBJECTS"] = "1" + environment["LC_ALL"] = "C" + git = [ + "git", + "-c", + "core.fsmonitor=false", + "-c", + f"core.excludesFile={os.devnull}", + "--literal-pathspecs", + ] + + def run_git( + arguments: list[str], *, directory: Path = repository, literal: bool = True + ) -> subprocess.CompletedProcess[bytes]: + command = git if literal else git[:-1] + git_environment = environment if literal else environment.copy() + if not literal: + git_environment.pop("GIT_LITERAL_PATHSPECS", None) try: - result = subprocess.run( - command, - cwd=repository, - stdout=inventory, + return subprocess.run( + [*command, f"--work-tree={directory}", *arguments], + cwd=directory, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=git_environment, check=False, ) except OSError as error: - raise InventoryError(f"could not run ripgrep: {error}") from error + raise InventoryError(f"could not run Git: {error}") from error + + def resolve_git_root(value: bytes) -> Path: + root_path = value.removesuffix(b"\n") + if os.name == "nt": + root_path = root_path.removesuffix(b"\r") + return Path(os.fsdecode(root_path)).resolve(strict=True) - if result.returncode not in (0, 1): - detail = result.stderr.decode("utf-8", errors="replace").strip() - message = f"ripgrep exited with status {result.returncode}" + def owns_git_root(value: bytes, expected: Path) -> bool: + actual = resolve_git_root(value) + return directory_identity(actual) == directory_identity(expected) and ( + actual.relative_to(repository).parts == expected.relative_to(repository).parts + ) + + worktree = ( + run_git(["rev-parse", "--show-toplevel"]) + if has_git_marker(repository) + else None + ) + if worktree is not None and worktree.returncode: + detail = worktree.stderr.decode("utf-8", errors="replace").strip() + if any( + reason in detail.lower() + for reason in ( + "not a git repository", + "gitfile does not point to a valid repository", + "invalid gitfile format", + ) + ): + worktree = None + else: + message = f"git rev-parse exited with status {worktree.returncode}" if detail: message = f"{message}: {detail}" raise InventoryError(message) - inventory.seek(0) - rows = sorted(inventory) + if worktree is not None: + try: + valid_worktree = owns_git_root(worktree.stdout, repository) + except (OSError, ValueError) as error: + raise InventoryError(f"could not resolve Git worktree root: {error}") from error + if not valid_worktree: + worktree = None + + scoped_files: dict[Path, list[Path]] = {} + inspected_directories: set[tuple[int, int]] = set() + if selected.is_dir(): + pending = [selected] + while pending: + visibility_groups: dict[tuple[tuple[str, ...], ...], list[Path]] = {} + for directory in pending: + identity = directory_identity(directory) + if identity in inspected_directories: + continue + inspected_directories.add(identity) + reject_symbolic_ignore(directory) + entries = list(directory.iterdir()) + metadata_aliases.update( + entry.relative_to(repository).parts + for entry in entries + if entry.name != ".git" and git_metadata_path(directory, entry.name) + ) + children = [ + entry + for entry in entries + if nonsymbolic_directory(entry) + and not git_metadata_path(directory, entry.name) + ] + if scope not in (".", "./"): + scoped_files[directory] = [ + entry + for entry in entries + if not git_metadata_path(directory, entry.name) + and not entry.is_symlink() + and entry.is_file() + ] + if children: + context: list[tuple[str, ...]] = [] + current = directory + while True: + relative = current.relative_to(repository).parts + context.extend( + (*relative, name) + for name in IGNORE_FILE_NAMES + if (current / name).is_file() + ) + if has_git_marker(current): + context.append((*relative, ".git")) + if current == repository: + break + current = current.parent + visibility_groups.setdefault(tuple(context), []).extend(children) + pending = [] + for children in visibility_groups.values(): + visible = visible_to_outer_ignores(children[0], children, directories_only=True) + for entry in children: + if normalized(os.fsencode(entry.relative_to(repository).as_posix())) not in visible: + continue + if has_git_marker(entry): + candidate = run_git(["rev-parse", "--show-toplevel"], directory=entry) + if candidate.returncode == 0: + try: + if owns_git_root(candidate.stdout, entry): + discovered_roots[directory_identity(entry)] = entry + except (OSError, ValueError): + pass + pending.append(entry) + + rows = ripgrep_inventory(repository, scope) + visible_directories = set(ancestors) + for row in rows: + current = (repository / os.fsdecode(row.removesuffix(b"\n"))).parent + while current != repository: + visible_directories.add(current) + current = current.parent + for directory in sorted(visible_directories): + reject_symbolic_ignore(directory) + if directory != repository and has_git_marker(directory): + discovered_roots[directory_identity(directory)] = directory + if selected.is_dir() and scope not in (".", "./") and not ripgrep_inventory( + repository, scope, directory_guard=True + ): + rows.clear() + elif scope not in (".", "./"): + prefix = b"./" if scope.startswith("./") else b"" + for candidates in scoped_files.values(): + if not candidates: + continue + visible = visible_to_outer_ignores(candidates[0], candidates) + for candidate in candidates: + relative = normalized(os.fsencode(candidate.relative_to(repository).as_posix())) + if b"\n" in relative or b"\r" in relative: + raise InventoryError("line separators are not supported in inventory paths") + row = prefix + relative + b"\n" + if relative in visible: + rows.add(row) + else: + rows.discard(row) + + if worktree is not None or discovered_roots: + prefix = b"./" if scope == "." or scope.startswith("./") else b"" + listed: list[list[bytes]] = [[], []] + cached_by_root: dict[tuple[int, int], tuple[Path, list[bytes]]] = {} + + def validated_git_path(relative: bytes, root: Path = repository) -> bytes: + portable = normalized(relative) + components = portable.removesuffix(b"/").split(b"/") + path = Path(os.fsdecode(portable)) + if ( + path.is_absolute() + or path.drive + or not (root / path).is_relative_to(root) + or any(component in (b"", b".", b"..") for component in components) + ): + raise InventoryError("out-of-scope Git inventory paths are not supported") + current = root + for component in path.parts[:-1]: + current /= component + try: + metadata = current.stat(follow_symlinks=False) + except OSError: + break + if symbolic_metadata(metadata): + raise InventoryError("symbolic Git inventory paths are not supported") + if not stat.S_ISDIR(metadata.st_mode): + break + return relative + + def listed_paths(index: int) -> Iterator[bytes]: + for chunk in listed[index]: + for relative in chunk.split(b"\0"): + if not relative: + continue + if b"\n" in relative or b"\r" in relative: + raise InventoryError("line separators are not supported in inventory paths") + yield validated_git_path(relative) + + if worktree is not None: + for index, arguments in enumerate( + (["--cached"], ["--others", "--exclude-standard"]) + ): + result = run_git(["ls-files", "--sparse", *arguments, "-z", "--", scope]) + if result.returncode: + detail = result.stderr.decode("utf-8", errors="replace").strip() + message = f"git ls-files exited with status {result.returncode}" + if detail: + message = f"{message}: {detail}" + raise InventoryError(message) + listed[index].append(result.stdout) + if index == 0: + cached_by_root[directory_identity(repository)] = ( + repository, + [relative for relative in result.stdout.split(b"\0") if relative], + ) + + def visible_nested_root(root: Path) -> bool: + selected_parts = selected.relative_to(repository).parts + root_parts = root.relative_to(repository).parts + shared_depth = min(len(selected_parts), len(root_parts)) + if selected_parts[:shared_depth] != root_parts[:shared_depth]: + return False + selected_ancestor = ( + selected + if len(selected_parts) == shared_depth + else selected.parents[len(selected_parts) - shared_depth - 1] + ) + root_ancestor = ( + root + if len(root_parts) == shared_depth + else root.parents[len(root_parts) - shared_depth - 1] + ) + return directory_identity(selected_ancestor) == directory_identity(root_ancestor) + + nested_roots = { + identity: root + for identity, root in discovered_roots.items() + if visible_nested_root(root) + } + current = selected if selected.is_dir() else selected.parent + while current != repository: + if has_git_marker(current): + nested_roots[directory_identity(current)] = current + current = current.parent + for index in range(len(listed)): + for relative in listed_paths(index): + candidate = repository / os.fsdecode(relative) + current = candidate if nonsymbolic_directory(candidate) else candidate.parent + while current != repository: + if index != 0 and directory_identity(current) not in inspected_directories: + current = current.parent + continue + if nonsymbolic_directory(current) and has_git_marker(current): + try: + discovered = current.resolve(strict=True) + discovered.relative_to(repository) + except (OSError, ValueError): + break + if index == 0 or visible_nested_root(discovered): + nested_roots[directory_identity(discovered)] = discovered + current = current.parent + + pending_roots = sorted(nested_roots.values()) + inspected_roots: dict[tuple[int, int], Path] = {} + while pending_roots: + nested = pending_roots.pop(0) + nested_identity = directory_identity(nested) + if nested_identity in inspected_roots: + continue + nested_worktree = run_git( + ["rev-parse", "--show-toplevel"], directory=nested + ) + if nested_worktree.returncode: + detail = nested_worktree.stderr.decode("utf-8", errors="replace").strip() + if any( + reason in detail.lower() + for reason in ( + "not a git repository", + "gitfile does not point to a valid repository", + "invalid gitfile format", + ) + ): + continue + raise InventoryError( + f"nested git rev-parse exited with status {nested_worktree.returncode}: {detail}" + ) + try: + if not owns_git_root(nested_worktree.stdout, nested): + continue + except (OSError, ValueError): + continue + inspected_roots[nested_identity] = nested + try: + nested_scope = selected.relative_to(nested).as_posix() or "." + except ValueError: + nested_scope = "." + nested_prefix = os.fsencode(nested.relative_to(repository).as_posix()) + b"/" + for index, arguments in enumerate( + (["--cached"], ["--others", "--exclude-standard"]) + ): + result = run_git( + ["ls-files", "--sparse", *arguments, "-z", "--", nested_scope], + directory=nested, + ) + if result.returncode: + detail = result.stderr.decode("utf-8", errors="replace").strip() + raise InventoryError( + f"nested git ls-files exited with status {result.returncode}: {detail}" + ) + listed[index].append( + b"".join( + nested_prefix + relative + b"\0" + for relative in result.stdout.split(b"\0") + if relative + ) + ) + if index == 0: + cached_by_root[nested_identity] = ( + nested, + [relative for relative in result.stdout.split(b"\0") if relative], + ) + for relative in result.stdout.split(b"\0"): + if not relative: + continue + validated_git_path(relative, nested) + candidate = nested / os.fsdecode(relative) + if not nonsymbolic_directory(candidate): + continue + if not has_git_marker(candidate): + continue + try: + discovered = candidate.resolve(strict=True) + discovered.relative_to(repository) + except (OSError, ValueError): + continue + if directory_identity(discovered) not in inspected_roots: + pending_roots.append(discovered) + + if scope not in (".", "./"): + for identity, (root, _) in list(cached_by_root.items()): + tracked = run_git(["ls-files", "--sparse", "--cached", "-z"], directory=root) + if tracked.returncode: + detail = tracked.stderr.decode("utf-8", errors="replace").strip() + raise InventoryError( + f"git ls-files exited with status {tracked.returncode}: {detail}" + ) + cached_by_root[identity] = ( + root, + [relative for relative in tracked.stdout.split(b"\0") if relative], + ) + recorded = {normalized(row.removesuffix(b"\n")) for row in rows} + directory_entries: dict[tuple[int, int], dict[bytes, list[Path]]] = {} + + def indexed_name_key(value: str) -> bytes: + return os.fsencode(filesystem_name_key(value)) + + selected_parts = tuple( + os.fsencode(part) for part in selected.relative_to(repository).parts + ) + selected_is_directory = selected.is_dir() + + def exact_descendant(candidate: Path, parent: Path) -> bool: + candidate_parts = candidate.relative_to(repository).parts + parent_parts = parent.relative_to(repository).parts + return candidate_parts[: len(parent_parts)] == parent_parts + + def tracked_gitlink(owner: Path, nested: Path, indexed_paths: set[bytes]) -> bool: + relative = nested.relative_to(owner) + if os.fsencode(relative.as_posix()) in indexed_paths: + return True + for indexed in indexed_paths: + components = PurePosixPath(os.fsdecode(indexed)).parts + if len(components) != len(relative.parts): + continue + parent = owner + for indexed_name, materialized in zip(components, relative.parts): + if indexed_name != materialized: + try: + expected = (parent / indexed_name).stat(follow_symlinks=False) + actual = (parent / materialized).stat(follow_symlinks=False) + except OSError: + break + if symbolic_metadata(expected) or symbolic_metadata(actual) or ( + expected.st_dev, + expected.st_ino, + ) != (actual.st_dev, actual.st_ino): + break + parent /= materialized + else: + return True + return False + + tracked_gitlinks = [] + for owner, _tracked_paths in cached_by_root.values(): + staged = run_git(["ls-files", "--sparse", "--stage", "-z"], directory=owner) + if staged.returncode: + detail = staged.stderr.decode("utf-8", errors="replace").strip() + raise InventoryError(f"git ls-files --stage exited with status {staged.returncode}: {detail}") + indexed_paths = { + path + for record in staged.stdout.split(b"\0") + if record + and (parts := record.partition(b"\t"))[1] + and (header := parts[0].split()) + and len(header) == 3 + and header[0] == b"160000" + and header[2] in (b"0", b"1", b"2", b"3") + for path in (parts[2],) + } + tracked_gitlinks.extend( + (owner, nested) + for nested in inspected_roots.values() + if nested != owner + and exact_descendant(nested, owner) + and tracked_gitlink(owner, nested, indexed_paths) + ) + def tracked_variants(root: Path, relative: bytes) -> Iterator[Path]: + components = PurePosixPath(os.fsdecode(relative)).parts + if not components or any(part in (".", "..") for part in components): + return + root_parts = tuple(os.fsencode(part) for part in root.relative_to(repository).parts) + indexed_parts = root_parts + tuple(os.fsencode(part) for part in components) + if (not selected_is_directory and len(indexed_parts) != len(selected_parts)) or ( + selected_is_directory and len(indexed_parts) <= len(selected_parts) + ): + return + for index, requested in enumerate(selected_parts): + indexed = indexed_parts[index] + if indexed == requested: + continue + if index < len(root_parts): + return + + def descend(parent: Path, index: int) -> list[Path]: + try: + parent_identity = directory_identity(parent) + except OSError: + return [] + if parent_identity not in directory_entries: + grouped: dict[bytes, list[Path]] = {} + try: + with os.scandir(parent) as entries: + for entry in entries: + if not git_metadata_path(parent, entry.name): + grouped.setdefault(indexed_name_key(entry.name), []).append( + parent / entry.name + ) + except OSError: + return [] + directory_entries[parent_identity] = grouped + component = components[index] + variants = directory_entries[parent_identity].get( + indexed_name_key(component), [] + ) + if not variants: + try: + expected = (parent / component).stat(follow_symlinks=False) + except OSError: + return [] + if symbolic_metadata(expected): + return [] + try: + addressed = (parent / component).resolve(strict=True) + except OSError: + return [] + variants = [ + candidate + for candidate in directory_entries[parent_identity].get( + indexed_name_key(addressed.name), [] + ) + if candidate.name == addressed.name + ] + selected_index = len(root_parts) + index + if selected_index < len(selected_parts): + requested = os.fsdecode(selected_parts[selected_index]) + variants = [candidate for candidate in variants if candidate.name == requested] + exact = [candidate for candidate in variants if candidate.name == component] + alternatives = [ + candidate + for candidate in variants + if candidate.name != component + ] + for group in (exact, alternatives): + matches: list[Path] = [] + for candidate in group: + try: + metadata = candidate.stat(follow_symlinks=False) + except OSError: + continue + if symbolic_metadata(metadata): + continue + if candidate.name != component: + try: + expected = (parent / component).stat(follow_symlinks=False) + except OSError: + continue + if symbolic_metadata(expected) or ( + metadata.st_dev, + metadata.st_ino, + ) != (expected.st_dev, expected.st_ino): + continue + if index + 1 < len(components): + if not stat.S_ISDIR(metadata.st_mode): + continue + matches.extend(descend(candidate, index + 1)) + elif stat.S_ISREG(metadata.st_mode): + matches.append(candidate) + if matches: + return matches + return [] + + for candidate in descend(root, 0): + try: + resolved = candidate.resolve(strict=True) + resolved.relative_to(repository) + except (OSError, ValueError): + continue + candidate_parts = tuple( + os.fsencode(part) for part in candidate.relative_to(repository).parts + ) + if selected_is_directory: + if candidate_parts[: len(selected_parts)] != selected_parts: + continue + elif candidate_parts != selected_parts: + continue + yield candidate + + for root, tracked_paths in cached_by_root.values(): + candidates = [ + candidate + for relative in tracked_paths + for candidate in tracked_variants(root, relative) + ] + outer_visible = ( + visible_to_outer_ignores(root, candidates) + if root != repository and selected_is_directory + else None + ) + gitlink_groups: dict[tuple[tuple[Path, Path], ...], list[Path]] = {} + scope_exemptions: tuple[tuple[Path, Path], ...] = () + if outer_visible is not None: + if selected != repository and ( + selected == root or exact_descendant(selected, root) + ): + selected_path = normalized( + os.fsencode(selected.relative_to(repository).as_posix()) + ) + selected_visible = visible_to_outer_ignores( + selected, [selected], directories_only=True + ) + candidate_exemption = ((repository, selected),) + if selected_path not in selected_visible and selected_path in visible_to_outer_ignores( + selected, + [selected], + directories_only=True, + exempt_gitignores=candidate_exemption, + ): + scope_exemptions = candidate_exemption + for candidate in candidates: + exemptions = tuple( + (owner, gitlink) + for owner, gitlink in tracked_gitlinks + if exact_descendant(candidate, gitlink) + ) + exemptions += scope_exemptions + if exemptions: + gitlink_groups.setdefault(exemptions, []).append(candidate) + gitlink_visible = { + relative + for exemptions, linked_candidates in gitlink_groups.items() + for relative in visible_to_outer_ignores( + root, + linked_candidates, + exempt_gitignores=exemptions, + preserve_gitignore_descendants=True, + ) + } + for candidate in candidates: + relative = os.fsencode(candidate.relative_to(repository).as_posix()) + if ( + outer_visible is not None + and normalized(relative) not in outer_visible + and normalized(relative) not in gitlink_visible + ): + continue + relative_path = prefix + relative + key = normalized(relative_path) + if key not in recorded: + rows.add(relative_path + b"\n") + recorded.add(key) + + rows = sorted(rows) return write_inventory(output, rows) diff --git a/sdk/typescript/tests-ts/runtime.test.ts b/sdk/typescript/tests-ts/runtime.test.ts index a4780903..7de1a21e 100644 --- a/sdk/typescript/tests-ts/runtime.test.ts +++ b/sdk/typescript/tests-ts/runtime.test.ts @@ -228,7 +228,8 @@ describe("plugin runtime preparation", () => { join(PLUGIN_ROOT, "scripts", "generate_in_scope_files.py"), "utf8", ); - expect(generator).toContain('"--no-ignore"'); + expect(generator).not.toContain('"--no-ignore"'); + expect(generator).toContain('"--cached"'); expect(generator).toContain('"--path-separator"'); return; } diff --git a/sdk/typescript/tests-ts/scan-inventory.test.ts b/sdk/typescript/tests-ts/scan-inventory.test.ts new file mode 100644 index 00000000..343b5ff0 --- /dev/null +++ b/sdk/typescript/tests-ts/scan-inventory.test.ts @@ -0,0 +1,3385 @@ +import { execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + chmod, + link as hardlink, + mkdir, + mkdtemp, + readdir, + readFile, + realpath, + rename, + rm, + symlink, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { basename, dirname, join } from "node:path"; +import { afterEach, describe, expect, test } from "bun:test"; +import { normalizeTarget } from "../src/targets.js"; +import { PLUGIN_ROOT } from "./plugin-root.js"; + +const directories: string[] = []; +const python = Bun.which("python3") ?? Bun.which("python") ?? Bun.which("py"); + +afterEach(async () => { + await Promise.all( + directories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +async function repository(initializeGit = true): Promise { + const root = await realpath( + await mkdtemp(join(tmpdir(), "codex-security-scan-inventory-")), + ); + directories.push(root); + const checkout = join(root, "repository"); + await mkdir(checkout); + if (initializeGit) execFileSync("git", ["init", "-q"], { cwd: checkout }); + return checkout; +} + +function commit(checkout: string): void { + execFileSync( + "git", + [ + "-c", + "user.name=Inventory Test", + "-c", + "user.email=inventory@example.test", + "commit", + "-qm", + "Track source", + ], + { cwd: checkout }, + ); +} + +function windowsShortPath(path: string): string | null { + if (process.platform !== "win32" || python === null) return null; + const alias = execFileSync( + python, + [ + "-B", + "-c", + [ + "import ctypes, sys", + "function = ctypes.windll.kernel32.GetShortPathNameW", + "size = function(sys.argv[1], None, 0)", + "buffer = ctypes.create_unicode_buffer(size) if size else None", + "print(buffer.value if buffer is not None and function(sys.argv[1], buffer, size) else '')", + ].join("\n"), + path, + ], + { encoding: "utf8" }, + ).trim(); + return alias && alias.toLowerCase() !== path.toLowerCase() ? alias : null; +} + +async function inventory( + checkout: string, + scope = ".", + env: NodeJS.ProcessEnv = process.env, +): Promise { + if (python === null) throw new Error("A Python interpreter is required."); + const output = join(dirname(checkout), "inventory.txt"); + execFileSync( + python, + [ + "-B", + join(PLUGIN_ROOT, "scripts", "generate_in_scope_files.py"), + "--repo", + checkout, + "--scope", + scope, + "--out", + output, + ], + { cwd: checkout, env, stdio: "pipe" }, + ); + return (await readFile(output, "utf8")).trimEnd().split("\n").filter(Boolean); +} + +describe("security scan file inventory", () => { + test("keeps tracked source while excluding ignored untracked files", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + await mkdir(join(checkout, "ignored")); + await mkdir(join(checkout, "src")); + await Promise.all([ + writeFile(join(checkout, ".gitignore"), ".env\nignored/\ntracked.env\n"), + writeFile(join(checkout, ".ignore"), "hidden.ts\n"), + writeFile(join(checkout, ".env"), "private\n"), + writeFile(join(checkout, "ignored", "private.ts"), "private\n"), + writeFile(join(checkout, "tracked.env"), "tracked\n"), + writeFile(join(checkout, "hidden.ts"), "tracked\n"), + writeFile(join(checkout, "src", "visible.ts"), "export {};\n"), + ]); + execFileSync("git", ["add", "--force", "tracked.env", "hidden.ts"], { + cwd: checkout, + }); + + expect(await inventory(checkout)).toEqual([ + "./.gitignore", + "./.ignore", + "./hidden.ts", + "./src/visible.ts", + "./tracked.env", + ]); + }); + + test.each([".gitignore", ".ignore", ".rgignore"])( + "inventories ordinary directories named %s", + async (name) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const directory = join(checkout, name); + await mkdir(directory); + await writeFile(join(directory, "visible.ts"), "visible\n"); + + expect(await inventory(checkout)).toContain(`./${name}/visible.ts`); + }, + ); + + test.skipIf(process.platform === "win32")( + "preserves distinct POSIX trailing-dot and trailing-space source paths", + async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const names = ["source.ts", "source.ts.", "source.ts "]; + await Promise.all( + names.map((name) => writeFile(join(checkout, name), "tracked\n")), + ); + execFileSync("git", ["add", ...names], { cwd: checkout }); + + const entries = await inventory(checkout); + for (const name of names) { + expect(entries).toContain(`./${name}`); + } + }, + ); + + test.each([".ignore", ".rgignore"])( + "keeps ordinary files re-included by higher-precedence %s rules", + async (override) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + await Promise.all([ + writeFile(join(checkout, ".gitignore"), "source.ts\n"), + writeFile(join(checkout, override), "!source.ts\n"), + writeFile(join(checkout, "source.ts"), "visible\n"), + ]); + + expect(await inventory(checkout)).toContain("./source.ts"); + }, + ); + + test("shares ignore probes across directories with the same rules", async () => { + if (Bun.which("rg") === null) return; + + async function countLookups(branches: number): Promise { + const checkout = await repository(); + const trace = join(dirname(checkout), "git-trace.log"); + await writeFile(join(checkout, ".ignore"), "ignored/\n"); + for (let index = 0; index < branches; index++) { + const nested = join(checkout, `branch-${index}`, "nested"); + await mkdir(nested, { recursive: true }); + await writeFile(join(nested, "source.ts"), "visible\n"); + } + await inventory(checkout, ".", { ...process.env, GIT_TRACE: trace }); + return ( + (await readFile(trace, "utf8")).match(/--git-path info\/exclude/g) ?? [] + ).length; + } + + expect(await countLookups(18)).toBeLessThanOrEqual(await countLookups(2)); + }); + + test("validates Git object files once across many scan directories", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const instrumentation = join(dirname(checkout), "instrumentation"); + const trace = join(dirname(checkout), "object-stats.log"); + await mkdir(instrumentation); + await writeFile( + join(instrumentation, "sitecustomize.py"), + "import os\nfrom pathlib import Path\noriginal = Path.stat\ndef observed(self, *args, **kwargs):\n if self.parent.parent.name == 'objects' and len(self.parent.name) == 2 and len(self.name) == 38:\n with open(os.environ['INVENTORY_OBJECT_STAT_TRACE'], 'a') as trace:\n trace.write(str(self) + '\\n')\n return original(self, *args, **kwargs)\nPath.stat = observed\n", + ); + for (let index = 0; index < 5; index++) { + execFileSync("git", ["hash-object", "-w", "--stdin"], { + cwd: checkout, + input: `object-${index}\n`, + }); + } + for (let index = 0; index < 10; index++) { + const branch = join(checkout, `branch-${index}`, "nested"); + await mkdir(branch, { recursive: true }); + await writeFile(join(branch, "visible.ts"), "visible\n"); + } + + await inventory(checkout, ".", { + ...process.env, + PYTHONPATH: instrumentation, + INVENTORY_OBJECT_STAT_TRACE: trace, + }); + expect((await readFile(trace, "utf8")).trim().split("\n")).toHaveLength(5); + }); + + test.each([".ignore", ".rgignore"])( + "preserves ancestor %s precedence for explicit directory scopes", + async (override) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const nested = join(checkout, "nested"); + await mkdir(nested); + await Promise.all([ + writeFile(join(checkout, override), "!nested/visible.ts\n"), + writeFile(join(nested, ".gitignore"), "*.ts\n"), + writeFile(join(nested, "visible.ts"), "visible\n"), + writeFile(join(nested, "private.ts"), "private\n"), + ]); + + const rows = await inventory(checkout, "nested"); + expect(rows).toContain("nested/visible.ts"); + expect(rows).not.toContain("nested/private.ts"); + }, + ); + + test.each(["case-alias", "short-alias"])( + "accepts absolute directory scopes through a %s", + async (kind) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const nested = join(checkout, "nested"); + await mkdir(nested); + await writeFile(join(nested, "visible.ts"), "visible\n"); + const alias = + kind === "case-alias" + ? join(dirname(checkout), basename(checkout).toUpperCase()) + : windowsShortPath(checkout); + if (alias === null) return; + const equivalent = await realpath(alias).then( + async (resolved) => resolved === (await realpath(checkout)), + () => false, + ); + if (!equivalent) return; + + expect(await inventory(checkout, join(alias, "nested"))).toContain( + "nested/visible.ts", + ); + }, + ); + + test.skipIf(process.platform === "win32")( + "rejects symbolic absolute directory scopes", + async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const nested = join(checkout, "nested"); + const alias = join(checkout, "alias"); + await mkdir(nested); + await writeFile(join(nested, "visible.ts"), "visible\n"); + await symlink(nested, alias); + + await expect(inventory(checkout, alias)).rejects.toThrow( + "symbolic links are not supported", + ); + }, + ); + + test.each([".ignore", ".rgignore"])( + "keeps nested checkout files re-included by higher-precedence %s rules", + async (override) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const nested = join(checkout, "nested"); + await mkdir(nested); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await Promise.all([ + writeFile(join(nested, ".gitignore"), "source.ts\n"), + writeFile(join(nested, override), "!source.ts\n"), + writeFile(join(nested, "source.ts"), "visible\n"), + ]); + + expect(await inventory(checkout)).toContain("./nested/source.ts"); + expect(await inventory(checkout, "nested")).toContain("nested/source.ts"); + }, + ); + + test("applies snapshot ignores without inheriting unrelated parent rules", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(false); + await Promise.all([ + writeFile( + join(dirname(checkout), ".gitignore"), + "repository/visible.ts\n", + ), + writeFile(join(checkout, ".gitignore"), "private.ts\n"), + writeFile(join(checkout, ".ignore"), "hidden.ts\n"), + writeFile(join(checkout, "private.ts"), "private\n"), + writeFile(join(checkout, "hidden.ts"), "private\n"), + writeFile(join(checkout, "visible.ts"), "export {};\n"), + ]); + + expect(await inventory(checkout)).toEqual([ + "./.gitignore", + "./.ignore", + "./visible.ts", + ]); + }); + + test.each([ + ["SS", "ss"], + ["Ä", "ä"], + ["Σ", "ς"], + ["ss", "\u00df"], + ["I", "\u0131"], + ["caf\u00e9", "cafe\u0301"], + ])( + "matches indexed %s against replacement %s using filesystem identity", + async (indexed, replacement) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + await mkdir(join(checkout, indexed)); + await writeFile(join(checkout, indexed, "private.ts"), "tracked\n"); + execFileSync("git", ["add", `${indexed}/private.ts`], { cwd: checkout }); + execFileSync("git", ["config", "core.ignoreCase", "true"], { + cwd: checkout, + }); + await rm(join(checkout, indexed), { recursive: true }); + await mkdir(join(checkout, replacement)); + await writeFile( + join(checkout, replacement, "private.ts"), + "replacement\n", + ); + await writeFile(join(checkout, ".gitignore"), `${replacement}/\n`); + const expected = await realpath( + join(checkout, indexed, "private.ts"), + ).then( + async (path) => + path === (await realpath(join(checkout, replacement, "private.ts"))), + () => false, + ); + + expect( + (await inventory(checkout)).includes(`./${replacement}/private.ts`), + ).toBe(expected); + + execFileSync("git", ["config", "core.ignoreCase", "false"], { + cwd: checkout, + }); + expect( + (await inventory(checkout)).includes(`./${replacement}/private.ts`), + ).toBe(expected); + expect( + (await inventory(checkout, replacement)).includes( + `${replacement}/private.ts`, + ), + ).toBe(expected); + }, + ); + + test.each([".", "LongDirectory", "LongDirectory/PrivateDocument.ts"])( + "restores tracked 8.3 aliases for %s scans using no-follow identity", + async (scope) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const indexed = join(checkout, "LONGDI~1"); + const materialized = join(checkout, "LongDirectory"); + const indexedLeaf = join(materialized, "PRIVAT~1.TS"); + const addressed = join(materialized, "PrivateDocument.ts"); + const unrelated = join(materialized, "ignored-hardlink.ts"); + await mkdir(indexed); + await writeFile(join(indexed, "PRIVAT~1.TS"), "tracked\n"); + execFileSync("git", ["add", "LONGDI~1/PRIVAT~1.TS"], { + cwd: checkout, + }); + await rm(indexed, { recursive: true }); + await mkdir(materialized); + await writeFile(addressed, "tracked\n"); + await hardlink(addressed, unrelated); + await writeFile(join(checkout, ".gitignore"), "LongDirectory/\n"); + + const instrumentation = join(dirname(checkout), "instrumentation"); + await mkdir(instrumentation); + await writeFile( + join(instrumentation, "sitecustomize.py"), + [ + "from pathlib import Path", + `indexed = Path(${JSON.stringify(indexed)})`, + `materialized = Path(${JSON.stringify(materialized)})`, + `indexed_leaf = Path(${JSON.stringify(indexedLeaf)})`, + `addressed = Path(${JSON.stringify(addressed)})`, + "original = Path.stat", + "original_resolve = Path.resolve", + "def guarded(self, *args, **kwargs):", + " if self == indexed and kwargs.get('follow_symlinks') is False:", + " return original(materialized, *args, **kwargs)", + " if self == indexed_leaf and kwargs.get('follow_symlinks') is False:", + " return original(addressed, *args, **kwargs)", + " return original(self, *args, **kwargs)", + "def resolved(self, *args, **kwargs):", + " if self == indexed:", + " return original_resolve(materialized, *args, **kwargs)", + " if self == indexed_leaf:", + " return original_resolve(addressed, *args, **kwargs)", + " return original_resolve(self, *args, **kwargs)", + "Path.stat = guarded", + "Path.resolve = resolved", + ].join("\n"), + ); + + const rows = await inventory(checkout, scope, { + ...process.env, + PYTHONPATH: instrumentation, + }); + const prefix = scope === "." ? "./" : ""; + expect(rows).toContain(`${prefix}LongDirectory/PrivateDocument.ts`); + expect(rows).not.toContain(`${prefix}LongDirectory/ignored-hardlink.ts`); + }, + ); + + test("keeps an explicitly selected ignored file without widening its directory", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + await mkdir(join(checkout, "ignored")); + await Promise.all([ + writeFile(join(checkout, ".gitignore"), "selected.skip\nignored/\n"), + writeFile(join(checkout, "selected.skip"), "selected\n"), + writeFile(join(checkout, "ignored", "tracked.ts"), "tracked\n"), + writeFile(join(checkout, "ignored", "private.ts"), "private\n"), + ]); + execFileSync("git", ["add", "--force", "ignored/tracked.ts"], { + cwd: checkout, + }); + + expect(await inventory(checkout, "selected.skip")).toEqual([ + "selected.skip", + ]); + expect(await inventory(checkout, "ignored")).toEqual([ + "ignored/tracked.ts", + ]); + }); + + test("respects tracked files and ignore rules inside nested Git checkouts", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const nested = join(checkout, "nested"); + await mkdir(nested); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await Promise.all([ + writeFile(join(nested, ".gitignore"), ".env\n"), + writeFile(join(nested, ".ignore"), "tracked.ts\n"), + writeFile(join(nested, ".env"), "private\n"), + writeFile(join(nested, "tracked.ts"), "tracked\n"), + writeFile(join(nested, "visible.ts"), "visible\n"), + ]); + execFileSync("git", ["add", "tracked.ts"], { cwd: nested }); + + const rows = await inventory(checkout); + expect(rows).toContain("./nested/tracked.ts"); + expect(rows).toContain("./nested/visible.ts"); + expect(rows).not.toContain("./nested/.env"); + }); + + test.each([ + ["embedded", "."], + ["embedded", "nested"], + ["conflicted Gitlink", "."], + ["conflicted Gitlink", "nested"], + ])( + "retains outer tracked source inside %s checkout for %s", + async (staging, scope) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const nested = join(checkout, "nested"); + await mkdir(nested); + await Promise.all([ + writeFile(join(nested, "outer.ts"), "outer tracked\n"), + writeFile(join(nested, "inner.ts"), "inner tracked\n"), + writeFile(join(nested, "private.ts"), "private\n"), + ]); + execFileSync("git", ["add", "nested/outer.ts"], { cwd: checkout }); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await writeFile( + join(nested, ".ignore"), + "outer.ts\ninner.ts\nprivate.ts\n", + ); + execFileSync("git", ["add", "inner.ts"], { cwd: nested }); + if (staging === "conflicted Gitlink") { + commit(nested); + const gitlink = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: nested, + encoding: "utf8", + }).trim(); + const outer = execFileSync("git", ["rev-parse", ":nested/outer.ts"], { + cwd: checkout, + encoding: "utf8", + }).trim(); + execFileSync("git", ["update-index", "--index-info"], { + cwd: checkout, + input: [ + `0 ${"0".repeat(40)}\tnested/outer.ts`, + `160000 ${gitlink} 1\tnested`, + `100644 ${outer} 2\tnested/outer.ts`, + "", + ].join("\n"), + }); + } + + const prefix = scope === "." ? "./nested" : "nested"; + const rows = await inventory(checkout, scope); + expect(rows).toContain(`${prefix}/outer.ts`); + expect(rows).toContain(`${prefix}/inner.ts`); + expect(rows).not.toContain(`${prefix}/private.ts`); + }, + ); + + test("recovers an embedded checkout hidden only by its own ignore file", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const nested = join(checkout, "nested"); + await mkdir(nested); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await writeFile(join(nested, ".ignore"), "*\n"); + await writeFile(join(nested, "tracked.ts"), "export {};\n"); + execFileSync("git", ["add", "tracked.ts"], { cwd: nested }); + + expect(await inventory(checkout)).toContain("./nested/tracked.ts"); + await writeFile(join(checkout, ".ignore"), ".*\n"); + expect(await inventory(checkout)).toContain("./nested/tracked.ts"); + await writeFile(join(checkout, ".ignore"), "nested/\n"); + expect(await inventory(checkout)).not.toContain("./nested/tracked.ts"); + }); + + test.each([".ignore", ".rgignore", ".git/info/exclude"])( + "does not recover nested tracked files excluded by outer %s rules", + async (ignore) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const nested = join(checkout, "nested"); + await mkdir(nested); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await writeFile(join(checkout, ignore), "nested/private.ts\n"); + await writeFile(join(nested, "private.ts"), "private\n"); + await writeFile(join(nested, "visible.ts"), "visible\n"); + execFileSync("git", ["add", "private.ts", "visible.ts"], { + cwd: nested, + }); + + const rows = await inventory(checkout); + expect(rows).toContain("./nested/visible.ts"); + expect(rows).not.toContain("./nested/private.ts"); + }, + ); + + test.each([".ignore", ".gitignore", ".git/info/exclude"])( + "applies %s file exclusions beneath tracked Git checkouts", + async (outerIgnore) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const nested = join(checkout, "nested"); + await mkdir(nested); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await Promise.all([ + writeFile( + join(checkout, ".gitignore"), + outerIgnore === ".gitignore" + ? "nested/\nnested/private.ts\n" + : "nested/\n", + ), + ...(outerIgnore === ".gitignore" + ? [] + : [writeFile(join(checkout, outerIgnore), "nested/private.ts\n")]), + writeFile(join(nested, "private.ts"), "private\n"), + writeFile(join(nested, "visible.ts"), "visible\n"), + ]); + execFileSync("git", ["add", "private.ts", "visible.ts"], { cwd: nested }); + commit(nested); + execFileSync("git", ["add", "--force", "nested"], { + cwd: checkout, + stdio: "ignore", + }); + + const rows = await inventory(checkout); + expect(rows).toContain("./nested/visible.ts"); + expect(rows).not.toContain("./nested/private.ts"); + + const scoped = await inventory(checkout, "nested"); + expect(scoped).toContain("nested/visible.ts"); + expect(scoped).not.toContain("nested/private.ts"); + + if (outerIgnore === ".git/info/exclude") { + await writeFile( + join(checkout, ".gitignore"), + "nested/\n!nested/private.ts\n", + ); + expect(await inventory(checkout)).toContain("./nested/private.ts"); + expect(await inventory(checkout, "nested")).toContain( + "nested/private.ts", + ); + } + }, + ); + + test.each(["nested", " #nested"])( + "applies configured excludes from every enclosing checkout to %s", + async (directory) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const middle = join(checkout, "middle"); + const nested = join(middle, directory); + await mkdir(nested, { recursive: true }); + execFileSync("git", ["init", "-q"], { cwd: middle }); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await Promise.all([ + writeFile( + join(middle, ".git", "info", "exclude"), + `${directory}/private.ts\n`, + ), + writeFile(join(nested, "private.ts"), "private\n"), + writeFile(join(nested, "visible.ts"), "visible\n"), + ]); + execFileSync("git", ["add", "private.ts", "visible.ts"], { cwd: nested }); + + const rows = await inventory(checkout); + expect(rows).toContain(`./middle/${directory}/visible.ts`); + expect(rows).not.toContain(`./middle/${directory}/private.ts`); + }, + ); + + test("preserves intermediate ignores beneath an ancestor Git link", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const middle = join(checkout, "middle"); + const nested = join(middle, "nested"); + await mkdir(nested, { recursive: true }); + execFileSync("git", ["init", "-q"], { cwd: middle }); + await writeFile(join(middle, "visible.ts"), "visible\n"); + execFileSync("git", ["add", "visible.ts"], { cwd: middle }); + commit(middle); + execFileSync("git", ["add", "middle"], { + cwd: checkout, + stdio: "ignore", + }); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await Promise.all([ + writeFile(join(middle, ".gitignore"), "nested/private.ts\n"), + writeFile(join(nested, "private.ts"), "private\n"), + writeFile(join(nested, "visible.ts"), "visible\n"), + ]); + execFileSync("git", ["add", "private.ts", "visible.ts"], { cwd: nested }); + + const rows = await inventory(checkout); + expect(rows).toContain("./middle/nested/visible.ts"); + expect(rows).not.toContain("./middle/nested/private.ts"); + }); + + test.each(["stage-0", "conflicted", "short-alias", "conflicted-short-alias"])( + "admits %s tracked Gitlinks through configured directory excludes", + async (staging) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const short = staging.endsWith("short-alias"); + const name = short ? "LongDirectory" : "nested"; + const nested = join(checkout, name); + await mkdir(nested); + const nativeAlias = short ? windowsShortPath(nested) : null; + if (short && process.platform === "win32" && nativeAlias === null) { + return; + } + const indexed = short + ? nativeAlias === null + ? "LONGDI~1" + : basename(nativeAlias) + : name; + execFileSync("git", ["init", "-q"], { cwd: nested }); + await Promise.all([ + writeFile(join(nested, "visible.ts"), "visible\n"), + writeFile(join(nested, "private.ts"), "private\n"), + ]); + execFileSync("git", ["add", "visible.ts", "private.ts"], { + cwd: nested, + }); + commit(nested); + execFileSync("git", ["add", name], { + cwd: checkout, + stdio: "ignore", + }); + if (short || staging === "conflicted") { + const object = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: nested, + encoding: "utf8", + }).trim(); + execFileSync("git", ["update-index", "--index-info"], { + cwd: checkout, + input: [ + `0 ${"0".repeat(40)}\t${name}`, + ...(staging.startsWith("conflicted") ? [1, 2, 3] : [0]).map( + (stage) => `160000 ${object} ${stage}\t${indexed}`, + ), + "", + ].join("\n"), + }); + } + if (short && nativeAlias === null) { + await symlink(nested, join(checkout, indexed)); + } + await writeFile( + join(checkout, ".git", "info", "exclude"), + `${name}/\n${name}/private.ts\n${indexed}/\n`, + ); + + let environment = process.env; + if (short) { + const instrumentation = join(dirname(checkout), "instrumentation"); + await mkdir(instrumentation); + await writeFile( + join(instrumentation, "sitecustomize.py"), + [ + "from pathlib import Path", + `indexed = Path(${JSON.stringify(join(checkout, indexed))})`, + `materialized = Path(${JSON.stringify(nested)})`, + "original = Path.stat", + "def guarded(self, *args, **kwargs):", + " if self == indexed and kwargs.get('follow_symlinks') is False:", + " return original(materialized, *args, **kwargs)", + " return original(self, *args, **kwargs)", + "Path.stat = guarded", + ].join("\n"), + ); + environment = { ...process.env, PYTHONPATH: instrumentation }; + } + + const rows = await inventory(checkout, ".", environment); + expect(rows).toContain(`./${name}/visible.ts`); + expect(rows).not.toContain(`./${name}/private.ts`); + }, + ); + + test.skipIf(process.platform === "win32").each([".ignore", ".rgignore"])( + "rejects a hidden Gitlink ancestor's symbolic %s", + async (name) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const hidden = join(checkout, "hidden"); + const nested = join(hidden, "nested"); + const external = join(dirname(checkout), "external.ignore"); + await mkdir(nested, { recursive: true }); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await writeFile(join(nested, "private.ts"), "tracked\n"); + execFileSync("git", ["add", "private.ts"], { cwd: nested }); + commit(nested); + await writeFile(join(checkout, ".gitignore"), "hidden/\n"); + execFileSync("git", ["add", "--force", "hidden/nested"], { + cwd: checkout, + stdio: "ignore", + }); + await writeFile(external, "nested/private.ts\n"); + await symlink(external, join(hidden, name)); + + await expect(inventory(checkout)).rejects.toThrow( + "symbolic ignore files are not supported", + ); + }, + ); + + test.each([ + ["visible", "nested/private.ts\n"], + ["ignored", "nested/\nnested/private.ts\n"], + ])( + "preserves outer Git file exclusions for %s explicit nested scopes", + async (_visibility, outerIgnores) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const nested = join(checkout, "nested"); + await mkdir(nested); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await Promise.all([ + writeFile(join(checkout, ".gitignore"), outerIgnores), + writeFile(join(nested, "private.ts"), "private\n"), + writeFile(join(nested, "visible.ts"), "visible\n"), + ]); + execFileSync("git", ["add", "private.ts", "visible.ts"], { cwd: nested }); + + expect(await inventory(checkout, "nested")).toEqual([ + "nested/visible.ts", + ]); + + await writeFile(join(checkout, ".git", "info", "exclude"), "nested/\n"); + expect(await inventory(checkout, "nested")).toEqual([]); + }, + ); + + test("does not grant Git link exemptions to replaced tracked files", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const nested = join(checkout, "nested"); + await writeFile(nested, "previously tracked file\n"); + execFileSync("git", ["add", "nested"], { cwd: checkout }); + await rm(nested); + await mkdir(nested); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await Promise.all([ + writeFile(join(checkout, ".gitignore"), "nested/private.ts\n"), + writeFile(join(nested, "private.ts"), "private\n"), + writeFile(join(nested, "visible.ts"), "visible\n"), + ]); + execFileSync("git", ["add", "private.ts", "visible.ts"], { cwd: nested }); + + const rows = await inventory(checkout); + expect(rows).toContain("./nested/visible.ts"); + expect(rows).not.toContain("./nested/private.ts"); + }); + + test.each([".ignore", ".rgignore", ".gitignore"])( + "does not inspect checkout metadata excluded by outer %s rules", + async (ignore) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const nested = join(checkout, "nested"); + const external = join(dirname(checkout), "external.config"); + await mkdir(nested); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await writeFile(external, "[core]\n\tignoreCase = true\n"); + execFileSync("git", ["config", "--local", "include.path", external], { + cwd: nested, + }); + await writeFile(join(checkout, ignore), "nested/\n"); + await writeFile(join(checkout, "visible.ts"), "visible\n"); + + expect(await inventory(checkout)).toContain("./visible.ts"); + }, + ); + + test("discovers self-hidden checkouts through visible snapshot directories", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(false); + const nested = join(checkout, "container", "nested"); + await mkdir(nested, { recursive: true }); + await writeFile(join(checkout, ".ignore"), "scan-source\n"); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await writeFile(join(nested, ".ignore"), "*\n"); + await writeFile(join(nested, "tracked.ts"), "export {};\n"); + execFileSync("git", ["add", "tracked.ts"], { cwd: nested }); + + expect(await inventory(checkout)).toContain( + "./container/nested/tracked.ts", + ); + + await writeFile( + join(checkout, "container", ".git"), + "malformed nested Git marker\n", + ); + expect(await inventory(checkout)).toContain( + "./container/nested/tracked.ts", + ); + + await writeFile(join(checkout, ".ignore"), "scan-source\ncontainer/\n"); + expect(await inventory(checkout)).not.toContain( + "./container/nested/tracked.ts", + ); + await writeFile(join(checkout, ".ignore"), "scan-source\n"); + await writeFile(join(checkout, ".git"), "malformed snapshot marker\n"); + expect(await inventory(checkout)).toContain( + "./container/nested/tracked.ts", + ); + }); + + test.skipIf(process.platform === "win32")( + "rejects line-separated snapshot directory names before parsing ignore diagnostics", + async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(false); + const nested = join(checkout, "victim", "nested"); + await mkdir(nested, { recursive: true }); + await mkdir(join(checkout, "evil\nrg: DEBUG|x: ignoring victim")); + await writeFile(join(checkout, ".ignore"), "evil*\n"); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await writeFile(join(nested, ".ignore"), "*\n"); + await writeFile(join(nested, "private.ts"), "tracked\n"); + execFileSync("git", ["add", "private.ts"], { cwd: nested }); + + await expect(inventory(checkout)).rejects.toThrow( + "line separators are not supported in inventory paths", + ); + }, + ); + + test("discovers Git-hidden checkouts reopened by ripgrep ignore rules", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const nested = join(checkout, "container", "nested"); + await mkdir(nested, { recursive: true }); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await Promise.all([ + writeFile(join(checkout, ".gitignore"), "container/\n"), + writeFile(join(checkout, ".ignore"), "!container/\n!container/nested/\n"), + writeFile(join(nested, ".ignore"), "*\n"), + writeFile(join(nested, "tracked.ts"), "export {};\n"), + ]); + execFileSync("git", ["add", "tracked.ts"], { cwd: nested }); + + expect(await inventory(checkout)).toContain( + "./container/nested/tracked.ts", + ); + }); + + test("discovers checkout overrides that hide their own ignore files", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const container = join(checkout, "container"); + const nested = join(container, "mid", "nested"); + await mkdir(nested, { recursive: true }); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await Promise.all([ + writeFile(join(checkout, ".gitignore"), "container/mid/\n"), + writeFile( + join(container, ".ignore"), + "*\n!mid/\n!mid/nested/\n!mid/nested/**\n", + ), + writeFile(join(nested, ".ignore"), "*\n"), + writeFile(join(nested, "tracked.ts"), "export {};\n"), + ]); + execFileSync("git", ["add", "tracked.ts"], { cwd: nested }); + + expect(await inventory(checkout)).toContain( + "./container/mid/nested/tracked.ts", + ); + }); + + test("keeps ignore scaffolding separate from case-distinct checkouts", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + await Promise.all([ + writeFile(join(checkout, ".gitignore"), ".IGNORE/tracked.ts\n"), + writeFile( + join(checkout, ".ignore"), + "!.IGNORE/tracked.ts\n.IGNORE/private.ts\n", + ), + ]); + const nested = join(checkout, ".IGNORE"); + try { + await mkdir(nested); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "EEXIST") return; + throw error; + } + execFileSync("git", ["init", "-q"], { cwd: nested }); + await Promise.all([ + writeFile(join(nested, "tracked.ts"), "visible\n"), + writeFile(join(nested, "private.ts"), "private\n"), + ]); + execFileSync("git", ["add", "tracked.ts", "private.ts"], { cwd: nested }); + + const rows = await inventory(checkout); + expect(rows).toContain("./.IGNORE/tracked.ts"); + expect(rows).not.toContain("./.IGNORE/private.ts"); + }); + + test("preserves rgignore precedence for case-distinct checkout paths", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + await Promise.all([ + writeFile(join(checkout, ".ignore"), "!.RGIGNORE/private.ts\n"), + writeFile(join(checkout, ".rgignore"), ".RGIGNORE/private.ts\n"), + ]); + const nested = join(checkout, ".RGIGNORE"); + try { + await mkdir(nested); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "EEXIST") return; + throw error; + } + execFileSync("git", ["init", "-q"], { cwd: nested }); + await Promise.all([ + writeFile(join(nested, "tracked.ts"), "visible\n"), + writeFile(join(nested, "private.ts"), "private\n"), + ]); + execFileSync("git", ["add", "tracked.ts", "private.ts"], { cwd: nested }); + + const rows = await inventory(checkout); + expect(rows).toContain("./.RGIGNORE/tracked.ts"); + expect(rows).not.toContain("./.RGIGNORE/private.ts"); + }); + + test.each([ + ["slash-only", "/\n"], + ["whitespace-only", " \n"], + ["embedded-carriage-return", ".IGNORE/tracked.ts\rignored\n"], + ["unterminated-carriage-return", ".IGNORE/tracked.ts\r"], + ])( + "keeps %s ignores inert when isolating nested checkout names", + async (_description, contents) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const container = join(checkout, "container"); + const nested = join(container, ".IGNORE"); + await mkdir(container); + await writeFile(join(container, ".ignore"), contents); + try { + await mkdir(nested); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "EEXIST") return; + throw error; + } + execFileSync("git", ["init", "-q"], { cwd: nested }); + await writeFile(join(nested, ".ignore"), "*\n"); + await writeFile(join(nested, "tracked.ts"), "tracked\n"); + execFileSync("git", ["add", "tracked.ts"], { cwd: nested }); + + expect(await inventory(checkout)).toContain( + "./container/.IGNORE/tracked.ts", + ); + }, + ); + + test("preserves BOM-prefixed ignores when isolating nested checkout names", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const container = join(checkout, "container"); + const nested = join(container, ".IGNORE"); + await mkdir(container); + await writeFile(join(container, ".ignore"), "\ufeff.IGNORE/private.ts\n"); + try { + await mkdir(nested); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "EEXIST") return; + throw error; + } + execFileSync("git", ["init", "-q"], { cwd: nested }); + await writeFile(join(nested, ".ignore"), "*\n"); + await writeFile(join(nested, "tracked.ts"), "tracked\n"); + await writeFile(join(nested, "private.ts"), "private\n"); + execFileSync("git", ["add", "tracked.ts", "private.ts"], { + cwd: nested, + }); + + const rows = await inventory(checkout); + expect(rows).toContain("./container/.IGNORE/tracked.ts"); + expect(rows).not.toContain("./container/.IGNORE/private.ts"); + }); + + test("preserves canonically equivalent tracked directory spellings", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const nested = join(checkout, "nested"); + const composed = join(nested, "caf\u00e9"); + const decomposed = join(nested, "cafe\u0301"); + await mkdir(composed, { recursive: true }); + try { + await mkdir(decomposed); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "EEXIST") return; + throw error; + } + execFileSync("git", ["init", "-q"], { cwd: nested }); + await Promise.all([ + writeFile(join(checkout, ".gitignore"), "nested/private.ts\n"), + writeFile(join(nested, ".ignore"), "*\n"), + writeFile(join(composed, "first.ts"), "first\n"), + writeFile(join(decomposed, "second.ts"), "second\n"), + ]); + execFileSync( + "git", + ["add", "--force", "caf\u00e9/first.ts", "cafe\u0301/second.ts"], + { + cwd: nested, + }, + ); + + const rows = await inventory(checkout); + expect(rows).toContain("./nested/caf\u00e9/first.ts"); + expect(rows).toContain("./nested/cafe\u0301/second.ts"); + }); + + test("keeps tracked files when an ignored snapshot checkout is explicitly selected", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(false); + const nested = join(checkout, "ignored"); + await mkdir(nested); + await writeFile(join(checkout, ".gitignore"), "ignored/\n"); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await Promise.all([ + writeFile(join(nested, "public.ts"), "tracked\n"), + writeFile(join(nested, "private.ts"), "ignored\n"), + ]); + execFileSync("git", ["add", "public.ts"], { cwd: nested }); + + expect(await inventory(checkout, "ignored")).toEqual(["ignored/public.ts"]); + }); + + test.each(["marker", "gitfile"])( + "rejects symbolic Git metadata through a %s", + async (kind) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(false); + const external = await repository(); + const metadata = + kind === "marker" + ? join(checkout, ".git") + : join(dirname(checkout), "linked-metadata"); + await symlink( + join(external, ".git"), + metadata, + process.platform === "win32" ? "junction" : "dir", + ); + if (kind === "gitfile") { + await writeFile(join(checkout, ".git"), `gitdir: ${metadata}\n`); + } + await writeFile(join(checkout, "visible.ts"), "visible\n"); + + await expect(inventory(checkout)).rejects.toThrow( + "symbolic Git metadata paths are not supported", + ); + }, + ); + + test.each(["gitdir", "backpointer", "commondir", "worktree"])( + "rejects symbolic %s metadata hops before parent traversal", + async (kind) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + await writeFile(join(checkout, "visible.ts"), "tracked\n"); + execFileSync("git", ["add", "visible.ts"], { cwd: checkout }); + commit(checkout); + const linked = join(dirname(checkout), "linked-worktree"); + execFileSync("git", ["worktree", "add", "--detach", linked, "HEAD"], { + cwd: checkout, + stdio: "ignore", + }); + const gitdir = (await readFile(join(linked, ".git"), "utf8")) + .replace(/^gitdir: /, "") + .trim(); + const outside = join(dirname(checkout), "outside", "hop-target"); + await mkdir(outside, { recursive: true }); + const hop = + kind === "gitdir" + ? join(dirname(gitdir), "hop") + : kind === "commondir" + ? join(checkout, "hop") + : join(linked, "hop"); + await symlink( + outside, + hop, + process.platform === "win32" ? "junction" : "dir", + ); + + if (kind === "gitdir") { + await writeFile( + join(linked, ".git"), + `gitdir: ${hop}/../${basename(gitdir)}\n`, + ); + } else if (kind === "backpointer") { + await writeFile(join(gitdir, "gitdir"), `${hop}/../.git\n`); + } else if (kind === "commondir") { + await writeFile(join(gitdir, "commondir"), `${hop}/../.git\n`); + } else { + execFileSync("git", ["config", "extensions.worktreeConfig", "true"], { + cwd: linked, + }); + execFileSync( + "git", + ["config", "--worktree", "core.worktree", `${hop}/..`], + { + cwd: linked, + }, + ); + } + + await expect(inventory(linked)).rejects.toThrow( + "symbolic Git metadata paths are not supported", + ); + }, + ); + + test.skipIf(process.platform !== "win32").each([ + ["gitdir", "network"], + ["gitdir", "device"], + ["backpointer", "network"], + ["commondir", "network"], + ["worktree", "network"], + ["alternates", "network"], + ])( + "rejects %s %s metadata before accessing its Windows anchor", + async (kind, prefix) => { + const checkout = await repository(kind !== "gitdir"); + const remote = `${ + prefix === "device" ? "\\\\?\\UNC" : "\\" + }\\codex-security.invalid\\share\\one\\two\\three\\four\\five\\six\\seven\\eight`; + let selected = checkout; + if (kind === "gitdir") { + await writeFile(join(checkout, ".git"), `gitdir: ${remote}\n`); + } else if (kind === "alternates") { + await writeFile( + join(checkout, ".git", "objects", "info", "alternates"), + `${remote}\n`, + ); + } else { + await writeFile(join(checkout, "tracked.ts"), "tracked\n"); + execFileSync("git", ["add", "tracked.ts"], { cwd: checkout }); + commit(checkout); + selected = join(dirname(checkout), "linked-worktree"); + execFileSync("git", ["worktree", "add", "--detach", selected, "HEAD"], { + cwd: checkout, + stdio: "ignore", + }); + const gitdir = (await readFile(join(selected, ".git"), "utf8")) + .replace(/^gitdir: /, "") + .trim(); + if (kind === "backpointer") { + await writeFile(join(gitdir, "gitdir"), `${remote}\n`); + } else if (kind === "commondir") { + await writeFile(join(gitdir, "commondir"), `${remote}\n`); + } else { + execFileSync("git", ["config", "extensions.worktreeConfig", "true"], { + cwd: selected, + }); + await writeFile( + join(gitdir, "config.worktree"), + `[core]\n\tworktree = ${JSON.stringify(remote)}\n`, + ); + } + } + const instrumentation = join(dirname(checkout), "instrumentation"); + await mkdir(instrumentation); + await writeFile( + join(instrumentation, "sitecustomize.py"), + [ + "from pathlib import Path", + "original = Path.stat", + "def guarded(self, *args, **kwargs):", + " if self.anchor.startswith(chr(92) * 2) and 'codex-security.invalid' in str(self).casefold():", + " raise RuntimeError('attempted network metadata access')", + " return original(self, *args, **kwargs)", + "Path.stat = guarded", + ].join("\n"), + ); + + await expect( + inventory(selected, ".", { + ...process.env, + PYTHONPATH: instrumentation, + }), + ).rejects.toThrow("network Git metadata paths are not supported"); + }, + ); + + test.each(["worktrees", "owner"])( + "rejects symbolic common %s metadata before following worktree ownership", + async (kind) => { + const checkout = await repository(); + await writeFile(join(checkout, "tracked.ts"), "tracked\n"); + execFileSync("git", ["add", "tracked.ts"], { cwd: checkout }); + commit(checkout); + const linked = join(dirname(checkout), "linked-worktree"); + execFileSync("git", ["worktree", "add", "--detach", linked, "HEAD"], { + cwd: checkout, + stdio: "ignore", + }); + const gitdir = (await readFile(join(linked, ".git"), "utf8")) + .replace(/^gitdir: /, "") + .trim(); + const common = join(dirname(checkout), "common-metadata"); + const external = join(dirname(checkout), "external-worktrees"); + await mkdir(common); + await mkdir(external); + if (kind === "owner") await mkdir(join(common, "worktrees")); + const symbolic = + kind === "worktrees" + ? join(common, "worktrees") + : join(common, "worktrees", basename(gitdir)); + await symlink( + external, + symbolic, + process.platform === "win32" ? "junction" : "dir", + ); + await writeFile(join(gitdir, "commondir"), `${common}\n`); + + const instrumentation = join(dirname(checkout), "instrumentation"); + await mkdir(instrumentation); + await writeFile( + join(instrumentation, "sitecustomize.py"), + [ + "from pathlib import Path", + `owner = Path(${JSON.stringify(join(common, "worktrees", basename(gitdir)))})`, + "original = Path.stat", + "def guarded(self, *args, **kwargs):", + " if self == owner and kwargs.get('follow_symlinks', True):", + " raise RuntimeError('followed unvalidated Git worktree owner')", + " return original(self, *args, **kwargs)", + "Path.stat = guarded", + ].join("\n"), + ); + + await expect( + inventory(linked, ".", { + ...process.env, + PYTHONPATH: instrumentation, + }), + ).rejects.toThrow("symbolic Git metadata paths are not supported"); + }, + ); + + test.each([".GIT", ".GIT.", ".g\u0131t", ".g\u0131t."])( + "rejects symbolic %s metadata before resolving its filesystem alias", + async (alias) => { + if (process.platform === "win32" && alias.endsWith(".")) return; + + const checkout = await repository(); + const nested = join(checkout, "visible"); + const external = join(dirname(checkout), "external-metadata"); + await mkdir(nested); + await mkdir(external); + await symlink( + external, + join(nested, alias), + process.platform === "win32" ? "junction" : "dir", + ); + + const instrumentation = join(dirname(checkout), "instrumentation"); + await mkdir(instrumentation); + await writeFile( + join(instrumentation, "sitecustomize.py"), + [ + "from pathlib import Path", + "original = Path.samefile", + "def guarded(self, other):", + " if self.parent.name == 'visible' and self.name.upper().casefold().rstrip('. ') == '.git':", + " raise RuntimeError('followed symbolic Git metadata alias')", + " return original(self, other)", + "Path.samefile = guarded", + ].join("\n"), + ); + + await expect( + inventory(checkout, ".", { + ...process.env, + PYTHONPATH: instrumentation, + }), + ).rejects.toThrow("symbolic Git metadata paths are not supported"); + }, + ); + + test.each(["missing", "mismatched"])( + "rejects an external gitdir with a %s worktree backpointer", + async (ownership) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(false); + const external = await repository(); + await writeFile(join(external, "secret.ts"), "tracked\n"); + execFileSync("git", ["add", "secret.ts"], { cwd: external }); + await writeFile(join(checkout, ".gitignore"), "secret.ts\n"); + await writeFile(join(checkout, "secret.ts"), "private\n"); + await writeFile( + join(checkout, ".git"), + `gitdir: ${join(external, ".git")}\n`, + ); + if (ownership === "mismatched") { + await writeFile( + join(external, ".git", "gitdir"), + `${join(external, ".git")}\n`, + ); + } + + await expect(inventory(checkout)).rejects.toThrow( + "Git metadata directory does not own selected worktree", + ); + }, + ); + + test.each(["missing", "conflicting"])( + "rejects an internal gitfile with %s checkout ownership", + async (ownership) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const nested = join(checkout, "nested"); + await mkdir(nested); + await writeFile(join(checkout, "secret.ts"), "tracked\n"); + execFileSync("git", ["add", "secret.ts"], { cwd: checkout }); + await writeFile(join(nested, ".ignore"), "secret.ts\n"); + await writeFile(join(nested, "secret.ts"), "private\n"); + await writeFile(join(nested, ".git"), "gitdir: ../.git\n"); + if (ownership === "conflicting") { + execFileSync( + "git", + ["config", "--local", "core.worktree", "../nested"], + { + cwd: checkout, + }, + ); + } + + await expect(inventory(checkout)).rejects.toThrow( + "Git metadata directory does not own selected worktree", + ); + }, + ); + + test.each([ + "disabled", + "disabled-comment", + "disabled-empty", + "disabled-quoted", + "disabled-carriage", + "disabled-symlink", + "hash-comment-override", + "semicolon-comment-override", + "indented-override", + "inline-section-override", + "inline-carriage-override", + "chained-section-override", + "inline-extension-disabled", + "carriage-return-override", + "carriage-return-section", + "carriage-return-section-comment", + "default-inheritance", + "unquoted-escape", + "literal-tab", + "literal-tab-missing", + "quoted-tab-owned", + "vertical-tab-owned", + "form-feed-owned", + "case-alias", + "short-alias", + "short-worktree", + "owned", + "external", + "mixed-case-external", + ])( + "honors %s worktree-specific Git ownership configuration", + async (ownership) => { + if (Bun.which("rg") === null) return; + if (ownership === "disabled-symlink" && process.platform === "win32") + return; + if (ownership === "unquoted-escape" && process.platform === "win32") + return; + const tabOwnership = + ownership.startsWith("literal-tab") || ownership === "quoted-tab-owned"; + const controlOwnership = + ownership === "vertical-tab-owned" || ownership === "form-feed-owned"; + if ((tabOwnership || controlOwnership) && process.platform === "win32") + return; + if ( + ownership.startsWith("short-") && + (process.platform !== "win32" || python === null) + ) + return; + + const checkout = await repository(); + const nested = join( + checkout, + ownership === "unquoted-escape" + ? "nested\\towner" + : tabOwnership + ? "nested\towner" + : ownership === "vertical-tab-owned" + ? "nested\vowner" + : ownership === "form-feed-owned" + ? "nested\fowner" + : "nested", + ); + const metadata = join(checkout, ".git", "modules", "nested"); + const external = + ownership === "unquoted-escape" + ? join(checkout, "nested\towner") + : ownership === "literal-tab" + ? join(checkout, "nested owner") + : join(dirname(checkout), "external-worktree"); + await mkdir(dirname(metadata), { recursive: true }); + await mkdir(external); + execFileSync( + "git", + ["init", "-q", "--separate-git-dir", metadata, nested], + { + cwd: checkout, + }, + ); + execFileSync("git", ["-C", nested, "config", "core.worktree", nested]); + execFileSync("git", [ + "-C", + nested, + "config", + "extensions.worktreeConfig", + ownership.startsWith("disabled") || + ownership.endsWith("comment-override") || + ownership === "indented-override" || + ownership === "inline-section-override" || + ownership === "inline-carriage-override" || + ownership === "chained-section-override" || + ownership.startsWith("carriage-return") || + ownership === "default-inheritance" || + ownership === "unquoted-escape" || + tabOwnership || + controlOwnership + ? "false" + : "true", + ]); + await writeFile(join(nested, "visible.ts"), "tracked\n"); + execFileSync("git", ["-C", nested, "add", "visible.ts"]); + const effective = + ownership === "owned" || + ownership === "case-alias" || + ownership === "short-alias" || + ownership === "short-worktree" + ? nested + : external; + const config = join(metadata, "config"); + if (ownership === "mixed-case-external") { + await writeFile( + config, + (await readFile(config, "utf8")).replace( + /^\[extensions\]$/im, + "[Extensions]", + ), + ); + } else if ( + ownership === "disabled-comment" || + ownership === "disabled-empty" || + ownership === "disabled-quoted" || + ownership === "disabled-carriage" + ) { + await writeFile( + config, + (await readFile(config, "utf8")).replace( + /^([ \t]*worktreeConfig[ \t]*=[ \t]*)false$/im, + ownership === "disabled-comment" + ? "$1false # disabled" + : ownership === "disabled-quoted" + ? '$1f"al"se' + : ownership === "disabled-carriage" + ? "$1\rfalse" + : "$1", + ), + ); + } else if (ownership.endsWith("comment-override")) { + const comment = ownership.startsWith("hash") ? "#" : ";"; + await writeFile( + config, + (await readFile(config, "utf8")).replace( + /^([ \t]*worktree[ \t]*=.*)$/im, + `$1\n\t${comment} owner comment \\\n\tworktree = ${external}`, + ), + ); + } else if (ownership === "indented-override") { + await writeFile( + config, + (await readFile(config, "utf8")).replace( + /^([ \t]*worktree[ \t]*=.*)$/im, + `$1 # selected owner\n\t\tworktree = ${external}`, + ), + ); + } else if ( + ownership === "inline-section-override" || + ownership === "inline-carriage-override" || + ownership === "chained-section-override" + ) { + const header = + ownership === "chained-section-override" + ? '[0][-][.legacy][ "quoted"][feature][unused.value][core]' + : "[core]"; + const whitespace = ownership === "inline-carriage-override" ? "\r" : ""; + await writeFile( + config, + `${await readFile(config, "utf8")}\n${header}${whitespace}worktree = ${external}\n`, + ); + } else if (ownership === "inline-extension-disabled") { + await writeFile( + config, + `${await readFile(config, "utf8")}\n[extensions]worktreeConfig = false\n`, + ); + } else if (ownership === "carriage-return-override") { + await writeFile( + config, + (await readFile(config, "utf8")).replace( + /^([ \t]*worktree[ \t]*=.*)$/im, + `$1\n\rworktree = ${external}`, + ), + ); + } else if ( + ownership === "carriage-return-section" || + ownership === "carriage-return-section-comment" + ) { + await writeFile( + config, + `${await readFile(config, "utf8")}\n${ + ownership === "carriage-return-section" + ? "\r[core]" + : "[core]\r# selected owner" + }\n\tworktree = ${external}\n`, + ); + } else if (ownership === "default-inheritance") { + const withoutOwner = (await readFile(config, "utf8")).replace( + /^[ \t]*worktree[ \t]*=.*\n/im, + "", + ); + await writeFile( + config, + `${withoutOwner}\n[DEFAULT]\n\tworktree = ${nested}\n`, + ); + } else if ( + ownership === "unquoted-escape" || + tabOwnership || + controlOwnership + ) { + await writeFile( + config, + (await readFile(config, "utf8")).replace( + /^([ \t]*worktree[ \t]*=).*$/im, + ownership === "quoted-tab-owned" + ? `$1 "${nested.replaceAll("\t", "\\t")}"` + : `$1 ${nested}`, + ), + ); + } + const configuredOwner = + ownership === "short-worktree" ? windowsShortPath(nested) : effective; + if (configuredOwner === null) return; + const override = `[${ownership === "mixed-case-external" ? "Core" : "core"}]\n\tworktree = ${configuredOwner}\n`; + if (ownership === "disabled-symlink") { + const unused = join(dirname(checkout), "unused.config"); + await writeFile(unused, override); + await symlink(unused, join(metadata, "config.worktree")); + } else if ( + ownership === "disabled-quoted" || + ownership === "disabled-carriage" + ) { + await mkdir(join(metadata, "config.worktree")); + } else { + await writeFile(join(metadata, "config.worktree"), override); + } + if (ownership === "case-alias" || ownership === "short-alias") { + const alias = + ownership === "case-alias" + ? metadata.toUpperCase() + : windowsShortPath(metadata); + if (alias === null) return; + const equivalent = await realpath(alias).then( + async (resolved) => resolved === (await realpath(metadata)), + () => false, + ); + if (!equivalent) return; + await writeFile(join(nested, ".git"), `gitdir: ${alias}\n`); + } + + if ( + ownership === "external" || + ownership === "mixed-case-external" || + ownership.endsWith("comment-override") || + ownership === "indented-override" || + ownership === "inline-section-override" || + ownership === "inline-carriage-override" || + ownership === "chained-section-override" || + ownership.startsWith("carriage-return") || + ownership === "default-inheritance" || + ownership === "unquoted-escape" || + ownership.startsWith("literal-tab") + ) { + await expect(inventory(checkout)).rejects.toThrow( + "Git metadata directory does not own selected worktree", + ); + } else { + expect(await inventory(checkout)).toContain( + `./${basename(nested)}/visible.ts`, + ); + } + }, + ); + + test + .skipIf(process.platform === "win32") + .each(["symbolic", "missing", "quoted"])( + "validates %s carriage-return Git worktree normalization", + async (ownership) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const nested = join(checkout, "nested"); + const metadata = join(checkout, ".git", "modules", "nested"); + const original = join(checkout, ".git", "owned\rmetadata"); + const normalized = join(checkout, ".git", "owned metadata"); + const external = join(dirname(checkout), "external-worktree"); + await mkdir(dirname(metadata), { recursive: true }); + await mkdir(original); + await mkdir(external); + if (ownership !== "missing") await symlink(external, normalized); + execFileSync( + "git", + ["init", "-q", "--separate-git-dir", metadata, nested], + { cwd: checkout }, + ); + execFileSync("git", ["-C", nested, "config", "core.worktree", nested]); + await writeFile(join(nested, "visible.ts"), "tracked\n"); + execFileSync("git", ["-C", nested, "add", "visible.ts"]); + const config = join(metadata, "config"); + const configured = `${original}/../../nested`; + await writeFile( + config, + (await readFile(config, "utf8")).replace( + /^([ \t]*worktree[ \t]*=).*$/im, + `$1 ${ownership === "quoted" ? `"${configured}"` : configured}`, + ), + ); + + if (ownership === "quoted") { + expect(await inventory(checkout)).toContain("./nested/visible.ts"); + } else { + await expect(inventory(checkout)).rejects.toThrow( + ownership === "symbolic" + ? "symbolic Git metadata paths are not supported" + : "Git metadata directory does not own selected worktree", + ); + } + }, + ); + + test("rejects unrelated external Git common directories", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const external = await repository(); + await writeFile(join(checkout, "visible.ts"), "visible\n"); + await writeFile( + join(checkout, ".git", "commondir"), + `${join(external, ".git")}\n`, + ); + + await expect(inventory(checkout)).rejects.toThrow( + "Git common directory does not own selected worktree", + ); + }); + + test("binds Git discovery to the selected checkout despite external core.worktree", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const external = join(dirname(checkout), "external-worktree"); + const trace = join(dirname(checkout), "git-trace.log"); + await mkdir(external); + await writeFile(join(checkout, "visible.ts"), "visible\n"); + execFileSync("git", ["config", "--local", "core.worktree", external], { + cwd: checkout, + }); + + expect( + await inventory(checkout, ".", { + ...process.env, + GIT_TRACE: trace, + GIT_TRACE_SETUP: "1", + }), + ).toEqual(["./visible.ts"]); + expect(await readFile(trace, "utf8")).not.toContain(external); + }); + + test + .skipIf(process.platform === "win32") + .each(["objects", "objects/info/alternates"])( + "rejects symbolic Git object metadata at %s", + async (relative) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const external = await repository(); + const metadata = join(checkout, ".git", relative); + const target = + relative === "objects" + ? join(external, ".git", "objects") + : join(dirname(checkout), "external-alternates"); + if (relative !== "objects") await writeFile(target, "external\n"); + await rm(metadata, { recursive: relative === "objects", force: true }); + await symlink(target, metadata); + + await expect(inventory(checkout)).rejects.toThrow( + "symbolic Git metadata paths are not supported", + ); + }, + ); + + test("rejects external Git object alternates before invoking Git", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const external = await repository(); + const trace = join(dirname(checkout), "git-trace.log"); + await writeFile( + join(checkout, ".git", "objects", "info", "alternates"), + `${join(external, ".git", "objects")}\n`, + ); + + await expect( + inventory(checkout, ".", { ...process.env, GIT_TRACE: trace }), + ).rejects.toThrow("external Git object alternates are not supported"); + await expect(readFile(trace, "utf8")).rejects.toThrow(); + }); + + test.each(["pack", "ab"])( + "rejects symbolic Git object-store %s directories", + async (name) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const external = await repository(); + const internal = join(checkout, ".git", "extra-objects"); + const target = join(external, ".git", "objects", name); + await mkdir(join(internal, "info"), { recursive: true }); + if (name !== "pack") await mkdir(target); + await symlink( + target, + join(internal, name), + process.platform === "win32" ? "junction" : "dir", + ); + await writeFile( + join(checkout, ".git", "objects", "info", "alternates"), + `${internal}\n`, + ); + + await expect(inventory(checkout)).rejects.toThrow( + "symbolic Git metadata paths are not supported", + ); + }, + ); + + test.each(["primary", "alternate"])( + "rejects symbolic incremental %s Git multi-pack-index directories", + async (owner) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const external = await repository(); + const objects = + owner === "primary" + ? join(checkout, ".git", "objects") + : join(checkout, ".git", "extra-objects"); + if (owner === "alternate") { + await mkdir(join(objects, "info"), { recursive: true }); + await mkdir(join(objects, "pack")); + await writeFile( + join(checkout, ".git", "objects", "info", "alternates"), + `${objects}\n`, + ); + } + const target = join( + external, + ".git", + "objects", + "pack", + "multi-pack-index.d", + ); + await mkdir(target); + await symlink( + target, + join(objects, "pack", "multi-pack-index.d"), + process.platform === "win32" ? "junction" : "dir", + ); + + await expect(inventory(checkout)).rejects.toThrow( + "symbolic Git metadata paths are not supported", + ); + }, + ); + + test + .skipIf(process.platform === "win32") + .each(["chain", "midx", "bitmap", "rev"])( + "rejects symbolic incremental Git multi-pack-index %s files", + async (kind) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const directory = join( + checkout, + ".git", + "objects", + "pack", + "multi-pack-index.d", + ); + const target = join(dirname(checkout), "external-index"); + await mkdir(directory); + await writeFile(target, "external\n"); + const name = + kind === "chain" + ? "multi-pack-index-chain" + : `multi-pack-index-${"a".repeat(40)}.${kind}`; + await symlink(target, join(directory, name)); + + await expect(inventory(checkout)).rejects.toThrow( + "symbolic Git metadata paths are not supported", + ); + }, + ); + + test("inventories genuine incremental Git multi-pack indexes", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + await writeFile(join(checkout, "visible.ts"), "tracked\n"); + execFileSync("git", ["add", "visible.ts"], { cwd: checkout }); + commit(checkout); + execFileSync("git", ["repack", "-ad"], { cwd: checkout, stdio: "ignore" }); + try { + execFileSync("git", ["multi-pack-index", "write", "--incremental"], { + cwd: checkout, + stdio: "pipe", + }); + } catch (error) { + const stderr = String( + (error as Error & { stderr?: Buffer }).stderr ?? "", + ); + if (/unknown|unrecognized/i.test(stderr)) return; + throw error; + } + await mkdir( + join( + checkout, + ".git", + "objects", + "pack", + "multi-pack-index.d", + "unrelated-dir", + ), + ); + + expect(await inventory(checkout)).toContain("./visible.ts"); + }); + + test.skipIf(process.platform === "win32").each([ + ["primary", "pack"], + ["primary-uppercase", "pack"], + ["primary-arbitrary-pack", "pack"], + ["primary-arbitrary-index", "pack"], + ["primary-midx", "pack"], + ["primary-uppercase-midx", "pack"], + ["primary-uppercase-extension", "pack"], + ["primary", "ab"], + ["primary-uppercase", "ab"], + ["alternate", "pack"], + ["alternate-uppercase", "pack"], + ["alternate-arbitrary-pack", "pack"], + ["alternate-arbitrary-index", "pack"], + ["alternate-midx", "pack"], + ["alternate-uppercase-midx", "pack"], + ["alternate-uppercase-extension", "pack"], + ["alternate", "ab"], + ["alternate-uppercase", "ab"], + ])("rejects symbolic %s Git object files in %s", async (owner, kind) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const external = join(dirname(checkout), "external-object"); + await writeFile(external, "external\n"); + const objects = owner.startsWith("primary") + ? join(checkout, ".git", "objects") + : join(checkout, ".git", "extra-objects"); + if (owner.startsWith("alternate")) { + await mkdir(join(objects, "info"), { recursive: true }); + await mkdir(join(objects, "pack")); + await writeFile( + join(checkout, ".git", "objects", "info", "alternates"), + `${objects}\n`, + ); + } + const directory = join(objects, kind); + if (kind !== "pack") await mkdir(directory); + const hex = owner.endsWith("uppercase") ? "A" : "0"; + const basename = owner.includes("arbitrary") + ? "arbitrary" + : `pack-${hex.repeat(40)}`; + const suffix = owner.endsWith("index") + ? "idx" + : owner.endsWith("extension") + ? "PACK" + : "pack"; + const member = + kind !== "pack" + ? hex.repeat(38) + : owner.endsWith("midx") + ? owner.includes("uppercase") + ? "MULTI-PACK-INDEX" + : "multi-pack-index" + : `${basename}.${suffix}`; + await symlink(external, join(directory, member)); + if ( + (kind !== "pack" && hex === "A") || + member === "MULTI-PACK-INDEX" || + member.endsWith(".PACK") + ) { + const aliases = await realpath( + join(directory, member.toLowerCase()), + ).then( + () => true, + () => false, + ); + if (!aliases) { + await writeFile(join(checkout, "visible.ts"), "visible\n"); + expect(await inventory(checkout)).toContain("./visible.ts"); + return; + } + } + + await expect(inventory(checkout)).rejects.toThrow( + "symbolic Git metadata paths are not supported", + ); + }); + + test.each(["pack", "ab"])( + "allows unrelated tooling directories inside Git object %s", + async (kind) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const directory = join(checkout, ".git", "objects", kind); + if (kind !== "pack") await mkdir(directory); + await mkdir(join(directory, "unrelated-dir")); + await writeFile(join(checkout, "visible.ts"), "visible\n"); + + expect(await inventory(checkout)).toContain("./visible.ts"); + }, + ); + + test.skipIf(process.platform === "win32").each(["PACK", "AB"])( + "ignores unrelated uppercase Git object-store name %s", + async (name) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const external = await repository(); + const objects = join(checkout, ".git", "objects"); + if (name === "AB") await mkdir(join(objects, "ab")); + try { + await symlink(join(external, ".git", "objects"), join(objects, name)); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "EEXIST") return; + throw error; + } + await writeFile(join(checkout, "visible.ts"), "visible\n"); + + expect(await inventory(checkout)).toContain("./visible.ts"); + }, + ); + + test.skipIf(process.platform === "win32")( + "preserves carriage returns in Git object-alternate paths", + async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const external = await repository(); + const internal = join(checkout, ".git", "safe-objects"); + await mkdir(join(internal, "info"), { recursive: true }); + await mkdir(join(internal, "pack")); + await symlink(join(external, ".git", "objects"), `${internal}\r`); + await writeFile( + join(checkout, ".git", "objects", "info", "alternates"), + `${internal}\r\n`, + ); + + await expect(inventory(checkout)).rejects.toThrow( + "symbolic Git metadata paths are not supported", + ); + }, + ); + + test("rejects external transitive Git object alternates before invoking Git", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const external = await repository(); + const internal = join(checkout, ".git", "extra-objects"); + const trace = join(dirname(checkout), "git-trace.log"); + await mkdir(join(internal, "info"), { recursive: true }); + await mkdir(join(internal, "pack")); + await writeFile( + join(checkout, ".git", "objects", "info", "alternates"), + `${internal}\n`, + ); + await writeFile( + join(internal, "info", "alternates"), + `${join(external, ".git", "objects")}\n`, + ); + + await expect( + inventory(checkout, ".", { ...process.env, GIT_TRACE: trace }), + ).rejects.toThrow("external Git object alternates are not supported"); + await expect(readFile(trace, "utf8")).rejects.toThrow(); + }); + + test.each(["primary", "transitive"])( + "rejects symbolic %s Git alternate prefixes before probing ownership", + async (kind) => { + const parent = await repository(false); + const checkout = join(parent, "selected", "nested"); + await mkdir(checkout, { recursive: true }); + execFileSync("git", ["init", "-q"], { cwd: checkout }); + const outside = join(dirname(parent), "outside"); + await mkdir(join(outside, "target"), { recursive: true }); + const hop = join(parent, "hop"); + await symlink( + outside, + hop, + process.platform === "win32" ? "junction" : "dir", + ); + const separator = process.platform === "win32" ? "\\" : "/"; + const alternate = [ + join(hop, "target"), + "..", + "selected", + "nested", + ".git", + "extra-objects", + ].join(separator); + const alternates = join( + checkout, + ".git", + "objects", + "info", + "alternates", + ); + if (kind === "primary") { + await writeFile(alternates, `${alternate}\n`); + } else { + const first = join(checkout, ".git", "first-objects"); + await mkdir(join(first, "info"), { recursive: true }); + await mkdir(join(first, "pack")); + await writeFile(alternates, `${first}\n`); + await writeFile(join(first, "info", "alternates"), `${alternate}\n`); + } + + const instrumentation = join(dirname(parent), "instrumentation"); + await mkdir(instrumentation); + await writeFile( + join(instrumentation, "sitecustomize.py"), + [ + "from pathlib import Path", + `unsafe = Path(${JSON.stringify(join(hop, "target"))})`, + "original = Path.stat", + "def guarded(self, *args, **kwargs):", + " if self == unsafe:", + " raise RuntimeError('probed unvalidated Git alternate prefix')", + " return original(self, *args, **kwargs)", + "Path.stat = guarded", + ].join("\n"), + ); + + await expect( + inventory(checkout, ".", { + ...process.env, + PYTHONPATH: instrumentation, + }), + ).rejects.toThrow("symbolic Git metadata paths are not supported"); + }, + ); + + test.skipIf(process.platform === "win32").each(["primary", "transitive"])( + "rejects symbolic %s Git object-alternate hops before parent traversal", + async (kind) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const outside = join(dirname(checkout), "outside"); + const external = join(outside, "store"); + const decoy = join(checkout, ".git", "store"); + const hop = join(checkout, ".git", "hop"); + await mkdir(join(outside, "hop-target"), { recursive: true }); + for (const objects of [external, decoy]) { + await mkdir(join(objects, "info"), { recursive: true }); + await mkdir(join(objects, "pack")); + } + await symlink(join(outside, "hop-target"), hop); + const alternate = `${hop}/../store`; + if (kind === "primary") { + await writeFile( + join(checkout, ".git", "objects", "info", "alternates"), + `${alternate}\n`, + ); + } else { + const first = join(checkout, ".git", "first-objects"); + await mkdir(join(first, "info"), { recursive: true }); + await mkdir(join(first, "pack")); + await writeFile( + join(checkout, ".git", "objects", "info", "alternates"), + `${first}\n`, + ); + await writeFile(join(first, "info", "alternates"), `${alternate}\n`); + } + + await expect(inventory(checkout)).rejects.toThrow( + "symbolic Git metadata paths are not supported", + ); + }, + ); + + test.each(["\\x61", "\\400"])( + "rejects quoted Git object alternates with unsupported escape %s", + async (escape) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const objects = join(checkout, ".git", "extra-objects"); + await writeFile( + join(checkout, ".git", "objects", "info", "alternates"), + `"${objects}${escape}"\n`, + ); + + await expect(inventory(checkout)).rejects.toThrow( + "invalid Git object alternate paths", + ); + }, + ); + + test("rejects differently cased sibling Git object alternates", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const sibling = join(dirname(checkout), "REPOSITORY"); + try { + await mkdir(sibling); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "EEXIST") return; + throw error; + } + execFileSync("git", ["init", "-q"], { cwd: sibling }); + await writeFile( + join(checkout, ".git", "objects", "info", "alternates"), + `${join(sibling, ".git", "objects")}\n`, + ); + + await expect(inventory(checkout)).rejects.toThrow( + "external Git object alternates are not supported", + ); + }); + + test("allows repository-owned Git object alternates", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const objects = join(checkout, ".git", "extra objects"); + await mkdir(join(objects, "info"), { recursive: true }); + await mkdir(join(objects, "pack")); + await writeFile( + join(checkout, ".git", "objects", "info", "alternates"), + `${JSON.stringify(objects).replace("extra objects", "extra\\040objects")}\n`, + ); + await writeFile(join(checkout, "visible.ts"), "visible\n"); + + expect(await inventory(checkout)).toContain("./visible.ts"); + }); + + test.each(["quoted", "unquoted"])( + "allows %s CRLF-terminated repository-owned Git object alternates", + async (format) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const objects = join(checkout, ".git", "extra objects"); + await mkdir(join(objects, "info"), { recursive: true }); + await mkdir(join(objects, "pack")); + await writeFile( + join(checkout, ".git", "objects", "info", "alternates"), + `${format === "quoted" ? JSON.stringify(objects) : objects}\r\n`, + ); + await writeFile(join(checkout, "visible.ts"), "visible\n"); + + expect(await inventory(checkout)).toContain("./visible.ts"); + }, + ); + + test("allows safe parent traversal to repository-owned Git object alternates", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const objects = join(checkout, ".git", "extra-objects"); + await mkdir(join(objects, "info"), { recursive: true }); + await mkdir(join(objects, "pack")); + await writeFile( + join(checkout, ".git", "objects", "info", "alternates"), + "../extra-objects\n", + ); + await writeFile(join(checkout, "visible.ts"), "visible\n"); + + expect(await inventory(checkout)).toContain("./visible.ts"); + }); + + test.each(["case-alias", "short-alias"])( + "allows %s repository-owned Git object alternate paths", + async (kind) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const objects = join(checkout, ".git", "extra-objects"); + await mkdir(join(objects, "info"), { recursive: true }); + await mkdir(join(objects, "pack")); + const alias = + kind === "case-alias" + ? objects.toUpperCase() + : windowsShortPath(objects); + if (alias === null) return; + const equivalent = await realpath(alias).then( + async (resolved) => resolved === (await realpath(objects)), + () => false, + ); + if (!equivalent) return; + await writeFile( + join(checkout, ".git", "objects", "info", "alternates"), + `${alias}\n`, + ); + await writeFile(join(checkout, "visible.ts"), "visible\n"); + + expect(await inventory(checkout)).toContain("./visible.ts"); + }, + ); + + test("allows repository-owned transitive Git object alternates", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const first = join(checkout, ".git", "first-objects"); + const second = join(checkout, ".git", "second-objects"); + for (const objects of [first, second]) { + await mkdir(join(objects, "info"), { recursive: true }); + await mkdir(join(objects, "pack")); + } + await writeFile( + join(checkout, ".git", "objects", "info", "alternates"), + `${first}\n`, + ); + await writeFile( + join(first, "info", "alternates"), + `${JSON.stringify(second)}\n`, + ); + await writeFile(join(second, "info", "alternates"), `${first}\n`); + await writeFile(join(checkout, "visible.ts"), "visible\n"); + + expect(await inventory(checkout)).toContain("./visible.ts"); + }); + + test.skipIf(process.platform === "win32")( + "rejects escaped Git index entries before probing sibling metadata", + async () => { + const git = Bun.which("git"); + if (Bun.which("rg") === null || git === null) return; + + const checkout = await repository(); + const outside = join(dirname(checkout), "outside"); + const wrappers = join(dirname(checkout), "bin"); + await mkdir(outside); + await mkdir(wrappers); + execFileSync("git", ["init", "-q"], { cwd: outside }); + await writeFile(join(checkout, "source.ts"), "visible\n"); + const wrapper = join(wrappers, "git"); + await writeFile( + wrapper, + `#!/bin/sh\ncase " $* " in\n *" ls-files --sparse --cached "*) printf '../outside\\000' ;;\n *) exec ${JSON.stringify(git)} "$@" ;;\nesac\n`, + ); + await chmod(wrapper, 0o755); + + await expect( + inventory(checkout, ".", { + ...process.env, + PATH: `${wrappers}:${process.env["PATH"] ?? ""}`, + }), + ).rejects.toThrow("out-of-scope Git inventory paths are not supported"); + }, + ); + + test.skipIf(process.platform === "win32")( + "disables lazy Git object fetching and replacement during inventory", + async () => { + const git = Bun.which("git"); + if (Bun.which("rg") === null || git === null) return; + + const checkout = await repository(); + const wrappers = join(dirname(checkout), "bin"); + const trace = join(dirname(checkout), "lazy-fetch.log"); + await mkdir(wrappers); + await writeFile(join(checkout, "visible.ts"), "visible\n"); + const wrapper = join(wrappers, "git"); + await writeFile( + wrapper, + `#!/bin/sh\nprintf '%s:%s\\n' "$GIT_NO_LAZY_FETCH" "$GIT_NO_REPLACE_OBJECTS" >> ${JSON.stringify(trace)}\nexec ${JSON.stringify(git)} "$@"\n`, + ); + await chmod(wrapper, 0o755); + + expect( + await inventory(checkout, ".", { + ...process.env, + GIT_NO_LAZY_FETCH: "0", + GIT_NO_REPLACE_OBJECTS: "0", + PATH: `${wrappers}:${process.env["PATH"] ?? ""}`, + }), + ).toContain("./visible.ts"); + expect((await readFile(trace, "utf8")).trim().split("\n")).toSatisfy( + (values: string[]) => + values.length > 0 && values.every((value) => value === "1:1"), + ); + }, + ); + + test.each(["selected", "nested"])( + "rejects symbolic ancestors in %s Git-listed paths before reading external metadata", + async (kind) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const owner = kind === "selected" ? checkout : join(checkout, "nested"); + if (kind === "nested") { + await mkdir(owner); + execFileSync("git", ["init", "-q"], { cwd: owner }); + } + const link = join(owner, "link"); + await mkdir(link); + await writeFile(join(link, "nested"), "tracked\n"); + execFileSync("git", ["add", "link/nested"], { cwd: owner }); + + const external = await repository(); + const nested = join(external, "nested"); + await mkdir(nested); + execFileSync("git", ["init", "-q"], { cwd: nested }); + await writeFile( + join(nested, ".git", "config"), + "[include]\n\tpath = outside\n", + ); + await rm(link, { recursive: true }); + await symlink( + external, + link, + process.platform === "win32" ? "junction" : "dir", + ); + + await expect(inventory(checkout)).rejects.toThrow( + "symbolic Git inventory paths are not supported", + ); + }, + ); + + test.skipIf(process.platform !== "win32")( + "rejects drive-relative Git index entries before probing another drive", + async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const original = "D-checkout"; + await writeFile(join(checkout, original), "tracked\n"); + execFileSync("git", ["add", original], { cwd: checkout }); + const indexPath = join(checkout, ".git", "index"); + const index = await readFile(indexPath); + const offset = index.indexOf(Buffer.from(`${original}\0`)); + if (offset === -1) + throw new Error("Expected the staged Git index entry."); + index.write("D:checkout", offset, "utf8"); + createHash("sha1") + .update(index.subarray(0, index.length - 20)) + .digest() + .copy(index, index.length - 20); + await writeFile(indexPath, index); + + await expect(inventory(checkout)).rejects.toThrow( + "out-of-scope Git inventory paths are not supported", + ); + }, + ); + + test.each([ + "shared-index", + "shared-index-v4", + "shared-index-sha256", + "shared-index-worktree-sha256", + "shared-index-sha256-worktree-sha1", + "multi-pack-index", + "pack-index-suffix", + "incremental-index-directory", + "incremental-index-chain", + "incremental-index-bitmap", + ...(process.platform === "win32" + ? [] + : [ + "object-store-trailing-dot", + "object-store-trailing-space", + "object-prefix-trailing-dot", + "pack-index-trailing-dot", + "pack-index-trailing-space", + "multi-pack-index-trailing-dot", + "incremental-index-directory-trailing-space", + "incremental-index-chain-trailing-dot", + "incremental-index-bitmap-trailing-space", + ]), + ])("rejects Windows-compatible %s metadata aliases", async (kind) => { + const sha256 = kind.startsWith("shared-index-sha256"); + const checkout = await repository(!sha256); + if (sha256) { + execFileSync("git", ["init", "-q", "--object-format=sha256"], { + cwd: checkout, + }); + } + const gitdir = join(checkout, ".git"); + let directory = join(gitdir, "objects", "pack"); + let canonical = "multi-pack-index"; + + if (kind.startsWith("shared-index")) { + await writeFile(join(checkout, "tracked.ts"), "tracked\n"); + execFileSync("git", ["add", "tracked.ts"], { cwd: checkout }); + if (kind === "shared-index-v4") { + execFileSync("git", ["update-index", "--index-version=4"], { + cwd: checkout, + }); + } + execFileSync("git", ["update-index", "--split-index"], { + cwd: checkout, + }); + if (kind.includes("-worktree-")) { + execFileSync("git", ["config", "extensions.worktreeConfig", "true"], { + cwd: checkout, + }); + await writeFile( + join(gitdir, "config.worktree"), + `[extensions]\n\tobjectFormat = ${sha256 ? "sha1" : "sha256"}\n`, + ); + } + const shared = (await readdir(gitdir)).find((name) => + name.startsWith("sharedindex."), + ); + if (shared === undefined) throw new Error("Expected a split Git index."); + directory = gitdir; + canonical = shared; + await rm(join(directory, canonical)); + } else if (kind.startsWith("object-store")) { + directory = join(gitdir, "objects"); + canonical = "pack"; + await rm(join(directory, canonical), { recursive: true }); + } else if (kind.startsWith("object-prefix")) { + directory = join(gitdir, "objects"); + canonical = "aa"; + } else if (kind.startsWith("pack-index")) { + canonical = `pack-${"a".repeat(40)}.idx`; + } else if (kind.startsWith("incremental-index-directory")) { + canonical = "multi-pack-index.d"; + } else if (kind.startsWith("incremental-index-")) { + directory = join(directory, "multi-pack-index.d"); + await mkdir(directory); + canonical = kind.startsWith("incremental-index-chain") + ? "multi-pack-index-chain" + : `multi-pack-index-${"a".repeat(40)}.bitmap`; + } + + const alias = kind.endsWith("-trailing-dot") + ? `${canonical}.` + : kind.endsWith("-trailing-space") + ? `${canonical} ` + : canonical.replace("i", "\u0131"); + const external = join(dirname(checkout), "external-metadata"); + await mkdir(external); + await symlink( + external, + join(directory, alias), + process.platform === "win32" ? "junction" : "dir", + ); + + const instrumentation = join(dirname(checkout), "instrumentation"); + await mkdir(instrumentation); + await writeFile( + join(instrumentation, "sitecustomize.py"), + [ + "from pathlib import Path", + `canonical = Path(${JSON.stringify(join(directory, canonical))})`, + `alias = Path(${JSON.stringify(join(directory, alias))})`, + "original = Path.stat", + "def guarded(self, *args, **kwargs):", + " if self == canonical and kwargs.get('follow_symlinks') is False:", + " return original(alias, *args, **kwargs)", + " return original(self, *args, **kwargs)", + "Path.stat = guarded", + ].join("\n"), + ); + + await expect( + inventory(checkout, ".", { + ...process.env, + PYTHONPATH: instrumentation, + }), + ).rejects.toThrow("symbolic Git metadata paths are not supported"); + }); + + test.each([ + ["missing", "symbolic"], + ["normal", "symbolic"], + ["normal", "fifo"], + ["normal", "directory"], + ["split", "symbolic"], + ["split-v4", "symbolic"], + ["split-sha256", "symbolic"], + ])("ignores stale %s split-index %s metadata", async (mode, kind) => { + if ( + Bun.which("rg") === null || + process.platform === "win32" || + (kind === "fifo" && Bun.which("mkfifo") === null) + ) { + return; + } + + const sha256 = mode === "split-sha256"; + const checkout = await repository(!sha256); + if (sha256) { + execFileSync("git", ["init", "-q", "--object-format=sha256"], { + cwd: checkout, + }); + } + await writeFile(join(checkout, "tracked.ts"), "tracked\n"); + if (mode !== "missing") { + execFileSync("git", ["add", "tracked.ts"], { cwd: checkout }); + } + if (mode.startsWith("split")) { + if (mode === "split-v4") { + execFileSync("git", ["update-index", "--index-version=4"], { + cwd: checkout, + }); + } + execFileSync("git", ["update-index", "--split-index"], { + cwd: checkout, + }); + } + + const stale = join( + checkout, + ".git", + `sharedindex.${"f".repeat(sha256 ? 64 : 40)}`, + ); + if (kind === "directory") { + await mkdir(stale); + } else if (kind === "fifo") { + execFileSync("mkfifo", [stale]); + } else { + const external = join(dirname(checkout), "unused-shared-index"); + await writeFile(external, "inactive\n"); + await symlink(external, stale); + } + + expect(await inventory(checkout)).toContain("./tracked.ts"); + }); + + test.skipIf(process.platform === "win32").each(["lowercase", "uppercase"])( + "rejects %s split-index backing files that leave the checkout", + async (casing) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + await writeFile(join(checkout, "tracked.ts"), "tracked\n"); + execFileSync("git", ["add", "tracked.ts"], { cwd: checkout }); + execFileSync("git", ["update-index", "--split-index"], { + cwd: checkout, + }); + const gitdir = join(checkout, ".git"); + const shared = (await readdir(gitdir)).find((name) => + name.startsWith("sharedindex."), + ); + if (shared === undefined) throw new Error("Expected a split Git index."); + await mkdir(join(gitdir, "sharedindex.notes")); + + expect(await inventory(checkout)).toContain("./tracked.ts"); + const original = join(gitdir, shared); + const external = join(dirname(checkout), shared); + await writeFile(external, await readFile(original)); + await rm(original); + const replacement = + casing === "uppercase" ? join(gitdir, shared.toUpperCase()) : original; + await symlink(external, replacement); + if ( + casing === "uppercase" && + !(await realpath(original).then( + () => true, + () => false, + )) + ) { + return; + } + + await expect(inventory(checkout)).rejects.toThrow( + "symbolic Git metadata paths are not supported", + ); + }, + ); + + test.each([ + ["include", "without BOM"], + ['includeIf "gitdir:**"', "without BOM"], + ["include", "with BOM"], + ["include", "with leading carriage return"], + ["include", "with carriage return after header"], + ["include", "with same-line path"], + ["include", "with same-line carriage path"], + ["include", "with chained section headers"], + ['includeIf "gitdir:**"', "with same-line path"], + ['includeIf "gitdir:**"', "with chained section headers"], + ['includeIf "gitdir:**"', "with carriage return before condition"], + [ + 'includeIf "gitdir:**"', + "with carriage return after condition whitespace", + ], + [ + 'includeIf "gitdir:**"', + "with carriage return replacing condition whitespace", + ], + ])( + "rejects repository-directed %s config %s before invoking Git", + async (section, bom) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const external = join(dirname(checkout), "external.config"); + await writeFile(external, "[core]\n\tignoreCase = true\n"); + const config = join(checkout, ".git", "config"); + const whitespace = + bom === "with carriage return before condition" + ? "\r " + : bom === "with carriage return after condition whitespace" + ? " \r" + : bom === "with carriage return replacing condition whitespace" + ? "\r" + : " "; + const configuredSection = section.replace(" ", whitespace); + const headers = + bom === "with chained section headers" + ? `[0][-][.legacy][ "quoted"][feature][unused.value][${configuredSection}]` + : `[${configuredSection}]`; + const assignment = + bom === "with same-line carriage path" + ? "\rpath" + : bom === "with same-line path" || + bom === "with chained section headers" + ? "path" + : "\n\tpath"; + await writeFile( + config, + `${bom === "with BOM" ? "\ufeff" : ""}${bom === "with leading carriage return" ? "\r" : ""}${headers}${bom === "with carriage return after header" ? "\r# included" : ""}${assignment} = ${external}\n${await readFile(config, "utf8")}`, + ); + + await expect(inventory(checkout)).rejects.toThrow( + "Git config includes are not supported", + ); + }, + ); + + test.each([ + ["include", "foo = bar"], + ["include", "# path = ignored"], + ['includeIf "gitdir:**"', "foo = bar"], + ['include "inactive"', "path = ignored"], + ])("allows inert [%s] Git config sections", async (section, assignment) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const config = join(checkout, ".git", "config"); + await writeFile( + config, + `[${section}]\n\t${assignment}\n${await readFile(config, "utf8")}`, + ); + await writeFile(join(checkout, "visible.ts"), "visible\n"); + + expect(await inventory(checkout)).toContain("./visible.ts"); + }); + + test.each([ + ["unset", "directory", false], + ["false", "symbolic", false], + ["false", "fifo", false], + ["00", "directory", false], + ["+0", "symbolic", false], + ["0k", "fifo", false], + ["0x0", "directory", false], + ["+0x00g", "directory", false], + ["1k", "directory", true], + ["true", "directory", true], + ["worktree-false", "directory", false], + ["worktree-true", "directory", true], + ])( + "inspects %s sparse-checkout %s metadata only when active", + async (setting, kind, active) => { + if ( + Bun.which("rg") === null || + (kind !== "directory" && process.platform === "win32") || + (kind === "fifo" && Bun.which("mkfifo") === null) + ) { + return; + } + + const checkout = await repository(); + if (setting.startsWith("worktree-")) { + execFileSync( + "git", + ["config", "core.sparseCheckout", active ? "false" : "true"], + { cwd: checkout }, + ); + execFileSync("git", ["config", "extensions.worktreeConfig", "true"], { + cwd: checkout, + }); + execFileSync( + "git", + ["config", "--worktree", "core.sparseCheckout", String(active)], + { cwd: checkout }, + ); + } else if (setting !== "unset") { + execFileSync("git", ["config", "core.sparseCheckout", setting], { + cwd: checkout, + }); + } + + const metadata = join(checkout, ".git", "info", "sparse-checkout"); + if (kind === "symbolic") { + const external = join(dirname(checkout), "unused-sparse-checkout"); + await writeFile(external, "external\n"); + await symlink(external, metadata); + } else if (kind === "fifo") { + execFileSync("mkfifo", [metadata]); + } else { + await mkdir(metadata); + } + await writeFile(join(checkout, "visible.ts"), "visible\n"); + + if (active) { + await expect(inventory(checkout)).rejects.toThrow( + "non-regular Git metadata files are not supported", + ); + } else { + expect(await inventory(checkout)).toContain("./visible.ts"); + } + }, + ); + + test + .skipIf(process.platform === "win32") + .each(["config", "info/sparse-checkout"])( + "rejects non-regular Git %s metadata before invoking Git", + async (relative) => { + if (Bun.which("rg") === null || Bun.which("mkfifo") === null) return; + + const checkout = await repository(); + if (relative === "info/sparse-checkout") { + execFileSync("git", ["config", "core.sparseCheckout", "true"], { + cwd: checkout, + }); + } + const metadata = join(checkout, ".git", relative); + await rm(metadata, { force: true }); + execFileSync("mkfifo", [metadata]); + + await expect(inventory(checkout)).rejects.toThrow( + "non-regular Git metadata files are not supported", + ); + }, + ); + + test("requires the Git info metadata path to be a directory", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const info = join(checkout, ".git", "info"); + await rm(info, { recursive: true }); + await writeFile(info, "not a directory\n"); + + await expect(inventory(checkout)).rejects.toThrow( + "non-directory Git metadata paths are not supported", + ); + }); + + test + .skipIf(process.platform === "win32") + .each([ + "index", + "config", + "info/exclude", + "info/sparse-checkout", + "packed-refs", + "refs", + "refs/heads", + ])("rejects a symbolic Git metadata %s", async (relative) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + if (relative === "info/sparse-checkout") { + execFileSync("git", ["config", "core.sparseCheckout", "true"], { + cwd: checkout, + }); + } + const external = await repository(); + await writeFile(join(external, "source.ts"), "tracked\n"); + execFileSync("git", ["add", "source.ts"], { cwd: external }); + const metadata = join(checkout, ".git", relative); + const directory = relative === "refs" || relative.startsWith("refs/"); + const target = join(external, ".git", relative); + if (relative === "info/sparse-checkout" || relative === "packed-refs") { + await writeFile(target, "external\n"); + } + await rm(metadata, { recursive: directory, force: true }); + await symlink(target, metadata); + await writeFile(join(checkout, "visible.ts"), "visible\n"); + + await expect(inventory(checkout)).rejects.toThrow( + "symbolic Git metadata paths are not supported", + ); + }); + + test.each(["file", "symbolic", "fifo", "reference"])( + "ignores inactive Git replacement %s metadata", + async (kind) => { + if ( + Bun.which("rg") === null || + (kind !== "file" && process.platform === "win32") || + (kind === "fifo" && Bun.which("mkfifo") === null) + ) { + return; + } + + const checkout = await repository(); + const replacement = join(checkout, ".git", "refs", "replace"); + const external = join(dirname(checkout), "external-replacement"); + await writeFile(external, `${"0".repeat(40)}\n`); + if (kind === "file") { + await writeFile(replacement, "inactive\n"); + } else if (kind === "symbolic") { + await symlink(external, replacement); + } else if (kind === "fifo") { + execFileSync("mkfifo", [replacement]); + } else { + await mkdir(replacement); + await symlink(external, join(replacement, "a".repeat(40))); + } + await writeFile(join(checkout, "visible.ts"), "visible\n"); + + expect(await inventory(checkout)).toContain("./visible.ts"); + }, + ); + + test("inventories linked worktrees with regular Git metadata", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + await writeFile(join(checkout, "visible.ts"), "visible\n"); + execFileSync("git", ["add", "visible.ts"], { cwd: checkout }); + commit(checkout); + const linked = join(dirname(checkout), "linked-worktree"); + execFileSync("git", ["worktree", "add", "--detach", linked, "HEAD"], { + cwd: checkout, + stdio: "ignore", + }); + + expect(await inventory(linked)).toContain("./visible.ts"); + + const gitdir = (await readFile(join(linked, ".git"), "utf8")) + .replace(/^gitdir: /, "") + .trim(); + await writeFile(join(gitdir, "objects"), "inactive worktree metadata\n"); + await mkdir(join(gitdir, "packed-refs")); + await writeFile( + join(gitdir, "refs", "heads"), + "inactive worktree references\n", + ); + await writeFile(join(gitdir, "config"), "[include]\npath = inactive\n"); + await mkdir(join(gitdir, "info", "exclude"), { recursive: true }); + expect(await inventory(linked)).toContain("./visible.ts"); + + const shortBackpointer = windowsShortPath(join(linked, ".git")); + if (shortBackpointer !== null) { + await writeFile(join(gitdir, "gitdir"), `${shortBackpointer}\n`); + expect(await inventory(linked)).toContain("./visible.ts"); + } + + const alternateBackpointer = join( + dirname(linked), + "LINKED-WORKTREE", + ".git", + ); + const equivalentBackpointer = await realpath(alternateBackpointer).then( + async (resolved) => resolved === (await realpath(join(linked, ".git"))), + () => false, + ); + if (equivalentBackpointer) { + await writeFile(join(gitdir, "gitdir"), `${alternateBackpointer}\n`); + expect(await inventory(linked)).toContain("./visible.ts"); + } + + const aliased = join(dirname(checkout), "REPOSITORY", ".git"); + const equivalent = await realpath(aliased).then( + async (resolved) => resolved === (await realpath(join(checkout, ".git"))), + () => false, + ); + if (!equivalent) return; + await writeFile(join(gitdir, "commondir"), `${aliased}\n`); + + expect(await inventory(linked)).toContain("./visible.ts"); + }); + + test.each(["owned", "external"])( + "honors %s worktree-specific ownership for linked Git worktrees", + async (ownership) => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + await writeFile(join(checkout, "visible.ts"), "tracked\n"); + execFileSync("git", ["add", "visible.ts"], { cwd: checkout }); + commit(checkout); + const linked = join(dirname(checkout), "linked-worktree"); + const external = join(dirname(checkout), "external-worktree"); + await mkdir(external); + execFileSync("git", ["worktree", "add", "--detach", linked, "HEAD"], { + cwd: checkout, + stdio: "ignore", + }); + execFileSync("git", ["config", "extensions.worktreeConfig", "true"], { + cwd: checkout, + }); + const metadata = execFileSync( + "git", + ["rev-parse", "--absolute-git-dir"], + { + cwd: linked, + encoding: "utf8", + }, + ).trim(); + const effective = ownership === "owned" ? linked : external; + await writeFile( + join(metadata, "config.worktree"), + `[Core]\n\tworktree = ${effective}\n`, + ); + + if (ownership === "external") { + await expect(inventory(linked)).rejects.toThrow( + "Git metadata directory does not own selected worktree", + ); + } else { + expect(await inventory(linked)).toContain("./visible.ts"); + } + }, + ); + + test("rejects worktree backpointers hard-linked through equivalent sibling names", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const source = await repository(); + const hidden = join(checkout, "ß"); + const visible = join(checkout, "ss"); + await writeFile(join(source, "secret.ts"), "tracked\n"); + execFileSync("git", ["add", "secret.ts"], { cwd: source }); + commit(source); + execFileSync("git", ["worktree", "add", "--detach", hidden, "HEAD"], { + cwd: source, + stdio: "ignore", + }); + try { + await mkdir(visible); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "EEXIST") return; + throw error; + } + await writeFile(join(checkout, ".ignore"), "ß/\n"); + await writeFile(join(visible, ".ignore"), "secret.ts\n"); + await writeFile(join(visible, "secret.ts"), "private\n"); + await hardlink(join(hidden, ".git"), join(visible, ".git")); + + await expect(inventory(checkout)).rejects.toThrow( + "Git metadata directory does not own selected worktree", + ); + }); + + test("inventories genuine Git submodules with internal metadata", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const source = await repository(); + await writeFile(join(source, "visible.ts"), "tracked\n"); + execFileSync("git", ["add", "visible.ts"], { cwd: source }); + commit(source); + execFileSync( + "git", + [ + "-c", + "protocol.file.allow=always", + "submodule", + "add", + "-q", + source, + "nested", + ], + { cwd: checkout }, + ); + const gitdir = execFileSync("git", ["rev-parse", "--absolute-git-dir"], { + cwd: join(checkout, "nested"), + encoding: "utf8", + }).trim(); + const config = join(gitdir, "config"); + const configured = (await readFile(config, "utf8")).replace( + /^([ \t]*worktree[ \t]*=[ \t]*)(.+)$/m, + (_match, prefix: string, value: string) => + `${prefix}"${value.slice(0, -3)}\\\n${value.slice(-3)}" # valid Git comment`, + ); + const suffix = + process.platform === "win32" + ? Buffer.alloc(0) + : Buffer.from([0x23, 0x20, 0xff, 0x0a]); + await writeFile( + config, + Buffer.concat([ + Buffer.from(`${configured}\n[feature]\n\tenabled\n`), + suffix, + ]), + ); + + expect(await inventory(checkout)).toContain("./nested/visible.ts"); + }); + + test.skipIf(process.platform !== "linux")( + "preserves non-UTF-8 paths in genuine Git worktree configurations", + async () => { + if (Bun.which("rg") === null || python === null) return; + + const checkout = await repository(); + const configure = [ + "import os, subprocess, sys", + "root = os.fsencode(sys.argv[1])", + "worktree = root + b'/nested-\\xff'", + "gitdir = root + b'/.git/modules/nested-bytes'", + "os.makedirs(os.path.dirname(gitdir), exist_ok=True)", + "subprocess.run([b'git', b'init', b'-q', b'--separate-git-dir', gitdir, worktree], check=True)", + "subprocess.run([b'git', b'--git-dir=' + gitdir, b'config', b'core.worktree', worktree], check=True)", + "with open(worktree + b'/visible.ts', 'wb') as source: source.write(b'tracked\\n')", + "subprocess.run([b'git', b'-C', worktree, b'add', b'visible.ts'], check=True)", + ].join("\n"); + execFileSync(python, ["-B", "-c", configure, checkout], { + stdio: "pipe", + }); + + expect( + (await inventory(checkout)).some((path) => + path.endsWith("/visible.ts"), + ), + ).toBe(true); + }, + ); + + test("does not inspect a differently cased checkout outside an explicit scope", async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const selected = join(checkout, "NESTED"); + const unselected = join(checkout, "nested"); + await mkdir(selected); + try { + await mkdir(unselected); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "EEXIST") return; + throw error; + } + for (const nested of [selected, unselected]) { + execFileSync("git", ["init", "-q"], { cwd: nested }); + await writeFile(join(nested, "tracked.ts"), "tracked\n"); + execFileSync("git", ["add", "tracked.ts"], { cwd: nested }); + } + const trace = join(dirname(checkout), "git-trace.log"); + + expect( + await inventory(checkout, "NESTED", { + ...process.env, + GIT_TRACE: trace, + GIT_TRACE_SETUP: "1", + }), + ).toEqual(["NESTED/tracked.ts"]); + expect(await readFile(trace, "utf8")).not.toContain(unselected); + }); + + test.skipIf(process.platform !== "win32")( + "does not traverse external Windows directory junctions", + async () => { + if (Bun.which("rg") === null) return; + + for (const initializeGit of [false, true]) { + const checkout = await repository(initializeGit); + const external = join(dirname(checkout), "outside"); + await mkdir(join(external, ".ignore"), { recursive: true }); + execFileSync("git", ["init", "-q"], { cwd: external }); + if (initializeGit) { + await writeFile(join(checkout, "junction"), "tracked\n"); + execFileSync("git", ["add", "junction"], { cwd: checkout }); + await rm(join(checkout, "junction")); + } + await symlink(external, join(checkout, "junction"), "junction"); + await writeFile(join(checkout, "visible.ts"), "visible\n"); + + expect(await inventory(checkout)).toEqual(["./visible.ts"]); + } + }, + ); + + test.skipIf(process.platform === "win32")( + "inventories an explicit file without listing its parent directory", + async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const directory = join(checkout, "restricted"); + await mkdir(directory); + await writeFile(join(directory, "source.ts"), "export {};\n"); + await chmod(directory, 0o111); + try { + expect(await inventory(checkout, "restricted/source.ts")).toEqual([ + "restricted/source.ts", + ]); + } finally { + await chmod(directory, 0o755); + } + }, + ); + + test("preserves supported in-repository symbolic path targets", async () => { + const checkout = await repository(); + const source = join(checkout, "source"); + await mkdir(source); + await writeFile(join(source, "app.ts"), "export {};\n"); + await symlink( + source, + join(checkout, "linked"), + process.platform === "win32" ? "junction" : "dir", + ); + + await expect(normalizeTarget(checkout, ["linked/app.ts"])).resolves.toEqual( + { kind: "paths", paths: ["source/app.ts"] }, + ); + }); + + test("rejects ignore files that point outside the requested repository", async () => { + if (process.platform === "win32" || Bun.which("rg") === null) return; + + const checkout = await repository(false); + const external = join(dirname(checkout), "external.ignore"); + await writeFile(external, "hidden.ts\n"); + await writeFile(join(checkout, "hidden.ts"), "export {};\n"); + await symlink(external, join(checkout, ".ignore")); + + await expect(inventory(checkout)).rejects.toThrow( + "symbolic ignore files are not supported", + ); + }); + + test.skipIf(process.platform === "win32")( + "rejects descendant ignore links before invoking repository-wide ripgrep", + async () => { + if (Bun.which("rg") === null) return; + + const checkout = await repository(); + const visible = join(checkout, "visible"); + const external = join(dirname(checkout), "external.ignore"); + await mkdir(visible); + await writeFile(join(visible, "source.ts"), "visible\n"); + await writeFile(external, "source.ts\n"); + await symlink(external, join(visible, ".ignore")); + + await expect(inventory(checkout)).rejects.toThrow( + "symbolic ignore files are not supported", + ); + }, + ); + + test.skipIf(process.platform === "win32")( + "excludes case-equivalent Git metadata before running ripgrep", + async () => { + const ripgrep = Bun.which("rg"); + if (ripgrep === null) return; + + const checkout = await repository(); + const metadata = join(checkout, ".GIT"); + await rename(join(checkout, ".git"), metadata); + const equivalent = await realpath(join(checkout, ".git")).then( + async (resolved) => resolved === (await realpath(metadata)), + () => false, + ); + if (!equivalent) return; + + const external = join(dirname(checkout), "external.ignore"); + const trace = join(dirname(checkout), "ripgrep-output"); + const wrappers = join(dirname(checkout), "bin"); + await mkdir(wrappers); + await writeFile(external, "# external ignore rules\n"); + await symlink(external, join(metadata, ".ignore")); + await writeFile(join(checkout, ".rgignore"), "!.GIT/\n!.GIT/**\n"); + await writeFile(join(checkout, "visible.ts"), "visible\n"); + const wrapper = join(wrappers, "rg"); + await writeFile( + wrapper, + `#!/bin/sh\nif [ "$PWD" = ${JSON.stringify(checkout)} ]; then\n ${JSON.stringify(ripgrep)} "$@" > ${JSON.stringify(trace)}\n status=$?\n cat ${JSON.stringify(trace)}\n exit "$status"\nfi\nexec ${JSON.stringify(ripgrep)} "$@"\n`, + ); + await chmod(wrapper, 0o755); + + expect( + await inventory(checkout, ".", { + ...process.env, + PATH: `${wrappers}:${process.env["PATH"] ?? ""}`, + }), + ).toEqual(["./.rgignore", "./visible.ts"]); + expect((await readFile(trace)).toString()).not.toContain(".GIT/"); + }, + ); + + test.skipIf(process.platform === "win32")( + "rejects snapshot ignore links without discovering a parent checkout", + async () => { + if (Bun.which("rg") === null) return; + + const parent = await repository(); + const snapshot = join(parent, "snapshot"); + const visible = join(snapshot, "visible"); + const external = join(dirname(parent), "external.ignore"); + const trace = join(dirname(parent), "git-trace.log"); + await mkdir(visible, { recursive: true }); + await writeFile(external, "# ignore rules\n"); + await writeFile(join(visible, "source.ts"), "visible\n"); + await symlink(external, join(visible, ".ignore")); + + await expect( + inventory(snapshot, ".", { + ...process.env, + GIT_DIR: join(parent, ".git"), + GIT_TRACE: trace, + }), + ).rejects.toThrow("symbolic ignore files are not supported"); + await expect(readFile(trace, "utf8")).rejects.toThrow(); + }, + ); +});