Skip to main content

grit_lib/
repo.rs

1//! Repository discovery and the primary `Repository` handle.
2//!
3//! # Discovery
4//!
5//! [`Repository::discover`] walks up from a starting directory to find the
6//! nearest `.git` directory (or bare repository), honouring `GIT_DIR` and
7//! `GIT_WORK_TREE` environment variables and the `.git` gitfile indirection.
8//!
9//! # Structure
10//!
11//! A [`Repository`] owns:
12//!
13//! - `git_dir` — absolute path to the `.git` directory (or the repo root for
14//!   bare repos).
15//! - `work_tree` — `Some(path)` for non-bare repos, `None` for bare.
16//! - [`Odb`] — the loose object database.
17//!
18//! Library guide: [repo](https://grit-scm.com/docs/library/repository)
19//!
20//! ```rust
21//! use grit_lib::repo::{InitOptions, Repository};
22//! # fn main() -> grit_lib::error::Result<()> {
23//! let dir = tempfile::tempdir()?;
24//! let _repo = Repository::init(dir.path(), &InitOptions::default())?;
25//! # Ok(())
26//! # }
27//! ```
28
29use crate::git_path::CanonicalizeExt;
30use std::collections::BTreeSet;
31use std::fs;
32use std::fs::OpenOptions;
33use std::io::Write;
34use std::path::{Component, Path, PathBuf};
35use std::sync::{Arc, Mutex};
36
37use crate::change_id::ChangeIdGenerator;
38use crate::check_ref_format::validate_branch_short_name;
39use crate::command_runner::CommandRunner;
40use crate::config::{
41    config_file_stamps, stamp_for_path, ConfigCacheKey, ConfigFile, ConfigScope, ConfigSet,
42    LoadConfigOptions,
43};
44use crate::diagnostics::{self, DiagnosticsHandle};
45use crate::environment::{Environment, RepositoryOptions};
46use crate::error::{Error, NotARepositoryReason, Result};
47use crate::git_dir::io_error::{map_git_dir_io, map_index_write_error, GitDirPathOperation};
48use crate::git_path::path_for_error_message;
49use crate::hooks::run_hook;
50use crate::index::Index;
51use crate::init_filesystem::{apply_init_filesystem_config, InitFilesystemConfigOptions};
52use crate::objects::{HashAlgo, ObjectId};
53use crate::odb::Odb;
54use crate::repo_caches::RepoCaches;
55use crate::rev_parse::is_inside_work_tree;
56use crate::sparse_checkout::effective_cone_mode_for_sparse_file;
57use crate::split_index::{write_index_file_split, WriteSplitIndexRequest};
58use crate::worktree::io_error::{map_worktree_io, WorktreePathOperation};
59use crate::worktree_cwd::cwd_relative_under_work_tree;
60
61/// Compute Git `GIT_PREFIX` for `env.cwd` relative to the work tree (POSIX, no trailing `/`).
62fn compute_git_prefix(env: &Environment, work_tree: &Path) -> Option<String> {
63    if let Some(prefix) = env.git_prefix.as_deref() {
64        let p = prefix.trim();
65        if !p.is_empty() {
66            return Some(p.to_owned());
67        }
68    }
69    let rel = cwd_relative_under_work_tree(work_tree, &env.cwd)?;
70    if rel.is_empty() {
71        None
72    } else {
73        Some(rel)
74    }
75}
76
77fn read_sparse_checkout_patterns(git_dir: &Path) -> Vec<String> {
78    let path = git_dir.join("info").join("sparse-checkout");
79    let Ok(content) = fs::read_to_string(&path) else {
80        return Vec::new();
81    };
82    content
83        .lines()
84        .map(|l| l.trim())
85        .filter(|l| !l.is_empty() && !l.starts_with('#'))
86        .map(String::from)
87        .collect()
88}
89
90/// Provenance flags recorded when a repository is discovered or opened explicitly.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct DiscoveryInfo {
93    /// Opened via `GIT_DIR` or an explicit git directory (not upward discovery).
94    pub explicit_git_dir: bool,
95    /// Directory where `.git` / a gitfile was found during upward discovery.
96    pub discovery_root: Option<PathBuf>,
97    /// `GIT_WORK_TREE` was applied after discovery without `GIT_DIR`.
98    pub work_tree_from_env: bool,
99    /// Discovery encountered a `.git` gitfile rather than a directory.
100    pub discovery_via_gitfile: bool,
101}
102
103/// Result of [`Repository::init_with_outcome`].
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct InitOutcome {
106    /// `true` when an existing repository was reinitialized in place.
107    pub reinitialized: bool,
108    /// Branch name recorded in `HEAD` after init (existing branch on reinit).
109    pub initial_branch: String,
110}
111
112/// Options for [`Repository::init`].
113#[derive(Debug, Clone)]
114#[non_exhaustive]
115pub struct InitOptions {
116    /// Create a bare repository at `path` instead of `path/.git`.
117    pub bare: bool,
118    /// Initial branch name written into `HEAD` when creating a new repository.
119    ///
120    /// When `None`, [`resolve_initial_branch`] reads `init.defaultBranch` from the
121    /// configuration cascade (without the new repository's local config) and falls
122    /// back to `main`. Ignored on reinit (existing `HEAD` is kept).
123    pub initial_branch: Option<String>,
124    /// Optional template directory (empty path skips hook installation).
125    pub template_dir: Option<PathBuf>,
126    /// Ref storage backend for the new repository.
127    pub ref_storage: crate::ref_storage::RefStorageFormat,
128    /// When set, place the git directory here and link from `path` via a gitfile.
129    pub separate_git_dir: Option<PathBuf>,
130    /// Bare git directory with `core.worktree` pointing at `path` (`GIT_WORK_TREE` clone layout).
131    pub bare_with_worktree_at_path: bool,
132    /// Object hash algorithm for a new repository (`extensions.objectformat`).
133    ///
134    /// When `None`, new repositories default to SHA-1; reinit preserves the existing format.
135    pub object_format: Option<HashAlgo>,
136    /// Environment used to resolve `init.defaultBranch` and other init-time config, and stored
137    /// on the returned [`Repository`] when set (identity, `core.autocrlf`, `safe.directory`, and
138    /// related cascade). When `None`, init uses [`Environment::empty`] and the handle stays
139    /// hermetic like [`RepositoryOptions::empty`].
140    pub environment: Option<Environment>,
141}
142
143impl Default for InitOptions {
144    fn default() -> Self {
145        Self {
146            bare: false,
147            initial_branch: None,
148            template_dir: None,
149            ref_storage: crate::ref_storage::RefStorageFormat::Files,
150            separate_git_dir: None,
151            bare_with_worktree_at_path: false,
152            object_format: None,
153            environment: None,
154        }
155    }
156}
157
158impl InitOptions {
159    /// Same as [`Default::default`].
160    #[must_use]
161    pub fn new() -> Self {
162        Self::default()
163    }
164
165    /// Set [`Self::bare`].
166    #[must_use]
167    pub fn with_bare(mut self, bare: bool) -> Self {
168        self.bare = bare;
169        self
170    }
171
172    /// Set [`Self::initial_branch`].
173    #[must_use]
174    pub fn with_initial_branch(mut self, initial_branch: impl Into<String>) -> Self {
175        self.initial_branch = Some(initial_branch.into());
176        self
177    }
178
179    /// Set [`Self::object_format`].
180    #[must_use]
181    pub fn with_object_format(mut self, object_format: HashAlgo) -> Self {
182        self.object_format = Some(object_format);
183        self
184    }
185
186    /// Set [`Self::environment`].
187    #[must_use]
188    pub fn with_environment(mut self, environment: Environment) -> Self {
189        self.environment = Some(environment);
190        self
191    }
192
193    /// Set [`Self::ref_storage`].
194    #[must_use]
195    pub fn with_ref_storage(mut self, ref_storage: crate::ref_storage::RefStorageFormat) -> Self {
196        self.ref_storage = ref_storage;
197        self
198    }
199
200    /// Set [`Self::template_dir`].
201    #[must_use]
202    pub fn with_template_dir(mut self, template_dir: Option<PathBuf>) -> Self {
203        self.template_dir = template_dir;
204        self
205    }
206
207    /// Set [`Self::separate_git_dir`].
208    #[must_use]
209    pub fn with_separate_git_dir(mut self, separate_git_dir: Option<PathBuf>) -> Self {
210        self.separate_git_dir = separate_git_dir;
211        self
212    }
213
214    /// Set [`Self::bare_with_worktree_at_path`].
215    #[must_use]
216    pub fn with_bare_with_worktree_at_path(mut self, bare_with_worktree_at_path: bool) -> Self {
217        self.bare_with_worktree_at_path = bare_with_worktree_at_path;
218        self
219    }
220}
221
222/// Optional custom ODB (and ref store) wiring for [`RepositoryBuilder::open`].
223///
224/// # On-disk layout
225///
226/// What `open` requires in the git directory depends on what is injected:
227///
228/// - Nothing injected: a normal git directory (`HEAD`, `objects/`).
229/// - A [`Self::ref_store`]: no `HEAD` file (the store holds `HEAD`), and `refs/` is never read.
230/// - A custom [`OdbBuilder::primary`](crate::odb::OdbBuilder::primary): no `objects/`.
231/// - Both: nothing at all. The git directory may be empty or missing; it is still where the
232///   repository config (`config`) is read from and where the index lives. Staging creates
233///   the directory when it is missing.
234///
235/// Hooks run with `GIT_DIR` exported only when the [`Environment`] in the options sets
236/// [`Environment::git_dir`]; `open` does not set it from its path. Objects and refs held in
237/// injected stores are not on disk, so a hook that runs the system `git` against such a
238/// repository finds no repository.
239///
240/// ```
241/// use std::sync::Arc;
242///
243/// use grit_lib::config::environment::RepositoryOptions;
244/// use grit_lib::objects::odb::store::MemoryStore;
245/// use grit_lib::objects::odb::OdbBuilder;
246/// use grit_lib::objects::HashAlgo;
247/// use grit_lib::refs::store::{MemoryRefStore, RawRef};
248/// use grit_lib::repo::RepositoryBuilder;
249///
250/// # fn main() -> grit_lib::error::Result<()> {
251/// let dir = tempfile::tempdir()?;
252/// let git_dir = dir.path().join("in-memory.git");
253/// let repo = RepositoryBuilder::new(RepositoryOptions::empty())
254///     .odb_builder(
255///         OdbBuilder::files(git_dir.join("objects"))
256///             .primary(Arc::new(MemoryStore::new(HashAlgo::Sha1)))
257///             .alternates(false),
258///     )
259///     .ref_store(Arc::new(MemoryRefStore::new()))
260///     .initial_branch("main")
261///     .open(&git_dir, None)?;
262/// assert_eq!(
263///     repo.refs().read_raw("HEAD")?,
264///     Some(RawRef::Symbolic("refs/heads/main".to_owned()))
265/// );
266/// assert!(!git_dir.exists());
267/// # Ok(())
268/// # }
269/// ```
270#[must_use]
271pub struct RepositoryBuilder {
272    options: RepositoryOptions,
273    odb_builder: Option<crate::odb::OdbBuilder>,
274    ref_store: Option<Arc<dyn crate::refs::store::RefStore>>,
275    initial_branch: Option<String>,
276}
277
278impl RepositoryBuilder {
279    /// Start building a repository open with `options`.
280    pub fn new(options: RepositoryOptions) -> Self {
281        Self {
282            options,
283            odb_builder: None,
284            ref_store: None,
285            initial_branch: None,
286        }
287    }
288
289    /// Use a preconfigured [`crate::odb::OdbBuilder`] for the object store.
290    ///
291    /// The builder's objects directory may be any path naming the repository's `objects/`
292    /// directory, or [`crate::environment::Environment::git_object_directory`] when that is
293    /// set (through symlinks, or relative to the environment's current directory);
294    /// [`Self::open`] replaces it with the canonical path of that directory, which is what
295    /// [`crate::odb::Odb::objects_dir`] then reports (the symlink target when `objects/`
296    /// itself is a symlink).
297    pub fn odb_builder(mut self, builder: crate::odb::OdbBuilder) -> Self {
298        self.odb_builder = Some(builder);
299        self
300    }
301
302    /// Use `store` as the repository's ref backend (for example
303    /// [`crate::refs::store::MemoryRefStore`]) instead of the one the git directory selects.
304    ///
305    /// The default backend is never opened, so the git directory needs no `HEAD` or `refs/`.
306    ///
307    /// When [`Self::open`] is given a linked worktree's admin directory
308    /// (`<git dir>/worktrees/<id>`), `store` holds only the main worktree's `HEAD`, so the
309    /// handle reads and writes the linked worktree's own `HEAD` and `HEAD` reflog in that
310    /// admin directory and every other ref in `store`. Files and reftable stores keep
311    /// per-worktree refs apart themselves and are used as given.
312    pub fn ref_store(mut self, store: Arc<dyn crate::refs::store::RefStore>) -> Self {
313        self.ref_store = Some(store);
314        self
315    }
316
317    /// When the ref store has no `HEAD` after opening, point it at `refs/heads/<branch>`
318    /// (an unborn branch), as [`Repository::init`] does for a new git directory.
319    ///
320    /// An existing `HEAD` is left alone. Mostly useful with a fresh [`Self::ref_store`].
321    pub fn initial_branch(mut self, branch: impl Into<String>) -> Self {
322        self.initial_branch = Some(branch.into());
323        self
324    }
325
326    /// Open `git_dir` with optional `work_tree`, applying builder overrides.
327    ///
328    /// With an [`Self::odb_builder`], relative `git_dir`, `work_tree` and objects paths are
329    /// resolved against the environment's current directory
330    /// ([`crate::environment::Environment::discovery_cwd`]), and paths through symlinks are
331    /// accepted.
332    ///
333    /// # Errors
334    ///
335    /// Same as [`Repository::open`], plus:
336    /// - [`Error::OdbObjectsDirMismatch`] when a files-backed [`Self::odb_builder`] names a
337    ///   directory other than the repository's `objects/` (or the environment's
338    ///   `GIT_OBJECT_DIRECTORY`).
339    /// - [`Error::ObjectFormatMismatch`] when the builder's custom primary, a read source or
340    ///   its attached [`commit_graph`](crate::odb::OdbBuilder::commit_graph) reports a
341    ///   [`hash_algo`](crate::odb::store::ObjectStore::hash_algo) other than the
342    ///   repository's `extensions.objectformat` (SHA-1 when unset or when the git directory
343    ///   has no config).
344    /// - [`Error::InvalidRef`] when [`Self::initial_branch`] is not a valid branch name.
345    /// - [`Error::RefStore`] when seeding `HEAD` in the ref store fails.
346    pub fn open(self, git_dir: &Path, work_tree: Option<&Path>) -> Result<Repository> {
347        let mut options = self.options;
348        if let Some(wt) = work_tree {
349            options.work_tree = Some(wt.to_path_buf());
350        }
351        let repo = if let Some(builder) = self.odb_builder {
352            Repository::open_with_odb_internal(&options, git_dir, builder, self.ref_store)?
353        } else {
354            Repository::open_internal_with_refs(&options, git_dir, self.ref_store)?
355        };
356        if let Some(branch) = self.initial_branch {
357            seed_unborn_head(repo.refs(), &branch)?;
358        }
359        Ok(repo)
360    }
361}
362
363/// Point `HEAD` at `refs/heads/<branch>` in `store` unless `HEAD` already exists.
364fn seed_unborn_head(store: &dyn crate::refs::store::RefStore, branch: &str) -> Result<()> {
365    use crate::refs::store::{Expected, RawRef, RefStoreError, RefTransaction, RefUpdate};
366
367    validate_branch_short_name(branch).map_err(|e| Error::InvalidRef(e.to_string()))?;
368    if store.read_raw("HEAD")?.is_some() {
369        return Ok(());
370    }
371    let txn = RefTransaction::new().update(RefUpdate {
372        name: "HEAD".to_owned(),
373        new_value: Some(RawRef::Symbolic(format!("refs/heads/{branch}"))),
374        expected: Expected::Missing,
375        reflog: None,
376        flags: crate::refs::store::RefUpdateFlags {
377            no_deref: true,
378            ..Default::default()
379        },
380    })?;
381    match store.prepare(txn).and_then(|prepared| prepared.commit()) {
382        Ok(()) | Err(RefStoreError::ExpectedMismatch { .. }) => Ok(()),
383        Err(err) => Err(err.into()),
384    }
385}
386
387/// A handle to an open Git repository.
388///
389/// ## Config and path caching
390///
391/// On open, Grit resolves the common git directory ([`Self::common_git_dir`]), loads the merged
392/// configuration cascade once, and keeps that snapshot for the lifetime of this handle. Hot paths
393/// (object reads, replace-ref checks, and similar) reuse the snapshot without re-statting config
394/// files on every access.
395///
396/// Refresh the snapshot when on-disk config may have changed:
397///
398/// - [`Self::reload_config`] after writing config through [`crate::config::ConfigHandle`] or other
399///   in-process library APIs on this handle.
400/// - Hook and filter subprocesses may rewrite config; [`crate::hooks::run_hook_opts`] reloads the
401///   snapshot after hooks finish when a [`Repository`] is provided.
402/// - External edits (another process, or the system `git` binary) are picked up on the next
403///   [`Self::config`] call, which revalidates file stamps once per call—not on every internal read.
404/// - Open a new [`Repository`] to observe ref or config changes made elsewhere without calling
405///   [`Self::reload_config`] (for example a `refs/replace/*` ref created by system `git`).
406///
407/// # Examples
408///
409/// ```
410/// use grit_lib::prelude::RepositoryOptions;
411/// use grit_lib::repo::{InitOptions, Repository};
412///
413/// # fn main() -> grit_lib::error::Result<()> {
414/// let dir = tempfile::tempdir()?;
415/// let repo = Repository::init(dir.path(), &InitOptions::default())?;
416/// let reopened = Repository::open(
417///     repo.git_dir(),
418///     &RepositoryOptions::empty().with_work_tree(dir.path()),
419/// )?;
420/// assert_eq!(reopened.git_dir(), repo.git_dir());
421/// # Ok(())
422/// # }
423/// ```
424pub struct Repository {
425    git_dir: PathBuf,
426    /// Common git directory (`commondir` target when linked; otherwise [`Self::git_dir`]).
427    common_git_dir: PathBuf,
428    work_tree: Option<PathBuf>,
429    odb: Odb,
430    explicit_git_dir: bool,
431    discovery_root: Option<PathBuf>,
432    work_tree_from_env: bool,
433    discovery_via_gitfile: bool,
434    /// Session config snapshot and derived hot-path flags (see struct-level rustdoc).
435    config_session: Mutex<Option<ConfigSession>>,
436    /// Repository-scoped caches (config, attributes, filters, precompose, …).
437    caches: Arc<RepoCaches>,
438    /// Pluggable ref backend selected for this repository, checked against [`Self::odb`].
439    ref_store: crate::refs::store::ObjectCheckedRefStore,
440    /// Discovery and configuration environment used to open this repository.
441    environment: Arc<Environment>,
442    /// Repository-relative path of [`Environment::cwd`] under [`Self::work_tree`] (Git `GIT_PREFIX`).
443    git_prefix: Option<String>,
444    /// Injectable subprocess runner (hooks, filters, helpers).
445    command_runner: Arc<dyn CommandRunner>,
446    /// Injectable random bytes for new change ids.
447    change_id_generator: Arc<dyn ChangeIdGenerator>,
448    diagnostics: DiagnosticsHandle,
449    network_trace: bool,
450    /// Embedder override for rev-parse relative dates (`@{yesterday}`, etc.).
451    reference_unix_time: Option<i64>,
452    test_assume_different_owner: bool,
453    thread_policy: crate::parallel::ThreadPolicy,
454    hook_stdio: crate::hooks::HookStdio,
455}
456
457const _: () = {
458    fn assert_send_sync<T: Send + Sync>() {}
459    let _ = assert_send_sync::<Repository>;
460};
461
462impl std::fmt::Debug for Repository {
463    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464        f.debug_struct("Repository")
465            .field("git_dir", &self.git_dir())
466            .field("work_tree", &self.work_tree())
467            .field("explicit_git_dir", &self.discovery_info().explicit_git_dir)
468            .field("network_trace", &self.network_trace)
469            .finish_non_exhaustive()
470    }
471}
472
473/// Legacy alias kept for [`Odb`] wiring during the repository-cache migration.
474pub(crate) type RepositoryConfigSnapshot = Arc<RepoCaches>;
475
476/// Repository-level settings derived from config that are read on hot paths.
477#[derive(Debug, Clone)]
478struct RepoCachedSettings {
479    /// `core.useReplaceRefs` (default `true`).
480    use_replace_refs: bool,
481    /// Effective `refs/replace/` base path (always slash-terminated).
482    replace_ref_base: String,
483}
484
485/// Stamp metadata for one loaded config cascade (session invalidation).
486#[derive(Clone)]
487struct ConfigSessionValidation {
488    key: ConfigCacheKey,
489    env_fingerprint: Vec<(String, Option<String>)>,
490    base_stamps: Vec<(PathBuf, Option<(std::time::SystemTime, u64)>)>,
491    extra_stamps: Vec<(PathBuf, Option<(std::time::SystemTime, u64)>)>,
492    /// The environment snapshot the stamps were last validated against. While the handle
493    /// still uses this very snapshot (and git dir), the fingerprint, cache key and cascade
494    /// paths cannot have changed, so validation only re-stats the stamped files.
495    environment: Arc<Environment>,
496}
497
498/// In-memory config session for one open [`Repository`] handle.
499struct ConfigSession {
500    config: Arc<ConfigSet>,
501    validation: ConfigSessionValidation,
502    cached_settings: RepoCachedSettings,
503    /// When `true`, [`Repository::resolve_replace_redirect`] skips replace-ref lookups.
504    replace_lookup_inactive: bool,
505}
506
507impl ConfigSessionValidation {
508    /// Whether the session's cascade still matches `env`, `git_dir` and the files on disk.
509    ///
510    /// `opts` builds the load options of the handle (always the same for one environment
511    /// and git dir); it is only called when `env` is not the snapshot last validated. A
512    /// full check that passes adopts `env` as that snapshot.
513    fn still_valid(
514        &mut self,
515        env: &Arc<Environment>,
516        git_dir: &Path,
517        opts: impl FnOnce() -> LoadConfigOptions,
518    ) -> bool {
519        if Arc::ptr_eq(env, &self.environment) && self.key.git_dir.as_deref() == Some(git_dir) {
520            return stamps_unchanged(&self.base_stamps) && stamps_unchanged(&self.extra_stamps);
521        }
522        let opts = &opts();
523        let Some(env_fp) = env.config_fingerprint() else {
524            return false;
525        };
526        if self.env_fingerprint.as_slice() != env_fp.as_slice() {
527            return false;
528        }
529        let key = ConfigCacheKey::new(Some(git_dir), opts);
530        if key != self.key {
531            return false;
532        }
533        let base_stamps = config_file_stamps(env, Some(git_dir), opts);
534        if self.base_stamps.as_slice() != base_stamps.as_slice()
535            || !stamps_unchanged(&self.extra_stamps)
536        {
537            return false;
538        }
539        self.environment = Arc::clone(env);
540        true
541    }
542}
543
544fn stamps_unchanged(stamps: &[(PathBuf, Option<(std::time::SystemTime, u64)>)]) -> bool {
545    stamps
546        .iter()
547        .all(|(path, stamp)| stamp_for_path(path) == *stamp)
548}
549
550#[allow(clippy::too_many_arguments)]
551fn checked_ref_store_with_hooks(
552    inner: Arc<dyn crate::refs::store::RefStore>,
553    odb: crate::odb::Odb,
554    git_dir: &Path,
555    work_tree: Option<&Path>,
556    environment: &Arc<Environment>,
557    command_runner: &Arc<dyn CommandRunner>,
558    hook_stdio: crate::hooks::HookStdio,
559    diagnostics: &crate::diagnostics::DiagnosticsHandle,
560) -> crate::refs::store::ObjectCheckedRefStore {
561    let hooks = Arc::new(crate::hooks::RefHookContext::new(
562        git_dir.to_path_buf(),
563        work_tree.map(Path::to_path_buf),
564        Arc::clone(environment),
565        Arc::clone(command_runner),
566        hook_stdio,
567        Arc::clone(diagnostics),
568        odb.hash_algo(),
569    ));
570    crate::refs::store::ObjectCheckedRefStore::new(inner, odb).with_ref_hooks(hooks)
571}
572
573impl Repository {
574    fn config_load_options(&self, include_system: bool) -> LoadConfigOptions {
575        LoadConfigOptions {
576            include_system,
577            diagnostics: Some(self.diagnostics()),
578            ..LoadConfigOptions::for_repository(
579                &self.environment,
580                Some(&self.git_dir),
581                Some(Arc::clone(&self.caches)),
582            )
583        }
584    }
585
586    fn from_canonical_git_dir(
587        options: &RepositoryOptions,
588        git_dir: PathBuf,
589        work_tree: Option<&Path>,
590        injected_refs: Option<Arc<dyn crate::refs::store::RefStore>>,
591    ) -> Result<Self> {
592        let environment = Arc::new(options.environment.clone());
593        let command_runner = Arc::clone(&options.command_runner);
594        let change_id_generator = Arc::clone(&options.change_id_generator);
595        // An injected ref store holds HEAD itself; otherwise HEAD must exist or be a symlink
596        // (linked worktrees have a symlink HEAD).
597        if injected_refs.is_none() && !head_file_present(&git_dir) {
598            return Err(not_a_repository_at(&git_dir));
599        }
600
601        let common_git_dir = resolve_common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
602        let objects_dir = resolve_objects_dir(&environment, &git_dir, &common_git_dir)
603            .filter(|dir| dir.exists())
604            .ok_or_else(|| not_a_repository_at(&git_dir))?;
605
606        let work_tree = match work_tree {
607            Some(p) => {
608                let cwd = environment.discovery_cwd();
609                let mut resolved = if p.is_absolute() {
610                    p.to_path_buf()
611                } else {
612                    cwd.join(p)
613                };
614                if resolved.exists() {
615                    resolved = resolved
616                        .canonicalize_simplified()
617                        .map_err(|_| Error::PathError(p.display().to_string()))?;
618                }
619                Some(resolved)
620            }
621            None => None,
622        };
623
624        let caches = RepoCaches::with_diagnostics(
625            Arc::clone(&command_runner),
626            Arc::clone(&options.diagnostics),
627        );
628        caches.set_promisor_fetcher(options.promisor_fetcher.clone());
629        let odb = if let Some(ref wt) = work_tree {
630            Odb::with_work_tree(&objects_dir, wt)
631                .with_config_git_dir(git_dir.clone())
632                .with_shared_config_state(caches.clone(), environment.clone())
633        } else {
634            Odb::new(&objects_dir)
635                .with_config_git_dir(git_dir.clone())
636                .with_shared_config_state(caches.clone(), environment.clone())
637        };
638
639        let git_prefix = work_tree
640            .as_ref()
641            .and_then(|wt| compute_git_prefix(environment.as_ref(), wt));
642
643        let ref_store = checked_ref_store_with_hooks(
644            open_or_install_ref_store(&caches, &git_dir, &environment, injected_refs)?,
645            odb.clone(),
646            &git_dir,
647            work_tree.as_deref(),
648            &environment,
649            &command_runner,
650            options.hook_stdio,
651            &options.diagnostics,
652        );
653        caches.configure_filter_subprocess(&environment, &git_dir, work_tree.as_deref());
654
655        Ok(Self {
656            git_dir,
657            common_git_dir,
658            work_tree,
659            odb,
660            explicit_git_dir: false,
661            discovery_root: None,
662            work_tree_from_env: false,
663            discovery_via_gitfile: false,
664            config_session: Mutex::new(None),
665            caches,
666            ref_store,
667            environment,
668            git_prefix,
669            command_runner,
670            change_id_generator,
671            diagnostics: Arc::clone(&options.diagnostics),
672            network_trace: options.network_trace,
673            reference_unix_time: options.reference_unix_time,
674            test_assume_different_owner: options.test_assume_different_owner,
675            thread_policy: options.thread_policy,
676            hook_stdio: options.hook_stdio,
677        })
678    }
679
680    /// Explicit parallel worker overrides for this handle (`--threads` / embedder policy).
681    #[must_use]
682    pub fn thread_policy(&self) -> crate::parallel::ThreadPolicy {
683        self.thread_policy
684    }
685
686    /// Return a copy of this repository handle with an updated parallel thread policy.
687    #[must_use]
688    pub fn with_thread_policy(mut self, policy: crate::parallel::ThreadPolicy) -> Self {
689        self.thread_policy = policy;
690        self
691    }
692
693    /// Return this handle's shared cache arena.
694    #[must_use]
695    pub(crate) fn caches(&self) -> &Arc<RepoCaches> {
696        &self.caches
697    }
698
699    /// Return the promisor hydration hook for this repository, if installed.
700    #[must_use]
701    pub(crate) fn promisor_hydrate_hook(&self) -> Option<crate::blame::PromisorHydrateHook> {
702        self.caches.promisor_hydrate_hook()
703    }
704
705    /// Return the [`Environment`] used to discover or open this repository.
706    #[must_use]
707    pub fn environment(&self) -> &Environment {
708        &self.environment
709    }
710
711    /// Unix timestamp for rev-parse relative date selectors (`@{yesterday}`, etc.).
712    #[must_use]
713    pub fn reference_unix_time(&self) -> i64 {
714        self.wall_clock_epoch()
715    }
716
717    /// Wall clock used for commit dates, rerere GC, relative dates, and similar operations on this handle.
718    #[must_use]
719    pub fn wall_clock_epoch(&self) -> i64 {
720        if let Some(ts) = self.reference_unix_time {
721            return ts;
722        }
723        if let Some(ts) = self.environment.git_now_date_override() {
724            return ts;
725        }
726        crate::git_date::tm::process_wall_clock_sec()
727    }
728
729    /// [`Self::wall_clock_epoch`] in this handle's `TZ`, for stamping author, committer and
730    /// reflog identity lines (see
731    /// [`resolve_identity_with`](crate::config::ident_resolve::resolve_identity_with)).
732    #[must_use]
733    pub fn identity_now(&self) -> time::OffsetDateTime {
734        crate::environment::offset_from_epoch_and_tz(
735            self.wall_clock_epoch(),
736            self.environment.tz.as_deref(),
737        )
738    }
739
740    /// Resolve the author or committer identity Git would record for a new commit, tag, stash,
741    /// or notes commit made through this handle.
742    ///
743    /// Reads the handle's [`Environment`] (`GIT_AUTHOR_*` / `GIT_COMMITTER_*`, including the
744    /// `*_DATE` overrides) and configuration (`author.*`, `committer.*`, `user.*`); see
745    /// [`resolve_signature_with`](crate::config::ident_resolve::resolve_signature_with).
746    ///
747    /// # Parameters
748    ///
749    /// - `role`: author or committer.
750    /// - `now`: timestamp recorded when no date override is set; usually [`Self::identity_now`].
751    ///
752    /// # Errors
753    ///
754    /// [`Error::Identity`] when no usable name, email, or date is configured, and config load
755    /// failures.
756    ///
757    /// # Examples
758    ///
759    /// ```
760    /// use grit_lib::config::ident_resolve::IdentRole;
761    /// use grit_lib::repo::{InitOptions, Repository};
762    /// # fn main() -> grit_lib::error::Result<()> {
763    /// let dir = tempfile::tempdir()?;
764    /// let repo = Repository::init(dir.path(), &InitOptions::default())?;
765    /// match repo.identity(IdentRole::Author, repo.identity_now()) {
766    ///     Ok(author) => assert!(!author.email.is_empty()),
767    ///     Err(grit_lib::error::Error::Identity(_)) => {} // nothing configured
768    ///     Err(other) => return Err(other),
769    /// }
770    /// # Ok(())
771    /// # }
772    /// ```
773    pub fn identity(
774        &self,
775        role: crate::config::ident_resolve::IdentRole,
776        now: time::OffsetDateTime,
777    ) -> Result<crate::ident::Signature> {
778        let config = self.config()?;
779        Ok(crate::config::ident_resolve::resolve_signature_with(
780            self.environment(),
781            &config,
782            role,
783            now,
784        )?)
785    }
786
787    /// Subprocess runner used for hooks, filters, and helpers.
788    #[must_use]
789    pub fn command_runner(&self) -> Arc<dyn CommandRunner> {
790        Arc::clone(&self.command_runner)
791    }
792
793    /// Where hooks run through this handle write their output
794    /// ([`RepositoryOptions::with_hook_stdio`]).
795    #[must_use]
796    pub fn hook_stdio(&self) -> crate::hooks::HookStdio {
797        self.hook_stdio
798    }
799
800    /// Generator used for random change ids on new commits.
801    #[must_use]
802    pub fn change_id_generator(&self) -> Arc<dyn ChangeIdGenerator> {
803        Arc::clone(&self.change_id_generator)
804    }
805
806    /// [`RepositoryOptions`] for discovering a linked worktree of this repository.
807    ///
808    /// Inherits config discovery ([`Environment`], command runner, diagnostics, thread policy)
809    /// from the caller handle but clears worktree-specific discovery overrides so
810    /// [`Repository::discover`] resolves the linked tree's own `.git` gitfile and index.
811    #[must_use]
812    pub fn linked_worktree_discover_options(&self) -> RepositoryOptions {
813        let mut environment = (*self.environment).clone();
814        environment.git_dir = None;
815        environment.git_work_tree = None;
816        environment.git_index_file = None;
817        environment.git_prefix = None;
818        environment.git_object_directory = None;
819        RepositoryOptions {
820            environment,
821            command_runner: Arc::clone(&self.command_runner),
822            change_id_generator: Arc::clone(&self.change_id_generator),
823            diagnostics: Arc::clone(&self.diagnostics),
824            network_trace: self.network_trace,
825            test_assume_different_owner: self.test_assume_different_owner,
826            reference_unix_time: self.reference_unix_time,
827            thread_policy: self.thread_policy,
828            work_tree: None,
829            validate_repository_format: true,
830            open_for_serving: false,
831            promisor_fetcher: self.caches().promisor_fetcher(),
832            hook_stdio: self.hook_stdio,
833        }
834    }
835
836    /// [`RepositoryOptions`] for opening another repository on this machine as the far end of
837    /// a local transfer, such as the destination of a local-path or `file://` push.
838    ///
839    /// Keeps the caller's environment (so that repository's hook scripts see the same `PATH`
840    /// and `HOME`, and global config applies), command runner and diagnostics, but drops the
841    /// variables that describe only this repository: its location, index and object
842    /// directories, `GIT_CONFIG`, command-line config overrides, namespace and replace-ref
843    /// settings. Git clears the same set before starting the far end of its local transport.
844    #[must_use]
845    pub(crate) fn peer_repository_options(&self) -> RepositoryOptions {
846        let mut options = self.linked_worktree_discover_options();
847        let env = &mut options.environment;
848        env.git_alternate_object_directories = None;
849        env.git_namespace = None;
850        env.git_replace_ref_base = None;
851        env.git_no_replace_objects = false;
852        env.git_config = None;
853        env.git_config_parameters = None;
854        env.git_config_count = None;
855        env.git_config_pairs.clear();
856        options
857    }
858
859    /// Handle for the linked worktree administered at `admin_dir` with its files at
860    /// `work_tree`.
861    ///
862    /// `admin_dir` is the worktree's directory under `<common git dir>/worktrees/`
863    /// ([`crate::worktree::WorktreeEntry::admin_dir`]). When both stores live in the git
864    /// directory the linked worktree is discovered from `work_tree`, as any later open would.
865    /// Otherwise discovery could not find an injected store, so the handle shares this
866    /// handle's object store and refs, except that `HEAD` and the `HEAD` reflog are the
867    /// worktree's own, in `admin_dir/HEAD` and `admin_dir/logs/HEAD`: commits and checkouts
868    /// made through the handle move the branch that `HEAD` names, never the main worktree's.
869    ///
870    /// # Errors
871    ///
872    /// Discovery failures, [`Error::NotARepository`] when `admin_dir` is missing, and
873    /// [`Error::PathError`] when `work_tree` cannot be canonicalized.
874    pub fn open_linked_worktree(&self, admin_dir: &Path, work_tree: &Path) -> Result<Self> {
875        let options = self.linked_worktree_discover_options();
876        if self.refs_on_disk() && self.odb.uses_files_primary() {
877            return Self::discover(Some(work_tree), &options);
878        }
879        let git_dir = admin_dir
880            .canonicalize_simplified()
881            .map_err(|_| not_a_repository_at(admin_dir))?;
882        let work_tree = work_tree
883            .canonicalize_simplified()
884            .map_err(|_| Error::PathError(work_tree.display().to_string()))?;
885        let environment = Arc::new(options.environment);
886        let caches = RepoCaches::with_diagnostics(
887            Arc::clone(&self.command_runner),
888            Arc::clone(&self.diagnostics),
889        );
890        let ref_store = if self.refs_on_disk() {
891            caches.open_ref_store(&git_dir, &environment)?
892        } else {
893            let view: Arc<dyn crate::refs::store::RefStore> =
894                Arc::new(crate::refs::store::LinkedWorktreeRefStore::new(
895                    git_dir.clone(),
896                    Arc::clone(self.ref_store.inner()),
897                ));
898            caches.install_ref_store(&git_dir, Arc::clone(&view));
899            view
900        };
901        let ref_store = checked_ref_store_with_hooks(
902            ref_store,
903            self.odb.clone(),
904            &git_dir,
905            Some(work_tree.as_path()),
906            &environment,
907            &self.command_runner,
908            self.hook_stdio,
909            &self.diagnostics,
910        );
911        let repo = Self {
912            git_prefix: compute_git_prefix(&environment, &work_tree),
913            git_dir,
914            common_git_dir: self.common_git_dir.clone(),
915            work_tree: Some(work_tree),
916            odb: self.odb.clone(),
917            explicit_git_dir: false,
918            discovery_root: None,
919            work_tree_from_env: false,
920            discovery_via_gitfile: true,
921            config_session: Mutex::new(None),
922            caches,
923            ref_store,
924            environment,
925            command_runner: Arc::clone(&self.command_runner),
926            change_id_generator: Arc::clone(&self.change_id_generator),
927            diagnostics: Arc::clone(&self.diagnostics),
928            network_trace: self.network_trace,
929            reference_unix_time: self.reference_unix_time,
930            test_assume_different_owner: self.test_assume_different_owner,
931            thread_policy: self.thread_policy,
932            hook_stdio: self.hook_stdio,
933        };
934        repo.caches().configure_filter_subprocess(
935            &repo.environment,
936            repo.git_dir(),
937            repo.work_tree(),
938        );
939        repo.refresh_config_session()?;
940        Ok(repo)
941    }
942
943    fn repo_cached_settings_from_config(
944        cfg: &ConfigSet,
945        environment: &Environment,
946    ) -> RepoCachedSettings {
947        let use_replace_refs = cfg
948            .get_bool("core.useReplaceRefs")
949            .and_then(|r| r.ok())
950            .unwrap_or(true);
951        let replace_ref_base = environment
952            .git_replace_ref_base
953            .clone()
954            .filter(|s| !s.is_empty())
955            .unwrap_or_else(|| "refs/replace/".to_owned());
956        let replace_ref_base = if replace_ref_base.ends_with('/') {
957            replace_ref_base
958        } else {
959            format!("{replace_ref_base}/")
960        };
961        RepoCachedSettings {
962            use_replace_refs,
963            replace_ref_base,
964        }
965    }
966
967    fn ensure_config_arc(&self) -> Result<Arc<ConfigSet>> {
968        self.config_arc_with_validation(true)
969    }
970
971    /// Hot-path config access: session snapshot without stamp revalidation.
972    pub(crate) fn ensure_config_arc_fast(&self) -> Result<Arc<ConfigSet>> {
973        self.config_arc_with_validation(false)
974    }
975
976    fn config_arc_with_validation(&self, validate_stamps: bool) -> Result<Arc<ConfigSet>> {
977        if validate_stamps {
978            if let Ok(mut guard) = self.config_session.lock() {
979                if let Some(session) = guard.as_mut() {
980                    if session
981                        .validation
982                        .still_valid(&self.environment, &self.git_dir, || {
983                            self.config_load_options(true)
984                        })
985                    {
986                        return Ok(Arc::clone(&session.config));
987                    }
988                }
989            }
990        } else if let Ok(guard) = self.config_session.lock() {
991            if let Some(session) = guard.as_ref() {
992                return Ok(Arc::clone(&session.config));
993            }
994        }
995        self.refresh_config_session()?;
996        self.config_session
997            .lock()
998            .map_err(|e| Error::Message(format!("config session lock poisoned: {e}")))?
999            .as_ref()
1000            .map(|s| Arc::clone(&s.config))
1001            .ok_or_else(|| Error::Message("config session missing after refresh".to_owned()))
1002    }
1003
1004    fn refresh_config_session(&self) -> Result<()> {
1005        crate::trace::trace_span!("grit.config.load");
1006        let opts = self.config_load_options(true);
1007        if let Ok(mut guard) = self.config_session.lock() {
1008            if let Some(session) = guard.as_mut() {
1009                if session
1010                    .validation
1011                    .still_valid(&self.environment, &self.git_dir, || opts.clone())
1012                {
1013                    return Ok(());
1014                }
1015                let env = self.environment.as_ref();
1016                let base_stamps = config_file_stamps(env, Some(&self.git_dir), &opts);
1017                if session.validation.base_stamps.as_slice() != base_stamps.as_slice() {
1018                    self.caches.invalidate_config_file_derived();
1019                }
1020            }
1021        }
1022        self.caches.clear_session_config();
1023        let env = self.environment.as_ref();
1024        let cfg = self
1025            .caches
1026            .load_config_with_options(env, Some(&self.git_dir), &opts)?;
1027        let key = ConfigCacheKey::new(Some(&self.git_dir), &opts);
1028        let (env_fingerprint, base_stamps, extra_stamps) =
1029            self.caches.config_entry_stamps(&key).unwrap_or_else(|| {
1030                (
1031                    env.config_fingerprint().unwrap_or_default(),
1032                    config_file_stamps(env, Some(&self.git_dir), &opts),
1033                    Vec::new(),
1034                )
1035            });
1036        let cached_settings =
1037            Self::repo_cached_settings_from_config(cfg.as_ref(), self.environment.as_ref());
1038        let replace_lookup_inactive = self.replace_lookup_inactive(cfg.as_ref());
1039        self.caches
1040            .set_session_config(key.clone(), Arc::clone(&cfg));
1041        let session = ConfigSession {
1042            config: Arc::clone(&cfg),
1043            validation: ConfigSessionValidation {
1044                key,
1045                env_fingerprint,
1046                base_stamps,
1047                extra_stamps,
1048                environment: Arc::clone(&self.environment),
1049            },
1050            cached_settings,
1051            replace_lookup_inactive,
1052        };
1053        *self
1054            .config_session
1055            .lock()
1056            .map_err(|e| Error::Message(format!("config session lock poisoned: {e}")))? =
1057            Some(session);
1058        Ok(())
1059    }
1060
1061    fn replace_lookup_inactive(&self, cfg: &ConfigSet) -> bool {
1062        if self.environment.git_no_replace_objects {
1063            return true;
1064        }
1065        if !cfg
1066            .get_bool("core.useReplaceRefs")
1067            .and_then(|r| r.ok())
1068            .unwrap_or(true)
1069        {
1070            return true;
1071        }
1072        let replace_dir = self.common_git_dir.join("refs/replace");
1073        !replace_dir_is_non_empty(&replace_dir)
1074    }
1075
1076    /// Return the diagnostic sink installed on this repository.
1077    #[must_use]
1078    pub fn diagnostics(&self) -> DiagnosticsHandle {
1079        Arc::clone(&self.diagnostics)
1080    }
1081
1082    /// Replace the diagnostic sink (for embedders and the CLI).
1083    pub fn set_diagnostics(&mut self, sink: DiagnosticsHandle) {
1084        self.caches.set_diagnostics(Arc::clone(&sink));
1085        self.diagnostics = sink;
1086    }
1087
1088    /// Whether network trace events should be delivered to the diagnostic sink.
1089    #[must_use]
1090    pub fn network_trace_enabled(&self) -> bool {
1091        self.network_trace
1092    }
1093
1094    /// Enable or disable network trace events on this handle.
1095    pub fn set_network_trace(&mut self, enabled: bool) {
1096        self.network_trace = enabled;
1097    }
1098
1099    pub(crate) fn warn(&self, warning: diagnostics::Warning) {
1100        self.diagnostics.warn(warning);
1101    }
1102
1103    /// Return the merged configuration cascade for this repository.
1104    ///
1105    /// Loads through [`RepoCaches`] on every call so global, system, include, and local
1106    /// config stamps stay validated (not only `.git/config` mtime). Prefer
1107    /// [`Self::reload_config`] after in-process config writes so related caches update
1108    /// immediately.
1109    ///
1110    /// # Errors
1111    ///
1112    /// Propagates errors from reading or parsing config files.
1113    pub fn config(&self) -> Result<Arc<ConfigSet>> {
1114        self.ensure_config_arc()
1115    }
1116
1117    /// Drop the in-memory config snapshot and load a fresh cascade from disk.
1118    ///
1119    /// Call after writing `.git/config` (or other cascade files) through library APIs on this
1120    /// repository handle so subsequent [`Self::config`] and hot-path readers observe the new data.
1121    ///
1122    /// # Errors
1123    ///
1124    /// Propagates errors from [`ConfigSet::load`].
1125    pub fn reload_config(&self) -> Result<()> {
1126        self.caches.invalidate_config_cache();
1127        if let Ok(mut guard) = self.config_session.lock() {
1128            *guard = None;
1129        }
1130        self.refresh_config_session()?;
1131        Ok(())
1132    }
1133
1134    /// Warm repository caches after discovery (reftable backend flag, ref store).
1135    pub(crate) fn install_config_snapshot(&self) {
1136        let _ = self
1137            .caches
1138            .open_ref_store(&self.git_dir, self.environment.as_ref());
1139    }
1140
1141    /// Common git directory for this handle (`commondir` when linked).
1142    ///
1143    /// The path is canonical. On Windows it uses the plain drive or UNC spelling (`C:\repo`)
1144    /// that callers pass in and compare against, not the `\\?\` form [`std::fs::canonicalize`]
1145    /// returns; the verbatim form is kept only when nothing else names the directory (longer
1146    /// than `MAX_PATH`, or a component Win32 would rewrite, such as a trailing dot).
1147    #[must_use]
1148    pub fn common_git_dir(&self) -> &Path {
1149        &self.common_git_dir
1150    }
1151
1152    fn validate_repository_format_cached(&self) -> Result<()> {
1153        let Some(config_path) = repository_config_path(&self.git_dir) else {
1154            return Ok(());
1155        };
1156        let parsed = self.caches.repository_format(&self.git_dir, &config_path)?;
1157        validate_repository_format_parsed(&parsed)
1158    }
1159
1160    /// Reference storage backend for this repository.
1161    ///
1162    /// Unlike a backend opened on its own, this store checks every transaction against the
1163    /// repository's objects in [`RefStore::prepare`](crate::refs::store::RefStore::prepare):
1164    /// a direct value must name a stored object
1165    /// ([`RefStoreError::MissingObject`](crate::refs::store::RefStoreError::MissingObject)),
1166    /// and a branch or `HEAD` must name a commit
1167    /// ([`RefStoreError::NotACommit`](crate::refs::store::RefStoreError::NotACommit)).
1168    #[must_use]
1169    pub fn refs(&self) -> &dyn crate::refs::store::RefStore {
1170        &self.ref_store
1171    }
1172
1173    /// Whether [`Self::refs`] is an on-disk files or reftable backend, so `HEAD` and refs can
1174    /// also be read from the git directory. False for injected stores such as
1175    /// [`crate::refs::store::MemoryRefStore`].
1176    #[must_use]
1177    pub(crate) fn refs_on_disk(&self) -> bool {
1178        use crate::refs::store::RefStorageFormat;
1179        matches!(
1180            self.refs().format(),
1181            RefStorageFormat::Files | RefStorageFormat::Reftable
1182        )
1183    }
1184
1185    /// Object read/write handle for this repository.
1186    #[inline]
1187    #[must_use]
1188    pub fn objects(&self) -> crate::objects::Objects<'_> {
1189        crate::objects::Objects::new(self)
1190    }
1191
1192    /// Reference resolve/list/update and reflog read handle.
1193    #[inline]
1194    #[must_use]
1195    pub fn references(&self) -> crate::refs::References<'_> {
1196        crate::refs::References::new(self)
1197    }
1198
1199    /// Index load/write handle.
1200    #[inline]
1201    #[must_use]
1202    pub fn index_handle(&self) -> crate::index::IndexHandle<'_> {
1203        crate::index::IndexHandle::new(self)
1204    }
1205
1206    /// Merged config read and local config write handle.
1207    #[inline]
1208    #[must_use]
1209    pub fn config_handle(&self) -> crate::config::ConfigHandle<'_> {
1210        crate::config::ConfigHandle::new(self)
1211    }
1212
1213    /// Revision parse and rev-list handle.
1214    #[inline]
1215    #[must_use]
1216    pub fn rev_walk(&self) -> crate::revwalk::RevWalk<'_> {
1217        crate::revwalk::RevWalk::new(self)
1218    }
1219
1220    /// Tree, index, and worktree diff handle.
1221    #[inline]
1222    #[must_use]
1223    pub fn diff(&self) -> crate::diff::Diff<'_> {
1224        crate::diff::Diff::new(self)
1225    }
1226
1227    /// Remote configuration, `ls-remote`, fetch, and push for this repository.
1228    #[must_use]
1229    pub fn remotes(&self) -> crate::repo_handles::RepositoryRemotes<'_> {
1230        crate::repo_handles::RepositoryRemotes::new(self)
1231    }
1232
1233    /// Linked worktree listing and admin for this repository.
1234    #[must_use]
1235    pub fn worktrees(&self) -> crate::repo_handles::RepositoryWorktrees<'_> {
1236        crate::repo_handles::RepositoryWorktrees::new(self)
1237    }
1238
1239    /// Stash stack operations for this repository.
1240    #[must_use]
1241    pub fn stash(&self) -> crate::repo_handles::RepositoryStash<'_> {
1242        crate::repo_handles::RepositoryStash::new(self)
1243    }
1244
1245    /// Capture and restore workspace state (index with conflict stages, worktree, refs, sentinels).
1246    #[must_use]
1247    pub fn workspace_snapshot(&self) -> crate::repo_handles::RepositoryWorkspaceSnapshot<'_> {
1248        crate::repo_handles::RepositoryWorkspaceSnapshot::new(self)
1249    }
1250
1251    /// Git notes read/write for this repository.
1252    #[must_use]
1253    pub fn notes(&self) -> crate::repo_handles::RepositoryNotes<'_> {
1254        crate::repo_handles::RepositoryNotes::new(self)
1255    }
1256
1257    /// Hook script discovery for this repository.
1258    #[must_use]
1259    pub fn hooks(&self) -> crate::repo_handles::RepositoryHooks<'_> {
1260        crate::repo_handles::RepositoryHooks::new(self)
1261    }
1262
1263    /// Check object store connectivity, refs, and optional pack indexes.
1264    ///
1265    /// Progress is reported through `progress`; the library never prints.
1266    ///
1267    /// # Errors
1268    ///
1269    /// Propagates object-store, ref, or index failures that abort the run. A custom primary
1270    /// object store is validated through the objects it lists (see [`crate::fsck::fsck`]).
1271    pub fn fsck(
1272        &self,
1273        options: &crate::fsck::FsckOptions,
1274        progress: &mut dyn crate::progress::ProgressSink,
1275        cancel: &dyn crate::progress::Cancel,
1276    ) -> crate::error::Result<crate::fsck::FsckReport> {
1277        crate::fsck::fsck(self, options, progress, cancel)
1278    }
1279
1280    /// SHA-256 content digest of a revision's tree ([tree-sha256] spec).
1281    ///
1282    /// [tree-sha256]: https://github.com/schacon/tree-sha256
1283    ///
1284    /// # Errors
1285    ///
1286    /// Returns [`crate::error::Error::TreeSha256MissingSubmodules`] when submodule
1287    /// checkouts or objects needed for hashing are absent.
1288    pub fn tree_sha256(
1289        &self,
1290        oid: crate::objects::ObjectId,
1291        options: &crate::tree_sha256::TreeSha256Options,
1292    ) -> crate::error::Result<crate::tree_sha256::TreeSha256Report> {
1293        crate::tree_sha256::tree_sha256(self, oid, options)
1294    }
1295
1296    /// Verify an annotated tag's signature and optional `tree-sha256` header.
1297    ///
1298    /// Reads the tag object by `oid` (not via a ref). Signature checking and digest
1299    /// recomputation run on separate threads when both are needed.
1300    ///
1301    /// # Errors
1302    ///
1303    /// See [`crate::tree_sha256::verify_tag`].
1304    pub fn verify_tag(
1305        &self,
1306        oid: crate::objects::ObjectId,
1307        options: &crate::tree_sha256::TagVerifyOptions,
1308    ) -> crate::error::Result<crate::tree_sha256::TagVerification> {
1309        crate::tree_sha256::verify_tag(self, oid, options)
1310    }
1311
1312    /// Patch-ID for a single commit (merge commits and empty diffs return `None`).
1313    ///
1314    /// Text diffs use the hunks of Git's default diff (Myers with change-compact
1315    /// and the indent heuristic, three context lines on each side), matching
1316    /// `git show --format= --no-ext-diff` piped to `git patch-id` in the
1317    /// selected mode, including where an ambiguous insertion or deletion is
1318    /// placed. Only context inside those hunks contributes to the ID. Stable
1319    /// and unstable modes drop spaces, tabs, CR and LF before hashing; form
1320    /// feed and vertical tab still count.
1321    ///
1322    /// # Errors
1323    ///
1324    /// Propagates object database read/parse failures.
1325    pub fn patch_id(
1326        &self,
1327        commit: &crate::objects::ObjectId,
1328        mode: crate::patch_ids::PatchIdMode,
1329    ) -> crate::error::Result<Option<crate::objects::ObjectId>> {
1330        crate::patch_ids::compute_patch_id(self.odb().hash_algo(), self.odb(), commit, mode)
1331    }
1332
1333    /// Patch-IDs for every commit named by `specs`, as `(patch_id, commit)` pairs.
1334    ///
1335    /// A spec that is a top-level range expands to the commits that range selects, in
1336    /// default `rev-list` order: `A..B` lists the commits reachable from `B` but not `A`,
1337    /// and `A...B` lists the commits on both sides of the symmetric difference in one
1338    /// walk-ordered sequence without saying which side each came from (use
1339    /// [`crate::revwalk::RevWalkListOptions::cherry_mark`] to compare the sides). Any
1340    /// other spec names one object, resolved like `git rev-parse` without index-path
1341    /// guessing. Merge commits and commits with no file changes are skipped, as in
1342    /// [`Self::patch_id`]. Results follow the order of `specs`; a commit named twice
1343    /// appears twice.
1344    ///
1345    /// # Errors
1346    ///
1347    /// Propagates rev-parse, merge-base, and rev-list errors for unresolvable specs, and
1348    /// object database read/parse failures while diffing.
1349    pub fn patch_ids_for_revs(
1350        &self,
1351        specs: &[&str],
1352        mode: crate::patch_ids::PatchIdMode,
1353    ) -> crate::error::Result<Vec<(crate::objects::ObjectId, crate::objects::ObjectId)>> {
1354        let walk = self.rev_walk();
1355        let walk_options = crate::revwalk::RevWalkListOptions::default();
1356        let mut out = Vec::new();
1357        for spec in specs {
1358            let commits = if crate::rev_parse::is_top_level_range(spec) {
1359                walk.commits(&[spec], &walk_options)?
1360            } else {
1361                vec![crate::rev_parse::resolve_revision_without_index_dwim(
1362                    self, spec,
1363                )?]
1364            };
1365            let patch_ids =
1366                crate::rev_list::map_commits_in_parallel(&commits, |c| self.patch_id(c, mode));
1367            for (patch_id, commit) in patch_ids.into_iter().zip(commits) {
1368                if let Some(patch_id) = patch_id? {
1369                    out.push((patch_id, commit));
1370                }
1371            }
1372        }
1373        Ok(out)
1374    }
1375
1376    /// Reflog listing, deletion, and expiry for this repository.
1377    #[must_use]
1378    pub fn reflogs(&self) -> crate::repo_handles::RepositoryReflogs<'_> {
1379        crate::repo_handles::RepositoryReflogs::new(self)
1380    }
1381
1382    /// Bundle create, verify, list-heads, and unbundle for this repository.
1383    #[must_use]
1384    pub fn bundles(&self) -> crate::repo_handles::RepositoryBundles<'_> {
1385        crate::repo_handles::RepositoryBundles::new(self)
1386    }
1387
1388    /// Repack reachable objects into a single pack and prune redundant storage.
1389    ///
1390    /// # Errors
1391    ///
1392    /// Propagates pack, prune, or unsupported-backend failures from [`crate::maintenance::repack`].
1393    pub fn repack(
1394        &self,
1395        opts: &crate::maintenance::RepackOptions,
1396    ) -> crate::error::Result<crate::maintenance::RepackReport> {
1397        crate::maintenance::repack(self, opts)
1398    }
1399
1400    /// Resolve `refname` through this repository's ref store.
1401    ///
1402    /// # Errors
1403    ///
1404    /// Propagates [`crate::refs::store::RefStoreError`] as [`Error::RefStore`].
1405    pub(crate) fn resolve_ref_oid(&self, refname: &str) -> Result<crate::objects::ObjectId> {
1406        self.refs().resolve(refname)
1407    }
1408
1409    /// Replace the ref store (embedders injecting [`crate::refs::store::MemoryRefStore`]).
1410    ///
1411    /// On a linked worktree's handle, `HEAD` and its reflog stay in the worktree's admin
1412    /// directory (see [`RepositoryBuilder::ref_store`]).
1413    #[must_use]
1414    pub fn with_ref_store(mut self, store: Arc<dyn crate::refs::store::RefStore>) -> Self {
1415        let store = linked_worktree_view(&self.git_dir, store);
1416        self.caches
1417            .install_ref_store(&self.git_dir, Arc::clone(&store));
1418        self.ref_store = checked_ref_store_with_hooks(
1419            store,
1420            self.odb.clone(),
1421            self.git_dir(),
1422            self.work_tree().map(Path::as_ref),
1423            &self.environment,
1424            &self.command_runner,
1425            self.hook_stdio,
1426            &self.diagnostics,
1427        );
1428        self
1429    }
1430
1431    /// Whether pathspec matching should NFC-normalize paths for this repository.
1432    #[must_use]
1433    pub(crate) fn pathspec_precompose_enabled(&self) -> bool {
1434        self.caches
1435            .pathspec_precompose_enabled(self.environment.as_ref(), &self.git_dir)
1436    }
1437
1438    /// Pretend the work tree aliases NFD/NFC spellings, as APFS and HFS+ do, so unit tests on
1439    /// other filesystems exercise the precomposed pathspec matching macOS users get.
1440    #[cfg(test)]
1441    pub(crate) fn force_pathspec_precompose_for_test(&self) {
1442        self.caches.force_pathspec_precompose_for_test();
1443    }
1444
1445    /// Resolve one argv pathspec against this repository's work tree.
1446    ///
1447    /// `prefix` is the work-tree-relative directory the pathspec was typed in (`None` at the
1448    /// root). Magic pathspecs pass through; plain paths are joined with `prefix`, normalized, and
1449    /// NFC-normalized when `core.precomposeunicode` is on and the filesystem aliases NFD/NFC
1450    /// spellings. That decision is computed once per handle, so resolving many pathspecs costs no
1451    /// config reload or filesystem probe per path.
1452    ///
1453    /// # Errors
1454    ///
1455    /// [`crate::pathspec::PathspecResolveError::NeedsWorkTree`] for a bare repository, and
1456    /// [`crate::pathspec::PathspecResolveError::OutsideRepository`] when `pathspec` resolves
1457    /// outside the work tree.
1458    pub fn resolve_worktree_pathspec(
1459        &self,
1460        pathspec: &str,
1461        prefix: Option<&str>,
1462    ) -> std::result::Result<String, crate::pathspec::PathspecResolveError> {
1463        let work_tree = self
1464            .work_tree()
1465            .ok_or(crate::pathspec::PathspecResolveError::NeedsWorkTree)?;
1466        Ok(
1467            crate::pathspec::resolve_pathspec_in_worktree_with_argv_precompose(
1468                pathspec,
1469                pathspec,
1470                work_tree,
1471                prefix,
1472                self.pathspec_precompose_enabled(),
1473            )?,
1474        )
1475    }
1476
1477    #[must_use]
1478    pub(crate) fn pathspec_match_context(&self) -> crate::pathspec::PathspecMatchContext {
1479        crate::pathspec::pathspec_match_context_with_precompose(self.pathspec_precompose_enabled())
1480    }
1481
1482    /// Whether `path` is selected by the resolved pathspec list `specs`.
1483    ///
1484    /// Shorthand for [`Self::pathspec_matcher`] followed by
1485    /// [`crate::pathspec::PathspecMatcher::matches`]; build the matcher once instead when
1486    /// filtering many paths with specs that may carry `:(attr:...)` magic.
1487    ///
1488    /// # Errors
1489    ///
1490    /// Same as [`Self::pathspec_matcher`].
1491    pub fn paths_match_pathspecs(&self, path: &str, specs: &[String]) -> Result<bool> {
1492        Ok(self.pathspec_matcher(specs)?.matches(path))
1493    }
1494
1495    /// Prepare `specs` (resolved pathspecs) for matching many paths in this repository.
1496    ///
1497    /// The matcher applies Git's list semantics and every pathspec magic word, including
1498    /// `:(attr:...)`, which is evaluated against this repository's attributes: the work-tree
1499    /// stack (global file, root and nested `.gitattributes`, `info/attributes`), or
1500    /// `info/attributes` and the global file for a bare repository. Attributes are read only
1501    /// when a spec carries attr magic, once per matcher, from the per-handle cache; `[attr]`
1502    /// macros are expanded and `core.ignorecase` makes attribute patterns case-insensitive.
1503    /// Paths and specs follow `core.precomposeunicode`.
1504    ///
1505    /// # Errors
1506    ///
1507    /// Only when a spec carries attr magic: [`Error::InvalidAttrPathspec`] for a malformed or
1508    /// repeated `attr:` clause, or the error from loading config or the attribute files (for
1509    /// example an unreadable `core.attributesFile` setting or a poisoned cache lock).
1510    pub fn pathspec_matcher<'s>(
1511        &self,
1512        specs: &'s [String],
1513    ) -> Result<crate::pathspec::PathspecMatcher<'s>> {
1514        let precompose = self.pathspec_precompose_enabled();
1515        if !specs
1516            .iter()
1517            .any(|spec| crate::pathspec::pathspec_has_attr_magic(spec))
1518        {
1519            return Ok(crate::pathspec::PathspecMatcher::new(
1520                specs, precompose, None, false,
1521            ));
1522        }
1523        crate::pathspec::validate_attr_pathspecs(specs)?;
1524        let attrs = match self.work_tree() {
1525            Some(work_tree) => self.caches().gitattributes_stack_shared(self, work_tree)?,
1526            None => self.caches().gitattributes_bare_shared(self)?,
1527        };
1528        let icase = crate::crlf::config_ignorecase(self.config()?.as_ref());
1529        Ok(crate::pathspec::PathspecMatcher::new(
1530            specs,
1531            precompose,
1532            Some(attrs),
1533            icase,
1534        ))
1535    }
1536
1537    /// Repository-relative prefix of the process cwd for argv pathspecs (no trailing slash).
1538    ///
1539    /// Matches Git `GIT_PREFIX` / `pathdiff` semantics used by `git log --`, `git rev-list --`,
1540    /// and `git blame` from a subdirectory.
1541    #[must_use]
1542    pub fn pathspec_cwd_prefix(&self) -> Option<&str> {
1543        self.git_prefix.as_deref().filter(|p| !p.is_empty())
1544    }
1545
1546    /// Cwd prefix for one argv pathspec (`None` for `:(top)` / `:/`, which stay repo-root-relative).
1547    pub fn pathspec_argv_prefix<'a>(&'a self, pathspec: &str) -> Option<&'a str> {
1548        if crate::pathspec::pathspec_has_top(pathspec) {
1549            None
1550        } else {
1551            self.pathspec_cwd_prefix()
1552        }
1553    }
1554
1555    /// Resolve argv path operands for revision walks (`log`, `revs`, `rev-list`, …).
1556    ///
1557    /// Literal segments are interpreted relative to the process cwd under the work tree, like
1558    /// `git diff --` and Git pathspec cwd rules. Exclude-only lists gain Git's implicit cwd
1559    /// positive prefix when appropriate.
1560    ///
1561    /// # Errors
1562    ///
1563    /// Returns [`Error::PathError`] when a pathspec resolves outside the work tree.
1564    pub fn resolve_rev_walk_pathspecs(&self, paths: &[String]) -> Result<Vec<String>> {
1565        if paths.is_empty() {
1566            return Ok(Vec::new());
1567        }
1568        let prefix = self.pathspec_cwd_prefix();
1569        let cwd_for_implicit = prefix.map(str::to_owned);
1570        let mut resolved = Vec::with_capacity(paths.len());
1571        if self.work_tree().is_some() {
1572            for pathspec in paths {
1573                let prefix = Self::pathspec_argv_prefix(self, pathspec);
1574                let spec = self
1575                    .resolve_worktree_pathspec(pathspec, prefix)
1576                    .map_err(|e| Error::PathError(e.to_string()))?;
1577                resolved.push(spec);
1578            }
1579        } else {
1580            let root = self.git_dir();
1581            for pathspec in paths {
1582                let prefix = Self::pathspec_argv_prefix(self, pathspec);
1583                resolved.push(crate::pathspec::resolve_pathspec(pathspec, root, prefix));
1584            }
1585        }
1586        Ok(crate::pathspec::extend_pathspec_list_implicit_cwd(
1587            &resolved,
1588            cwd_for_implicit.as_deref(),
1589        ))
1590    }
1591
1592    /// Absolute path to the git directory (`.git/` or bare repository root).
1593    ///
1594    /// The path is canonical. On Windows it uses the plain drive or UNC spelling (`C:\repo`)
1595    /// that callers pass in and compare against, not the `\\?\` form [`std::fs::canonicalize`]
1596    /// returns; the verbatim form is kept only when nothing else names the directory (longer
1597    /// than `MAX_PATH`, or a component Win32 would rewrite, such as a trailing dot).
1598    #[must_use]
1599    pub fn git_dir(&self) -> &Path {
1600        &self.git_dir
1601    }
1602
1603    /// Absolute work tree path for non-bare repositories.
1604    ///
1605    /// The path is canonical. On Windows it uses the plain drive or UNC spelling (`C:\repo`)
1606    /// that callers pass in and compare against, not the `\\?\` form [`std::fs::canonicalize`]
1607    /// returns; the verbatim form is kept only when nothing else names the directory (longer
1608    /// than `MAX_PATH`, or a component Win32 would rewrite, such as a trailing dot).
1609    #[must_use]
1610    pub fn work_tree(&self) -> Option<&Path> {
1611        self.work_tree.as_deref()
1612    }
1613
1614    /// Object database for this repository.
1615    #[must_use]
1616    pub fn odb(&self) -> &Odb {
1617        &self.odb
1618    }
1619
1620    /// Fetch missing promisor objects in one batch before checkout or other bulk reads.
1621    ///
1622    /// # Errors
1623    ///
1624    /// Returns [`crate::error::Error::Message`] when no fetcher is installed, or transport failures.
1625    pub fn prefetch_promised(&self, oids: &[ObjectId]) -> Result<()> {
1626        let Some(fetcher) = self.caches().promisor_fetcher() else {
1627            return Err(Error::Message(
1628                "promisor prefetch requires a PromisorFetcher on RepositoryOptions".into(),
1629            ));
1630        };
1631        fetcher.fetch_missing(self, oids)
1632    }
1633
1634    /// Discovery provenance for safe-directory and setup tracing.
1635    #[must_use]
1636    pub fn discovery_info(&self) -> DiscoveryInfo {
1637        DiscoveryInfo {
1638            explicit_git_dir: self.explicit_git_dir,
1639            discovery_root: self.discovery_root.clone(),
1640            work_tree_from_env: self.work_tree_from_env,
1641            discovery_via_gitfile: self.discovery_via_gitfile,
1642        }
1643    }
1644
1645    fn cached_settings(&self) -> RepoCachedSettings {
1646        if let Ok(guard) = self.config_session.lock() {
1647            if let Some(session) = guard.as_ref() {
1648                return session.cached_settings.clone();
1649            }
1650        }
1651        self.ensure_config_arc_fast()
1652            .map(|cfg| {
1653                Self::repo_cached_settings_from_config(cfg.as_ref(), self.environment.as_ref())
1654            })
1655            .unwrap_or_else(|_| {
1656                Self::repo_cached_settings_from_config(&ConfigSet::new(), self.environment.as_ref())
1657            })
1658    }
1659
1660    /// Initialise a repository at `path` and return an open handle.
1661    ///
1662    /// # Errors
1663    ///
1664    /// Propagates filesystem and layout errors from the underlying init helpers.
1665    pub fn init(path: &Path, options: &InitOptions) -> Result<Self> {
1666        Self::init_with_outcome(path, options).map(|(repo, _)| repo)
1667    }
1668
1669    /// Like [`Self::init`], but also reports whether the repository was reinitialized and
1670    /// the effective initial branch name.
1671    ///
1672    /// # Errors
1673    ///
1674    /// Propagates filesystem and layout errors from the underlying init helpers.
1675    pub fn init_with_outcome(path: &Path, options: &InitOptions) -> Result<(Self, InitOutcome)> {
1676        if options.bare && options.separate_git_dir.is_some() {
1677            return Err(Error::InitBareWithSeparateGitDir);
1678        }
1679        let env = options
1680            .environment
1681            .clone()
1682            .unwrap_or_else(Environment::empty);
1683        let git_dir = resolve_git_dir_for_init(path, options)?;
1684        let reinitialized = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
1685        let existing_format = hash_algo_from_git_dir(&git_dir)?;
1686        let object_format = match (reinitialized, options.object_format) {
1687            (true, None) => existing_format,
1688            (true, Some(requested)) if requested != existing_format => {
1689                return Err(Error::InitObjectFormatChange);
1690            }
1691            (true, Some(requested)) => requested,
1692            (false, None) => HashAlgo::Sha1,
1693            (false, Some(requested)) => requested,
1694        };
1695        let initial_branch = if reinitialized {
1696            head_branch_name_for_outcome(&git_dir)?
1697        } else {
1698            resolve_initial_branch(&env, options.initial_branch.as_deref())?
1699        };
1700
1701        let separate_layout = options.separate_git_dir.is_some()
1702            || (!options.bare
1703                && !options.bare_with_worktree_at_path
1704                && path.join(".git").is_file());
1705
1706        let repo = if options.bare_with_worktree_at_path {
1707            init_bare_with_env_worktree(
1708                path,
1709                path,
1710                &initial_branch,
1711                options.template_dir.as_deref(),
1712                options.ref_storage,
1713                object_format,
1714                &env,
1715            )?
1716        } else if separate_layout {
1717            init_repository_separate_git_dir(
1718                path,
1719                &git_dir,
1720                &initial_branch,
1721                options.template_dir.as_deref(),
1722                options.ref_storage,
1723                object_format,
1724                &env,
1725            )?
1726        } else {
1727            init_repository_with_object_format(
1728                path,
1729                options.bare,
1730                &initial_branch,
1731                options.template_dir.as_deref(),
1732                options.ref_storage,
1733                object_format,
1734                &env,
1735            )?
1736        };
1737        Ok((
1738            repo,
1739            InitOutcome {
1740                reinitialized,
1741                initial_branch,
1742            },
1743        ))
1744    }
1745
1746    /// Open or resolve a repository at `path` using `options`.
1747    ///
1748    /// When [`RepositoryOptions::open_for_serving`] is set, `path` is resolved like
1749    /// smart-HTTP serving (work tree, bare root, or `path.git`). Otherwise `path` is
1750    /// the git directory and [`RepositoryOptions::work_tree`] selects the work tree.
1751    ///
1752    /// # Errors
1753    ///
1754    /// Returns [`Error::NotARepository`] when the layout is invalid.
1755    pub fn open(path: &Path, options: &RepositoryOptions) -> Result<Self> {
1756        if options.open_for_serving {
1757            return Self::open_for_serving_internal(path, options);
1758        }
1759        Self::open_internal(options, path)
1760    }
1761
1762    fn open_with_work_tree(
1763        options: &RepositoryOptions,
1764        git_dir: &Path,
1765        work_tree: Option<&Path>,
1766    ) -> Result<Self> {
1767        let mut opts = options.clone();
1768        opts.work_tree = work_tree.map(|p| p.to_path_buf());
1769        Self::open_internal(&opts, git_dir)
1770    }
1771
1772    fn open_internal(options: &RepositoryOptions, git_dir: &Path) -> Result<Self> {
1773        Self::open_internal_with_refs(options, git_dir, None)
1774    }
1775
1776    fn open_internal_with_refs(
1777        options: &RepositoryOptions,
1778        git_dir: &Path,
1779        injected_refs: Option<Arc<dyn crate::refs::store::RefStore>>,
1780    ) -> Result<Self> {
1781        crate::trace::trace_span!("grit.repository.open");
1782        let work_tree = options.work_tree.as_deref();
1783        let git_dir = git_dir
1784            .canonicalize_simplified()
1785            .map_err(|_| not_a_repository_at(git_dir))?;
1786
1787        let repo = Self::from_canonical_git_dir(options, git_dir, work_tree, injected_refs)?;
1788        repo.refresh_config_session()?;
1789        if options.validate_repository_format {
1790            repo.validate_repository_format_cached()?;
1791        }
1792        repo.install_config_snapshot();
1793        warn_core_bare_worktree_conflict(options, &repo);
1794        let _ = trace_repo_setup_if_requested(&repo);
1795        Ok(repo)
1796    }
1797
1798    fn open_for_serving_internal(path: &Path, options: &RepositoryOptions) -> Result<Self> {
1799        let mut with_git_suffix = path.as_os_str().to_owned();
1800        with_git_suffix.push(".git");
1801        let candidates = [
1802            (path.join(".git"), Some(path.to_path_buf())),
1803            (path.to_path_buf(), None),
1804            (PathBuf::from(with_git_suffix), None),
1805        ];
1806        for (git_dir, work_tree) in candidates {
1807            if !git_dir.join("HEAD").is_file() {
1808                continue;
1809            }
1810            let mut opts = options.clone();
1811            opts.work_tree = work_tree;
1812            opts.open_for_serving = false;
1813            return Self::open_internal(&opts, &git_dir);
1814        }
1815        Err(not_a_repository_at(path))
1816    }
1817
1818    fn open_with_odb_internal(
1819        options: &RepositoryOptions,
1820        git_dir: &Path,
1821        mut builder: crate::odb::OdbBuilder,
1822        injected_refs: Option<Arc<dyn crate::refs::store::RefStore>>,
1823    ) -> Result<Self> {
1824        let work_tree = options.work_tree.as_deref();
1825        let environment = Arc::new(options.environment.clone());
1826        let cwd = environment.discovery_cwd();
1827        let command_runner = Arc::clone(&options.command_runner);
1828        let change_id_generator = Arc::clone(&options.change_id_generator);
1829
1830        // With both stores injected nothing is read from the git directory's layout, so it
1831        // may be empty or missing; config and the index are still looked up there.
1832        let fully_injected = injected_refs.is_some() && builder.has_custom_primary();
1833        let requested_git_dir = cwd.join(git_dir);
1834        let git_dir = match requested_git_dir.canonicalize_simplified() {
1835            Ok(dir) => dir,
1836            Err(_) if fully_injected => canonicalize_existing_prefix(&requested_git_dir)?,
1837            Err(_) => return Err(not_a_repository_at(git_dir)),
1838        };
1839
1840        if injected_refs.is_none() && !head_file_present(&git_dir) {
1841            return Err(not_a_repository_at(&git_dir));
1842        }
1843
1844        let common_git_dir = resolve_common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
1845        let objects_dir = resolve_objects_dir(&environment, &git_dir, &common_git_dir)
1846            .ok_or_else(|| not_a_repository_at(&git_dir))?;
1847
1848        // A custom primary does not need objects/ to exist, but the builder must still name it.
1849        if !builder.has_custom_primary() && !objects_dir.exists() {
1850            return Err(not_a_repository_at(&git_dir));
1851        }
1852        let objects_dir = canonicalize_existing_prefix(&objects_dir)?;
1853        let builder_objects = canonicalize_existing_prefix(&cwd.join(builder.objects_dir()))?;
1854        if builder_objects != objects_dir {
1855            return Err(Error::OdbObjectsDirMismatch {
1856                builder: builder_objects,
1857                repository: objects_dir,
1858            });
1859        }
1860        builder.set_objects_dir(objects_dir);
1861        let injected_algos: Vec<HashAlgo> = builder.injected_hash_algos().collect();
1862
1863        let work_tree = match work_tree {
1864            Some(p) => {
1865                let mut resolved = cwd.join(p);
1866                if resolved.exists() {
1867                    resolved = resolved
1868                        .canonicalize_simplified()
1869                        .map_err(|_| Error::PathError(p.display().to_string()))?;
1870                }
1871                Some(resolved)
1872            }
1873            None => None,
1874        };
1875
1876        let caches = RepoCaches::with_diagnostics(
1877            Arc::clone(&command_runner),
1878            Arc::clone(&options.diagnostics),
1879        );
1880        let odb = builder
1881            .build()
1882            .with_config_git_dir(git_dir.clone())
1883            .with_shared_config_state(caches.clone(), environment.clone())
1884            .with_resolved_work_tree(work_tree.clone());
1885
1886        let git_prefix = work_tree
1887            .as_ref()
1888            .and_then(|wt| compute_git_prefix(environment.as_ref(), wt));
1889
1890        let ref_store = checked_ref_store_with_hooks(
1891            open_or_install_ref_store(&caches, &git_dir, &environment, injected_refs)?,
1892            odb.clone(),
1893            &git_dir,
1894            work_tree.as_deref(),
1895            &environment,
1896            &command_runner,
1897            options.hook_stdio,
1898            &options.diagnostics,
1899        );
1900        caches.configure_filter_subprocess(&environment, &git_dir, work_tree.as_deref());
1901
1902        let repo = Self {
1903            git_dir,
1904            common_git_dir,
1905            work_tree,
1906            odb,
1907            explicit_git_dir: false,
1908            discovery_root: None,
1909            work_tree_from_env: false,
1910            discovery_via_gitfile: false,
1911            config_session: Mutex::new(None),
1912            caches,
1913            ref_store,
1914            environment,
1915            git_prefix,
1916            command_runner,
1917            change_id_generator,
1918            diagnostics: Arc::clone(&options.diagnostics),
1919            network_trace: options.network_trace,
1920            reference_unix_time: options.reference_unix_time,
1921            test_assume_different_owner: options.test_assume_different_owner,
1922            thread_policy: options.thread_policy,
1923            hook_stdio: options.hook_stdio,
1924        };
1925        repo.refresh_config_session()?;
1926        if options.validate_repository_format {
1927            repo.validate_repository_format_cached()?;
1928        }
1929        repo.check_injected_object_format(&injected_algos)?;
1930        warn_core_bare_worktree_conflict(options, &repo);
1931        let _ = trace_repo_setup_if_requested(&repo);
1932        Ok(repo)
1933    }
1934
1935    /// Fail with [`Error::ObjectFormatMismatch`] when one of `injected` (the hash algorithms
1936    /// of the stores and commit-graph passed to [`RepositoryBuilder::odb_builder`]) differs from
1937    /// `extensions.objectformat`, SHA-1 when unset.
1938    ///
1939    /// Reads the config snapshot the open already loaded. Files-backed primaries and
1940    /// alternates take the repository's algorithm themselves and are not listed.
1941    fn check_injected_object_format(&self, injected: &[HashAlgo]) -> Result<()> {
1942        if injected.is_empty() {
1943            return Ok(());
1944        }
1945        let repository = self
1946            .ensure_config_arc_fast()?
1947            .get("extensions.objectformat")
1948            .and_then(|name| HashAlgo::from_name(&name))
1949            .unwrap_or(HashAlgo::Sha1);
1950        match injected.iter().find(|algo| **algo != repository) {
1951            Some(&store) => Err(Error::ObjectFormatMismatch { repository, store }),
1952            None => Ok(()),
1953        }
1954    }
1955
1956    /// Discover a repository starting from `start` (defaults to [`Environment::cwd`] when `None`).
1957    pub fn discover(start: Option<&Path>, options: &RepositoryOptions) -> Result<Self> {
1958        let env = &options.environment;
1959        let cwd = env.discovery_cwd();
1960
1961        // GIT_DIR override
1962        if let Some(dir) = env.git_dir.as_deref() {
1963            let mut git_dir = PathBuf::from(dir);
1964            if git_dir.is_relative() {
1965                git_dir = cwd.join(git_dir);
1966            }
1967            // `GIT_DIR` may name a gitfile (`.git` as a file); resolve like Git's `read_gitfile`.
1968            git_dir = resolve_git_dir_env_path(&git_dir)?;
1969            let work_tree = env.git_work_tree.as_deref().map(|wt| {
1970                let p = PathBuf::from(wt);
1971                if p.is_absolute() {
1972                    p
1973                } else {
1974                    cwd.join(p)
1975                }
1976            });
1977            if let Some(ref wt_path) = work_tree {
1978                if env
1979                    .git_work_tree
1980                    .as_deref()
1981                    .is_some_and(|raw| Path::new(raw).is_absolute())
1982                {
1983                    validate_git_work_tree_path(wt_path)?;
1984                }
1985            }
1986            if work_tree.is_some() {
1987                let mut repo = Self::open_with_work_tree(options, &git_dir, work_tree.as_deref())?;
1988                repo.explicit_git_dir = true;
1989                repo.discovery_root = None;
1990                repo.work_tree_from_env = false;
1991                repo.discovery_via_gitfile = false;
1992                return Ok(repo);
1993            }
1994            // `GIT_DIR` without `GIT_WORK_TREE`: honour `core.bare` / `core.worktree` like Git.
1995            let (is_bare, core_wt) = read_core_bare_and_worktree(&git_dir)?;
1996            let bare_worktree_conflict = is_bare && core_wt.is_some();
1997            let resolved_wt = if is_bare {
1998                None
1999            } else if let Some(raw) = core_wt {
2000                Some(resolve_core_worktree_path(&git_dir, &raw)?)
2001            } else {
2002                // Without `GIT_WORK_TREE`, Git uses the current working directory as the work
2003                // tree root (see git-config(1) / `git help repository-layout`), not the parent
2004                // of `$GIT_DIR`. This matches upstream tests that run
2005                // `GIT_DIR=other/.git git …` from the top-level repo while manipulating paths
2006                // under `$PWD` (e.g. t5402-post-merge-hook).
2007                Some(
2008                    cwd.canonicalize_simplified()
2009                        .unwrap_or_else(|_| cwd.clone()),
2010                )
2011            };
2012            let mut repo = Self::open_with_work_tree(options, &git_dir, resolved_wt.as_deref())?;
2013            repo.explicit_git_dir = true;
2014            repo.discovery_root = None;
2015            repo.work_tree_from_env = false;
2016            repo.discovery_via_gitfile = false;
2017            let _ = bare_worktree_conflict;
2018            return Ok(repo);
2019        }
2020
2021        // If GIT_WORK_TREE is set without GIT_DIR, we still need to honor it
2022        // after discovery (path is relative to cwd, like Git).
2023        let env_work_tree = env.git_work_tree.as_deref().map(|wt| {
2024            let p = PathBuf::from(wt);
2025            if p.is_absolute() {
2026                p
2027            } else {
2028                cwd.join(p)
2029            }
2030        });
2031        if let Some(ref p) = env_work_tree {
2032            if env
2033                .git_work_tree
2034                .as_deref()
2035                .is_some_and(|raw| Path::new(raw).is_absolute())
2036            {
2037                validate_git_work_tree_path(p)?;
2038            }
2039        }
2040        let start = start.unwrap_or(&cwd);
2041        let start = if start.is_absolute() {
2042            start.to_path_buf()
2043        } else {
2044            cwd.join(start)
2045        };
2046
2047        // Parse GIT_CEILING_DIRECTORIES — mirror Git `setup_git_directory_gently_1` +
2048        // `longest_ancestor_length` on the canonical cwd path.
2049        // A leading colon disables symlink resolution for both ceiling paths and cwd.
2050        let (ceiling_paths, no_resolve_ceilings) = parse_ceiling_directories(env);
2051        let ceiling_dirs: Vec<String> = ceiling_paths
2052            .into_iter()
2053            .map(|p| path_for_ceiling_compare(&p))
2054            .collect();
2055
2056        let start_canon = start
2057            .canonicalize_simplified()
2058            .unwrap_or_else(|_| start.clone());
2059        // For ceiling comparison, use non-canonical path when leading colon disables resolution.
2060        let ceil_cmp_buf = if no_resolve_ceilings {
2061            path_for_ceiling_compare(&start)
2062        } else {
2063            path_for_ceiling_compare(&start_canon)
2064        };
2065        let mut dir_buf = path_for_ceiling_compare(&start_canon);
2066        let min_offset = offset_1st_component(&dir_buf);
2067        let mut ceil_offset: isize = longest_ancestor_length(&ceil_cmp_buf, &ceiling_dirs)
2068            .map(|n| n as isize)
2069            .unwrap_or(-1);
2070        if ceiling_dirs.contains(&dir_buf) {
2071            ceil_offset = dir_buf.len() as isize;
2072        } else if ceil_offset < 0 {
2073            ceil_offset = min_offset as isize - 2;
2074        }
2075
2076        loop {
2077            let current = Path::new(&dir_buf);
2078            if let Some(DiscoveredAt { mut repo, gitfile }) = try_open_at(options, current)? {
2079                // git/setup.c `setup_git_directory` runs `check_repository_format` on the resolved
2080                // git dir and dies on a bad format (e.g. a v1-only `extensions.*` in a
2081                // `repositoryformatversion = 0` repo; t0001 #60). Discovery itself opens with
2082                // validation skipped so an empty `.git/` is walked past, but a *found* repository
2083                // must satisfy the format check.
2084                repo.environment = Arc::new(env.clone());
2085                repo.refresh_config_session()?;
2086                repo.validate_repository_format_cached()?;
2087                repo.install_config_snapshot();
2088                let cfg = repo.config()?;
2089                repo.set_diagnostics(Arc::clone(&options.diagnostics));
2090                if let Some(ref wt) = env_work_tree {
2091                    repo.work_tree =
2092                        Some(wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone()));
2093                    repo.work_tree_from_env = true;
2094                } else {
2095                    repo.work_tree_from_env = false;
2096                    // Linked worktree (gitfile → admin dir with `commondir`): `Repository::open`
2097                    // already set `work_tree` to the directory that contains the `.git` file.
2098                    // Do not replace it with `core.worktree` from the common config — it may be
2099                    // stale (t1501 multi-worktree) or point at another linked checkout.
2100                    let linked_gitfile =
2101                        repo.discovery_via_gitfile && resolve_common_dir(&repo.git_dir).is_some();
2102                    if !linked_gitfile {
2103                        let (is_bare, core_wt) =
2104                            read_core_bare_and_worktree_from_config(cfg.as_ref());
2105                        if is_bare {
2106                            repo.work_tree = None;
2107                        } else if let Some(raw) = core_wt {
2108                            repo.work_tree = Some(resolve_core_worktree_path(&repo.git_dir, &raw)?);
2109                        }
2110                    }
2111                }
2112                let assume_different =
2113                    options.test_assume_different_owner || env.test_assume_different_owner();
2114                if assume_different {
2115                    repo.enforce_safe_directory()?;
2116                } else {
2117                    ensure_valid_ownership(
2118                        env,
2119                        gitfile.as_deref(),
2120                        repo.work_tree.as_deref(),
2121                        &repo.git_dir,
2122                    )?;
2123                }
2124                if let Some(wt) = repo.work_tree.as_ref() {
2125                    repo.git_prefix = compute_git_prefix(env, wt);
2126                }
2127                let _ = trace_repo_setup_if_requested(&repo);
2128                return Ok(repo);
2129            }
2130
2131            let mut offset: isize = dir_buf.len() as isize;
2132            if offset <= min_offset as isize {
2133                break;
2134            }
2135            loop {
2136                offset -= 1;
2137                if offset <= ceil_offset {
2138                    break;
2139                }
2140                if dir_buf
2141                    .as_bytes()
2142                    .get(offset as usize)
2143                    .is_some_and(|b| *b == b'/')
2144                {
2145                    break;
2146                }
2147            }
2148            if offset <= ceil_offset {
2149                break;
2150            }
2151            let off_u = offset as usize;
2152            let new_len = if off_u > min_offset {
2153                off_u
2154            } else {
2155                min_offset
2156            };
2157            dir_buf.truncate(new_len);
2158        }
2159
2160        Err(not_a_repository_at(&start))
2161    }
2162
2163    /// Path to the index file.
2164    #[must_use]
2165    pub(crate) fn index_path(&self) -> PathBuf {
2166        self.git_dir.join("index")
2167    }
2168
2169    /// Resolve which index file to use, honouring `GIT_INDEX_FILE` like Git plumbing.
2170    ///
2171    /// Relative paths are resolved from the process current directory.
2172    pub(crate) fn index_path_for_env(&self) -> Result<PathBuf> {
2173        if let Some(raw) = self.environment.git_index_file.as_deref() {
2174            if !raw.is_empty() {
2175                let p = PathBuf::from(raw);
2176                return Ok(if p.is_absolute() {
2177                    p
2178                } else {
2179                    self.environment.discovery_cwd().join(p)
2180                });
2181            }
2182        }
2183        Ok(self.index_path())
2184    }
2185
2186    /// Load the index, expanding sparse-directory placeholders from the object database.
2187    ///
2188    /// Commands that operate on individual paths should use this instead of [`Index::load`].
2189    pub(crate) fn load_index(&self) -> Result<Index> {
2190        let path = self.index_path_for_env()?;
2191        self.load_index_at(&path)
2192    }
2193
2194    /// Like [`Repository::load_index`], but reads from an explicit index file path
2195    /// (e.g. `GIT_INDEX_FILE` or a worktree-specific index).
2196    pub(crate) fn load_index_at(&self, path: &std::path::Path) -> Result<Index> {
2197        self.load_index_at_with_context(
2198            path,
2199            crate::index::IndexParseContext::from_policy(self.thread_policy()),
2200        )
2201    }
2202
2203    pub(crate) fn load_index_at_with_context(
2204        &self,
2205        path: &std::path::Path,
2206        parse_ctx: crate::index::IndexParseContext,
2207    ) -> Result<Index> {
2208        let mut idx = self.load_index_at_preserving_sparse(path, parse_ctx)?;
2209        let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
2210        idx.expand_sparse_directory_placeholders(
2211            &self.odb,
2212            crate::path_validate::ProtectOptions::from_config(cfg.as_ref()),
2213        )?;
2214        self.clear_present_skip_worktree(&mut idx, cfg.as_ref());
2215        Ok(idx)
2216    }
2217
2218    /// Load rows for a sparse-aware operation, with the same repository context as `load_index`.
2219    pub(crate) fn load_index_preserving_sparse(&self) -> Result<Index> {
2220        self.load_index_at_preserving_sparse(
2221            &self.index_path_for_env()?,
2222            crate::index::IndexParseContext::from_policy(self.thread_policy()),
2223        )
2224    }
2225
2226    /// Load the index at `path` without expanding sparse-directory placeholders (and
2227    /// so without reading any tree), resolving a split index.
2228    pub(crate) fn load_index_at_preserving_sparse(
2229        &self,
2230        path: &std::path::Path,
2231        parse_ctx: crate::index::IndexParseContext,
2232    ) -> Result<Index> {
2233        crate::trace::trace_span!("grit.index.read");
2234        let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
2235        if let Some(res) = cfg.as_ref().get_bool("index.sparse") {
2236            res.map_err(|s| Error::Config(s.into()))?;
2237        }
2238        let mut idx = Index::load_preserving_sparse_optional_with_context(
2239            path,
2240            &self.odb,
2241            Some(cfg.as_ref()),
2242            parse_ctx,
2243        )?;
2244        crate::split_index::resolve_split_index_if_needed(&mut idx, &self.git_dir, path)?;
2245        if idx.source_mtime.is_none() {
2246            idx.source_mtime = crate::index::index_file_mtime(path);
2247        }
2248        Ok(idx)
2249    }
2250
2251    pub(crate) fn clear_present_skip_worktree(&self, index: &mut Index, cfg: &ConfigSet) {
2252        if let Some(ref wt) = self.work_tree {
2253            crate::sparse_checkout::clear_skip_worktree_from_present_files(
2254                &self.git_dir,
2255                wt,
2256                index,
2257                Some(cfg),
2258            );
2259        }
2260    }
2261
2262    /// Write the index to the default path after optionally collapsing skip-worktree
2263    /// subtrees into sparse-directory placeholders (when sparse index is enabled).
2264    pub(crate) fn write_index(&self, index: &mut Index) -> Result<()> {
2265        self.write_index_with_post_index_change(index, false, false)
2266    }
2267
2268    /// Acquire `.git/index.lock` before mutating the working tree for index+worktree commands.
2269    ///
2270    /// Call [`Self::commit_index_update`] to persist the index and release the lock, or drop the
2271    /// lock without committing to abort without writing.
2272    ///
2273    /// # Errors
2274    ///
2275    /// Returns [`Error::Io`] when the lock file already exists or cannot be created.
2276    pub(crate) fn begin_index_update(&self) -> Result<crate::index::IndexLock> {
2277        test_inject_index_write_fail()?;
2278        let _ = crate::porcelain::commit::reap_stale_next_index_files(self, false)?;
2279        let path = self.index_path_for_env()?;
2280        let cfg = self.config()?;
2281        crate::index::IndexLock::acquire(&path, Some(cfg.as_ref()))
2282    }
2283
2284    /// Write `index` to disk using a lock from [`Self::begin_index_update`].
2285    ///
2286    /// # Errors
2287    ///
2288    /// Propagates index finalization, serialization, and I/O failures.
2289    pub(crate) fn commit_index_update(
2290        &self,
2291        lock: &mut crate::index::IndexLock,
2292        index: &mut Index,
2293        updated_workdir: bool,
2294    ) -> Result<()> {
2295        self.flush_index_update(lock, index)?;
2296        self.finish_index_update(lock, updated_workdir)
2297    }
2298
2299    /// Serialize `index` into a held [`crate::index::IndexLock`] without replacing the on-disk index.
2300    pub(crate) fn flush_index_update(
2301        &self,
2302        lock: &mut crate::index::IndexLock,
2303        index: &mut Index,
2304    ) -> Result<()> {
2305        let path = lock.index_path.clone();
2306        let (skip_hash, cfg) = self.prepare_index_for_disk_write(&path, index)?;
2307        write_index_file_split(
2308            &path,
2309            &self.git_dir,
2310            index,
2311            cfg.as_ref(),
2312            WriteSplitIndexRequest::default(),
2313            skip_hash,
2314            self.thread_policy(),
2315            Some(lock),
2316            false,
2317        )
2318    }
2319
2320    /// Commit a staged index lock after a successful working-tree update.
2321    pub(crate) fn finish_index_update(
2322        &self,
2323        lock: &mut crate::index::IndexLock,
2324        updated_workdir: bool,
2325    ) -> Result<()> {
2326        lock.commit_replace_index()?;
2327        let updated_workdir_arg = if updated_workdir { "1" } else { "0" };
2328        let _ = run_hook(self, "post-index-change", &[updated_workdir_arg, "0"], None);
2329        Ok(())
2330    }
2331
2332    /// Write a `.bitmap` sidecar for the largest pack in the primary object store.
2333    ///
2334    /// # Errors
2335    ///
2336    /// Returns [`crate::pack_bitmap::PackBitmapWriteError`] when no pack index exists or the
2337    /// pack is not closed under reachability from repository refs.
2338    pub fn write_largest_pack_bitmap(
2339        &self,
2340        options: &crate::pack_bitmap::PackBitmapWriteOptions,
2341        now: std::time::SystemTime,
2342    ) -> std::result::Result<std::path::PathBuf, crate::pack_bitmap::PackBitmapWriteError> {
2343        let path = self
2344            .odb
2345            .largest_pack_index_path()
2346            .map_err(|e| crate::pack_bitmap::PackBitmapWriteError::Object(e.to_string()))?
2347            .ok_or(crate::pack_bitmap::PackBitmapWriteError::InvalidPackPath(
2348                "no pack index in object store".to_owned(),
2349            ))?;
2350        crate::pack_bitmap::PackBitmapWriter::write(self, &path, options, now)
2351    }
2352
2353    /// Persist the index when the lock can be acquired (Git `repo_update_index_if_able`).
2354    ///
2355    /// Opportunistic writers such as `status` stat refresh call this instead of
2356    /// [`Repository::write_index`]: a held `.git/index.lock` or an unwritable repository
2357    /// directory must not fail the parent operation.
2358    ///
2359    /// Returns `Ok(true)` when the index was written, `Ok(false)` when persistence was skipped.
2360    ///
2361    /// # Errors
2362    ///
2363    /// Propagates non-recoverable failures (for example cache-tree verification or split-index
2364    /// constraints).
2365    pub(crate) fn try_write_index(&self, index: &mut Index) -> Result<bool> {
2366        self.try_write_index_at(&self.index_path_for_env()?, index)
2367    }
2368
2369    /// Like [`Repository::try_write_index`], but writes to an explicit index file path
2370    /// (for example the path selected by `GIT_INDEX_FILE`).
2371    pub(crate) fn try_write_index_at(&self, path: &Path, index: &mut Index) -> Result<bool> {
2372        if !self.environment().git_optional_locks {
2373            return Ok(false);
2374        }
2375        let cfg = self.config().ok();
2376        let mut lock = match crate::index::IndexLock::acquire(path, cfg.as_deref()) {
2377            Ok(lock) => lock,
2378            Err(e) if Self::opportunistic_index_write_skippable(&e) => return Ok(false),
2379            Err(e) => return Err(e),
2380        };
2381        let on_disk = crate::index::read_index_on_disk_identity(path, index.hash_algo)?;
2382        if !crate::index::opportunistic_index_write_still_current(index, &on_disk) {
2383            return Ok(false);
2384        }
2385        match self.write_index_with_lock(&mut lock, index, false) {
2386            Ok(()) => Ok(true),
2387            Err(e) if Self::opportunistic_index_write_skippable(&e) => Ok(false),
2388            Err(e) => Err(e),
2389        }
2390    }
2391
2392    fn opportunistic_index_write_skippable(err: &crate::error::Error) -> bool {
2393        use crate::error::Error;
2394        match err {
2395            Error::LockHeld { .. } => true,
2396            Error::Io(e) => matches!(
2397                e.kind(),
2398                std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::ReadOnlyFilesystem
2399            ),
2400            _ => false,
2401        }
2402    }
2403
2404    /// Write the index to the default path and pass explicit `post-index-change` hook flags.
2405    ///
2406    /// Parameters:
2407    /// - `index` is the in-memory index to serialize.
2408    /// - `updated_workdir` reports that the write is paired with a working-tree update.
2409    /// - `updated_skipworktree` reports that skip-worktree related index state changed.
2410    ///
2411    /// Returns `Ok(())` after the index is written and the hook has been attempted.
2412    ///
2413    /// Errors when the index cannot be finalized or written.
2414    pub(crate) fn write_index_with_post_index_change(
2415        &self,
2416        index: &mut Index,
2417        updated_workdir: bool,
2418        updated_skipworktree: bool,
2419    ) -> Result<()> {
2420        let path = self.index_path_for_env()?;
2421        self.write_index_at_with_post_index_change(
2422            &path,
2423            index,
2424            updated_workdir,
2425            updated_skipworktree,
2426        )
2427    }
2428
2429    /// Like [`Repository::write_index`], but writes to an explicit index file path.
2430    pub(crate) fn write_index_at(&self, path: &std::path::Path, index: &mut Index) -> Result<()> {
2431        self.write_index_at_with_post_index_change(path, index, false, false)
2432    }
2433
2434    /// Like [`Repository::write_index_at`], with explicit split-index emission control.
2435    pub(crate) fn write_index_at_with_split(
2436        &self,
2437        path: &std::path::Path,
2438        index: &mut Index,
2439        split: WriteSplitIndexRequest,
2440    ) -> Result<()> {
2441        self.write_index_at_split(path, index, split)
2442    }
2443
2444    /// Whether reading `index` under this repository's config would mark the cache changed solely to
2445    /// materialize a split index.
2446    ///
2447    /// Like [`Repository::write_index_at`], but passes explicit `post-index-change` hook flags.
2448    ///
2449    /// Parameters:
2450    /// - `path` is the destination index file.
2451    /// - `index` is the in-memory index to serialize.
2452    /// - `updated_workdir` reports that the write is paired with a working-tree update.
2453    /// - `updated_skipworktree` reports that skip-worktree related index state changed.
2454    ///
2455    /// Returns `Ok(())` after the index is written and the hook has been attempted.
2456    ///
2457    /// Errors when the index cannot be finalized or written.
2458    pub(crate) fn write_index_at_with_post_index_change(
2459        &self,
2460        path: &std::path::Path,
2461        index: &mut Index,
2462        updated_workdir: bool,
2463        updated_skipworktree: bool,
2464    ) -> Result<()> {
2465        self.write_index_at_split_with_post_index_change(
2466            path,
2467            index,
2468            WriteSplitIndexRequest::default(),
2469            updated_workdir,
2470            updated_skipworktree,
2471        )
2472    }
2473
2474    /// Write the index to `path`, optionally emitting a split index (shared base + `link` extension).
2475    pub(crate) fn write_index_at_split(
2476        &self,
2477        path: &std::path::Path,
2478        index: &mut Index,
2479        split: WriteSplitIndexRequest,
2480    ) -> Result<()> {
2481        self.write_index_at_split_with_post_index_change(path, index, split, false, false)
2482    }
2483
2484    /// Write the index to `path`, optionally emitting a split index, with explicit hook flags.
2485    ///
2486    /// Parameters:
2487    /// - `path` is the destination index file.
2488    /// - `index` is the in-memory index to serialize.
2489    /// - `split` controls whether a split index should be written.
2490    /// - `updated_workdir` reports that the write is paired with a working-tree update.
2491    /// - `updated_skipworktree` reports that skip-worktree related index state changed.
2492    ///
2493    /// Returns `Ok(())` after the index is written and the hook has been attempted.
2494    ///
2495    /// Errors when the index cannot be finalized or written.
2496    pub(crate) fn write_index_at_split_with_post_index_change(
2497        &self,
2498        path: &std::path::Path,
2499        index: &mut Index,
2500        split: WriteSplitIndexRequest,
2501        updated_workdir: bool,
2502        updated_skipworktree: bool,
2503    ) -> Result<()> {
2504        self.write_index_file(
2505            path,
2506            index,
2507            split,
2508            updated_workdir,
2509            updated_skipworktree,
2510            None,
2511        )
2512    }
2513
2514    /// Persist `index` through `lock` (from [`Self::lock_index`] or [`Self::begin_index_update`])
2515    /// and replace the on-disk index, with the same sparse/split-index handling and
2516    /// `post-index-change` hook as [`Self::write_index`].
2517    ///
2518    /// `updated_workdir` is reported to the hook: set it when the write follows a working-tree
2519    /// update made under the same lock.
2520    ///
2521    /// # Errors
2522    ///
2523    /// Propagates index finalization, serialization, and I/O failures. On error the lock is
2524    /// still held and released when dropped.
2525    pub(crate) fn write_index_with_lock(
2526        &self,
2527        lock: &mut crate::index::IndexLock,
2528        index: &mut Index,
2529        updated_workdir: bool,
2530    ) -> Result<()> {
2531        let path = lock.index_path.clone();
2532        self.write_index_file(
2533            &path,
2534            index,
2535            WriteSplitIndexRequest::default(),
2536            updated_workdir,
2537            false,
2538            Some(lock),
2539        )
2540    }
2541
2542    /// Lock the index selected by `GIT_INDEX_FILE` (or `$GIT_DIR/index`) without reading it.
2543    ///
2544    /// Commands that change the working tree together with the index take this lock before
2545    /// their first visible write, so a held lock refuses the whole command instead of failing
2546    /// after files were already rewritten. Persist with [`Self::write_index_with_lock`];
2547    /// dropping the lock without writing leaves the index as is.
2548    ///
2549    /// # Errors
2550    ///
2551    /// Returns [`Error::LockHeld`] when another process still holds the lock after the
2552    /// `core.filesRefLockTimeout` wait, or [`Error::Io`] when the lock cannot be created.
2553    pub(crate) fn lock_index(&self) -> Result<crate::index::IndexLock> {
2554        let path = self.index_path_for_env()?;
2555        let cfg = self.config()?;
2556        crate::index::IndexLock::acquire(&path, Some(cfg.as_ref()))
2557    }
2558
2559    /// Lock the index selected by `GIT_INDEX_FILE` (or `$GIT_DIR/index`), then load it.
2560    ///
2561    /// Read-modify-write commands must load the index only after the lock is held: an index
2562    /// read before locking can be replaced by a concurrent writer, and writing the stale copy
2563    /// back would silently drop that writer's update. Persist with
2564    /// [`Self::write_index_with_lock`]; dropping the lock without writing leaves the index as is.
2565    ///
2566    /// Returns the held lock and the index read under it.
2567    ///
2568    /// # Errors
2569    ///
2570    /// Returns [`Error::LockHeld`] when another process still holds the lock after the
2571    /// `core.filesRefLockTimeout` wait, and propagates index read and parse failures
2572    /// (including [`std::io::ErrorKind::NotFound`] when there is no index file yet).
2573    pub(crate) fn lock_and_load_index(&self) -> Result<(crate::index::IndexLock, Index)> {
2574        let lock = self.lock_index()?;
2575        let index = self.load_index_at(&lock.index_path)?;
2576        Ok((lock, index))
2577    }
2578
2579    /// Like [`Self::lock_and_load_index`], but keeps sparse-directory placeholders collapsed.
2580    pub(crate) fn lock_and_load_index_preserving_sparse(
2581        &self,
2582    ) -> Result<(crate::index::IndexLock, Index)> {
2583        let lock = self.lock_index()?;
2584        let index = self.load_index_at_preserving_sparse(
2585            &lock.index_path,
2586            crate::index::IndexParseContext::from_policy(self.thread_policy()),
2587        )?;
2588        Ok((lock, index))
2589    }
2590
2591    /// Shared pre-write housekeeping for [`Self::write_index_file`] and [`Self::flush_index_update`].
2592    fn prepare_index_for_disk_write(
2593        &self,
2594        path: &std::path::Path,
2595        index: &mut Index,
2596    ) -> Result<(bool, Arc<ConfigSet>)> {
2597        test_inject_index_write_fail()?;
2598        // The on-disk index format (entry OID width and trailing checksum) is
2599        // fixed by the repository's hash algorithm. Stamp it here so every
2600        // index written through the repository is consistent, regardless of how
2601        // the in-memory `Index` was constructed (e.g. a fresh `Index::new`).
2602        index.hash_algo = self.odb.hash_algo();
2603        self.finalize_sparse_index_if_needed(index)?;
2604        let prev_index_mtime = crate::index::index_file_mtime(path);
2605        let cfg = self.config()?;
2606        if let Some(work_tree) = self.work_tree.as_deref() {
2607            if !index.suppress_pre_write_index_housekeeping {
2608                crate::untracked_cache::ensure_untracked_cache_shell(
2609                    index,
2610                    work_tree,
2611                    cfg.as_ref(),
2612                );
2613                if !index.skip_racy_smudge_on_write {
2614                    crate::diff::smudge_racily_clean_entries(
2615                        &self.odb,
2616                        &self.git_dir,
2617                        index,
2618                        work_tree,
2619                        prev_index_mtime,
2620                        Some(cfg.as_ref()),
2621                        Some(self.caches().filters()),
2622                    );
2623                }
2624            }
2625        }
2626        index.clear_content_verified();
2627        let skip_hash = crate::index::index_skip_hash_for_write(Some(cfg.as_ref()));
2628        Ok((skip_hash, cfg))
2629    }
2630
2631    fn write_index_file(
2632        &self,
2633        path: &std::path::Path,
2634        index: &mut Index,
2635        split: WriteSplitIndexRequest,
2636        updated_workdir: bool,
2637        updated_skipworktree: bool,
2638        existing_lock: Option<&mut crate::index::IndexLock>,
2639    ) -> Result<()> {
2640        crate::trace::trace_span!("grit.index.write");
2641        let (skip_hash, cfg) = self.prepare_index_for_disk_write(path, index)?;
2642        write_index_file_split(
2643            path,
2644            &self.git_dir,
2645            index,
2646            cfg.as_ref(),
2647            split,
2648            skip_hash,
2649            self.thread_policy(),
2650            existing_lock,
2651            true,
2652        )
2653        .map_err(|err| map_index_write_error(&self.git_dir, path, err))?;
2654        // Git `write_locked_index`: `post-index-change` after a successful index write (t1800).
2655        let updated_workdir_arg = if updated_workdir { "1" } else { "0" };
2656        let updated_skipworktree_arg = if updated_skipworktree { "1" } else { "0" };
2657        let _ = run_hook(
2658            self,
2659            "post-index-change",
2660            &[updated_workdir_arg, updated_skipworktree_arg],
2661            None,
2662        );
2663        Ok(())
2664    }
2665
2666    fn finalize_sparse_index_if_needed(&self, index: &mut Index) -> Result<()> {
2667        let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
2668        let sparse_enabled = cfg
2669            .get("core.sparseCheckout")
2670            .map(|v| v == "true")
2671            .unwrap_or(false);
2672        if !sparse_enabled {
2673            index.sparse_directories = false;
2674            return Ok(());
2675        }
2676        let cone_cfg = cfg
2677            .get("core.sparseCheckoutCone")
2678            .and_then(|v| v.parse::<bool>().ok())
2679            .unwrap_or(true);
2680        let sparse_ix = cfg
2681            .get("index.sparse")
2682            .map(|v| v == "true")
2683            .unwrap_or(false);
2684        let patterns = read_sparse_checkout_patterns(&self.git_dir);
2685        let cone = effective_cone_mode_for_sparse_file(cone_cfg, &patterns);
2686        // Checkout writes the index before moving HEAD; a valid cache-tree root names the tree
2687        // the index already matches.
2688        let tree_oid = match index.cache_tree_root {
2689            Some(tree) => Some(tree),
2690            None => crate::state::resolve_head_for_repository(self)?.tree(&self.odb)?,
2691        };
2692        if let Some(t) = tree_oid {
2693            let protect = crate::path_validate::ProtectOptions::from_config(cfg.as_ref());
2694            index.try_collapse_sparse_directories(
2695                &self.odb, &t, &patterns, cone, sparse_ix, protect,
2696            )?;
2697        } else {
2698            index.sparse_directories = false;
2699        }
2700        Ok(())
2701    }
2702
2703    /// Relative path from the work tree root to the process current directory, `/`-separated.
2704    ///
2705    /// Used for `:(top)` / `:/` pathspec Bloom lookups. Returns `None` for bare repositories or
2706    /// when paths cannot be resolved; callers should treat `None` like an empty prefix.
2707    #[must_use]
2708    pub(crate) fn bloom_pathspec_cwd(&self) -> Option<String> {
2709        let wt = self.work_tree.as_ref()?;
2710        let cwd = self.environment.discovery_cwd();
2711        let wt = wt.canonicalize_simplified().ok()?;
2712        let cwd = cwd.canonicalize_simplified().ok()?;
2713        let rel = cwd.strip_prefix(&wt).ok()?;
2714        let s = rel.to_string_lossy().replace('\\', "/");
2715        let s = s.trim_start_matches('/').to_string();
2716        Some(s)
2717    }
2718
2719    /// Whether this is a bare repository (no working tree).
2720    #[must_use]
2721    pub fn is_bare(&self) -> bool {
2722        if let Ok(cfg) = self.config() {
2723            if let Some(Ok(bare)) = cfg.get_bool("core.bare") {
2724                return bare;
2725            }
2726        }
2727        self.work_tree.is_none()
2728    }
2729
2730    /// Ref prefix holding replace refs (`refs/replace/` unless `GIT_REPLACE_REF_BASE` says
2731    /// otherwise), or `None` when `GIT_NO_REPLACE_OBJECTS` or `core.useReplaceRefs=false`
2732    /// disables them.
2733    pub(crate) fn replace_ref_base_if_enabled(&self) -> Option<String> {
2734        if self.environment.git_no_replace_objects {
2735            return None;
2736        }
2737        let settings = self.cached_settings();
2738        settings
2739            .use_replace_refs
2740            .then_some(settings.replace_ref_base)
2741    }
2742
2743    /// Object id to use after applying `refs/replace` (when enabled).
2744    pub(crate) fn object_id_after_replace(
2745        &self,
2746        oid: &crate::objects::ObjectId,
2747    ) -> crate::objects::ObjectId {
2748        self.resolve_replace_redirect(oid).unwrap_or(*oid)
2749    }
2750
2751    /// Metadata for an object, following replace refs without reading the payload.
2752    pub(crate) fn read_info_replaced(
2753        &self,
2754        oid: &crate::objects::ObjectId,
2755    ) -> Result<crate::objects::ObjectInfo> {
2756        let effective = self.object_id_after_replace(oid);
2757        self.odb.read_info(&effective)
2758    }
2759
2760    /// Read an object, transparently following replace refs.
2761    ///
2762    /// If `refs/replace/<hex>` exists for the requested OID and
2763    /// `GIT_NO_REPLACE_OBJECTS` is **not** set, this reads the
2764    /// replacement object instead.  Otherwise it behaves identically
2765    /// to `self.odb.read(oid)`.
2766    pub(crate) fn read_replaced(
2767        &self,
2768        oid: &crate::objects::ObjectId,
2769    ) -> Result<crate::objects::Object> {
2770        let effective = self.object_id_after_replace(oid);
2771        self.odb.read(&effective)
2772    }
2773
2774    fn resolve_replace_redirect(
2775        &self,
2776        oid: &crate::objects::ObjectId,
2777    ) -> Option<crate::objects::ObjectId> {
2778        if self.environment.git_no_replace_objects {
2779            return None;
2780        }
2781        if let Ok(guard) = self.config_session.lock() {
2782            if guard.as_ref().is_some_and(|s| s.replace_lookup_inactive) {
2783                return None;
2784            }
2785        }
2786        let settings = self.cached_settings();
2787        if !settings.use_replace_refs {
2788            return None;
2789        }
2790        let replace_ref =
2791            self.common_git_dir
2792                .join(format!("{}{}", settings.replace_ref_base, oid.to_hex()));
2793        if !replace_ref.is_file() {
2794            return None;
2795        }
2796        let content = std::fs::read_to_string(&replace_ref).ok()?;
2797        let hex = content.trim();
2798        let replacement_oid = hex.parse::<crate::objects::ObjectId>().ok()?;
2799        if self.odb.read_info(&replacement_oid).is_ok() {
2800            Some(replacement_oid)
2801        } else {
2802            None
2803        }
2804    }
2805}
2806
2807/// If `GIT_TRACE_SETUP` is an absolute path, append `setup:` lines (Git test format).
2808///
2809/// Upstream tests grep `^setup: ` from the trace file; they do not use the timestamped
2810/// `trace.c:` prefix that full Git tracing adds.
2811pub(crate) fn trace_repo_setup_if_requested(repo: &Repository) -> std::io::Result<()> {
2812    let Some(path) = repo.environment.git_trace_setup.as_deref() else {
2813        return Ok(());
2814    };
2815    if path.is_empty() || path == "0" {
2816        return Ok(());
2817    }
2818    let trace_path = Path::new(path);
2819    if !trace_path.is_absolute() {
2820        return Ok(());
2821    }
2822
2823    let actual_cwd = repo.environment.discovery_cwd();
2824    let actual_cwd = actual_cwd
2825        .canonicalize_simplified()
2826        .unwrap_or_else(|_| actual_cwd.clone());
2827
2828    // After setup, Git's traced `cwd` is the worktree root when the process cwd started inside
2829    // the worktree, but stays at the real cwd when outside (t1510 nephew cases).
2830    let (trace_cwd, prefix) = if let Some(ref wt) = repo.work_tree {
2831        let wt_canon = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
2832        if actual_cwd.starts_with(&wt_canon) {
2833            let rel = actual_cwd
2834                .strip_prefix(&wt_canon)
2835                .map(|p| p.to_path_buf())
2836                .unwrap_or_default();
2837            let prefix = if rel.as_os_str().is_empty() {
2838                "(null)".to_owned()
2839            } else {
2840                let mut s = rel.to_string_lossy().replace('\\', "/");
2841                if !s.ends_with('/') {
2842                    s.push('/');
2843                }
2844                s
2845            };
2846            (wt_canon, prefix)
2847        } else {
2848            (actual_cwd.clone(), "(null)".to_owned())
2849        }
2850    } else {
2851        (actual_cwd.clone(), "(null)".to_owned())
2852    };
2853
2854    let git_dir_display =
2855        display_git_dir_for_setup_trace(repo, &trace_cwd, &actual_cwd, prefix.as_str());
2856    let common_display = display_common_dir_for_setup_trace(
2857        repo,
2858        &trace_cwd,
2859        &actual_cwd,
2860        prefix.as_str(),
2861        &git_dir_display,
2862    );
2863    let worktree_display = repo
2864        .work_tree
2865        .as_ref()
2866        .map(|p| {
2867            p.canonicalize_simplified()
2868                .unwrap_or_else(|_| lexical_normalize_path(p))
2869                .display()
2870                .to_string()
2871        })
2872        .unwrap_or_else(|| "(null)".to_owned());
2873
2874    let mut f = OpenOptions::new()
2875        .create(true)
2876        .append(true)
2877        .open(trace_path)?;
2878    writeln!(f, "setup: git_dir: {git_dir_display}")?;
2879    writeln!(f, "setup: git_common_dir: {common_display}")?;
2880    writeln!(f, "setup: worktree: {worktree_display}")?;
2881    writeln!(f, "setup: cwd: {}", trace_cwd.display())?;
2882    writeln!(f, "setup: prefix: {prefix}")?;
2883    Ok(())
2884}
2885
2886/// Collapse `.` / `..` in a path for display when `canonicalize()` fails (e.g. non-existent `..` segments).
2887fn lexical_normalize_path(path: &Path) -> PathBuf {
2888    let mut out = PathBuf::new();
2889    let mut absolute = false;
2890    for c in path.components() {
2891        match c {
2892            Component::Prefix(p) => {
2893                out.push(p.as_os_str());
2894            }
2895            Component::RootDir => {
2896                absolute = true;
2897                out.push(c.as_os_str());
2898            }
2899            Component::CurDir => {}
2900            Component::ParentDir => {
2901                if absolute {
2902                    let _ = out.pop();
2903                } else if !out.pop() {
2904                    out.push("..");
2905                }
2906            }
2907            Component::Normal(s) => out.push(s),
2908        }
2909    }
2910    if out.as_os_str().is_empty() {
2911        PathBuf::from(".")
2912    } else {
2913        out
2914    }
2915}
2916
2917/// Path from `base` to `target` using `..` segments when needed (matches Git setup traces).
2918fn path_relative_to(target: &Path, base: &Path) -> Option<PathBuf> {
2919    let t = target.canonicalize_simplified().ok()?;
2920    let b = base.canonicalize_simplified().ok()?;
2921    let tc: Vec<_> = t.components().collect();
2922    let bc: Vec<_> = b.components().collect();
2923    let mut i = 0usize;
2924    while i < tc.len() && i < bc.len() && tc[i] == bc[i] {
2925        i += 1;
2926    }
2927    let up = bc.len().saturating_sub(i);
2928    let mut out = PathBuf::new();
2929    for _ in 0..up {
2930        out.push("..");
2931    }
2932    for comp in &tc[i..] {
2933        out.push(comp.as_os_str());
2934    }
2935    Some(out)
2936}
2937
2938fn rel_path_for_setup_trace(target: &Path, trace_cwd: &Path) -> String {
2939    let t = target
2940        .canonicalize_simplified()
2941        .unwrap_or_else(|_| target.to_path_buf());
2942    let tc = trace_cwd
2943        .canonicalize_simplified()
2944        .unwrap_or_else(|_| trace_cwd.to_path_buf());
2945    if let Some(rel) = path_relative_to(&t, &tc) {
2946        let s = rel.to_string_lossy().replace('\\', "/");
2947        return if s.is_empty() || s == "." {
2948            ".".to_owned()
2949        } else {
2950            s
2951        };
2952    }
2953    t.display().to_string()
2954}
2955
2956fn trace_cwd_strictly_inside_git_parent(trace_cwd: &Path, git_dir: &Path) -> bool {
2957    let tc = trace_cwd
2958        .canonicalize_simplified()
2959        .unwrap_or_else(|_| trace_cwd.to_path_buf());
2960    let gd = git_dir
2961        .canonicalize_simplified()
2962        .unwrap_or_else(|_| git_dir.to_path_buf());
2963    let Some(parent) = gd.parent() else {
2964        return false;
2965    };
2966    let parent = parent.to_path_buf();
2967    if tc == parent {
2968        return false;
2969    }
2970    tc.starts_with(&parent) && tc != parent
2971}
2972
2973fn display_git_dir_for_setup_trace(
2974    repo: &Repository,
2975    trace_cwd: &Path,
2976    actual_cwd: &Path,
2977    setup_prefix: &str,
2978) -> String {
2979    let gd = repo
2980        .git_dir
2981        .canonicalize_simplified()
2982        .unwrap_or_else(|_| repo.git_dir.clone());
2983    let tc = trace_cwd
2984        .canonicalize_simplified()
2985        .unwrap_or_else(|_| trace_cwd.to_path_buf());
2986    let ac = actual_cwd
2987        .canonicalize_simplified()
2988        .unwrap_or_else(|_| actual_cwd.to_path_buf());
2989
2990    // Bare repo discovered without `GIT_DIR`: cwd inside the git directory (t1510 #16).
2991    // Trace uses `.` at the git-dir root and the absolute git-dir path from subdirectories.
2992    if repo.work_tree.is_none() && !repo.explicit_git_dir {
2993        if ac == gd {
2994            return ".".to_owned();
2995        }
2996        if ac.starts_with(&gd) && ac != gd {
2997            return gd.display().to_string();
2998        }
2999    }
3000
3001    // Non-bare repo with `core.worktree` while cwd is inside the git-dir (t1510 #20a).
3002    if !repo.explicit_git_dir {
3003        if let Some(wt) = &repo.work_tree {
3004            let wt = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3005            if ac.starts_with(&gd) && ac != wt {
3006                return gd.display().to_string();
3007            }
3008        }
3009    }
3010
3011    // `GIT_DIR` set: Git's `set_git_dir(gitdirenv, make_realpath)` keeps a relative
3012    // `gitdirenv` only when cwd is at the worktree root or outside the worktree; from a
3013    // subdirectory it realpath()s to an absolute path (see `setup.c` / t1510).
3014    if repo.explicit_git_dir {
3015        if repo.work_tree.is_none() {
3016            if let Some(raw) = repo.environment.git_dir.as_deref() {
3017                let p = Path::new(raw.trim());
3018                if p.is_absolute() {
3019                    return gd.display().to_string();
3020                }
3021                let joined = ac.join(p);
3022                if joined.is_file() {
3023                    return gd.display().to_string();
3024                }
3025                if let Some(rel) = path_relative_to(&gd, &tc) {
3026                    let s = rel.to_string_lossy().replace('\\', "/");
3027                    return if s.is_empty() || s == "." {
3028                        ".".to_owned()
3029                    } else {
3030                        s
3031                    };
3032                }
3033            }
3034            return gd.display().to_string();
3035        }
3036        if let Some(wt) = &repo.work_tree {
3037            let wt = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3038            let strictly_inside_wt = ac.starts_with(&wt) && ac != wt;
3039            if strictly_inside_wt {
3040                return gd.display().to_string();
3041            }
3042            if let Some(raw) = repo.environment.git_dir.as_deref() {
3043                let p = Path::new(raw.trim());
3044                if p.is_relative() {
3045                    let joined = ac.join(p);
3046                    if joined.is_file() {
3047                        // `GIT_DIR` points at a gitfile; trace shows the resolved git dir.
3048                        return gd.display().to_string();
3049                    }
3050                    if let Some(rel) = path_relative_to(&gd, &tc) {
3051                        let s = rel.to_string_lossy().replace('\\', "/");
3052                        return if s.is_empty() || s == "." {
3053                            ".".to_owned()
3054                        } else {
3055                            s
3056                        };
3057                    }
3058                }
3059                return gd.display().to_string();
3060            }
3061        }
3062        if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
3063            return rel_path_for_setup_trace(&gd, trace_cwd);
3064        }
3065        return gd.display().to_string();
3066    }
3067
3068    let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
3069        (Some(root), Some(wt)) if !repo.work_tree_from_env => {
3070            let r = root
3071                .canonicalize_simplified()
3072                .unwrap_or_else(|_| root.clone());
3073            let w = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3074            r != w
3075        }
3076        _ => false,
3077    };
3078
3079    if repo.work_tree_from_env {
3080        if !repo.discovery_via_gitfile {
3081            if setup_prefix == "(null)" {
3082                if let (Some(root), Some(wt)) = (&repo.discovery_root, &repo.work_tree) {
3083                    let r = root
3084                        .canonicalize_simplified()
3085                        .unwrap_or_else(|_| root.clone());
3086                    let w = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3087                    if r == w {
3088                        let dot_git = r.join(".git");
3089                        let dot_git = dot_git.canonicalize_simplified().unwrap_or(dot_git);
3090                        if gd == dot_git {
3091                            return ".git".to_owned();
3092                        }
3093                    }
3094                }
3095            }
3096            if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
3097                return rel_path_for_setup_trace(&gd, trace_cwd);
3098            }
3099        }
3100        return gd.display().to_string();
3101    }
3102
3103    if work_relocated {
3104        if let Some(wt) = &repo.work_tree {
3105            let wt = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3106            if ac == wt {
3107                return gd.display().to_string();
3108            }
3109            let inside_wt = ac.starts_with(&wt) && ac != wt;
3110            if inside_wt {
3111                if let Some(rel) = path_relative_to(&gd, &ac) {
3112                    let s = rel.to_string_lossy().replace('\\', "/");
3113                    return if s.is_empty() || s == "." {
3114                        ".".to_owned()
3115                    } else {
3116                        s
3117                    };
3118                }
3119            }
3120        }
3121    }
3122    if repo.work_tree.is_some() {
3123        if let Some(root) = &repo.discovery_root {
3124            let r = root
3125                .canonicalize_simplified()
3126                .unwrap_or_else(|_| root.clone());
3127            let dot_git = r.join(".git");
3128            let dot_git = dot_git.canonicalize_simplified().unwrap_or(dot_git);
3129            if gd == dot_git {
3130                return ".git".to_owned();
3131            }
3132        } else if let Some(wt) = &repo.work_tree {
3133            let wt = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3134            let dot_git = wt.join(".git");
3135            let dot_git = dot_git.canonicalize_simplified().unwrap_or(dot_git);
3136            if gd == dot_git {
3137                return ".git".to_owned();
3138            }
3139        }
3140    }
3141
3142    if repo.discovery_via_gitfile && !repo.explicit_git_dir {
3143        return gd.display().to_string();
3144    }
3145
3146    // Bare repo whose git-dir is `parent/.git`: at `parent` the trace shows `.git`; from a
3147    // subdirectory of `parent` that is still outside `.git`, Git uses the absolute git-dir (t1510
3148    // #16c sub/ case — not `../.git`).
3149    if repo.work_tree.is_none() && !repo.explicit_git_dir {
3150        if let Some(gp) = gd.parent() {
3151            let gp = gp
3152                .canonicalize_simplified()
3153                .unwrap_or_else(|_| gp.to_path_buf());
3154            let gdc = gd.canonicalize_simplified().unwrap_or_else(|_| gd.clone());
3155            if tc.starts_with(&gp) && tc != gp && !tc.starts_with(&gdc) {
3156                return gdc.display().to_string();
3157            }
3158            if tc == gp {
3159                return rel_path_for_setup_trace(&gd, trace_cwd);
3160            }
3161        }
3162    }
3163
3164    if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
3165        rel_path_for_setup_trace(&gd, trace_cwd)
3166    } else {
3167        gd.display().to_string()
3168    }
3169}
3170
3171fn display_common_dir_for_setup_trace(
3172    repo: &Repository,
3173    trace_cwd: &Path,
3174    actual_cwd: &Path,
3175    _setup_prefix: &str,
3176    git_dir_display: &str,
3177) -> String {
3178    let gd = repo
3179        .git_dir
3180        .canonicalize_simplified()
3181        .unwrap_or_else(|_| repo.git_dir.clone());
3182    let Some(common) = resolve_common_dir(&gd) else {
3183        return git_dir_display.to_owned();
3184    };
3185    let common = common.canonicalize_simplified().unwrap_or(common);
3186    if common == gd {
3187        return git_dir_display.to_owned();
3188    }
3189
3190    let ac = actual_cwd
3191        .canonicalize_simplified()
3192        .unwrap_or_else(|_| actual_cwd.to_path_buf());
3193    if repo.work_tree.is_none() && !repo.explicit_git_dir {
3194        if ac == common {
3195            return ".".to_owned();
3196        }
3197        if ac.starts_with(&common) && ac != common {
3198            return common.display().to_string();
3199        }
3200    }
3201
3202    let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
3203        (Some(root), Some(wt)) if !repo.work_tree_from_env => {
3204            let r = root
3205                .canonicalize_simplified()
3206                .unwrap_or_else(|_| root.clone());
3207            let w = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3208            r != w
3209        }
3210        _ => false,
3211    };
3212    if work_relocated {
3213        if let Some(wt) = &repo.work_tree {
3214            let wt = wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone());
3215            if ac == wt {
3216                return common.display().to_string();
3217            }
3218            let inside_wt = ac.starts_with(&wt) && ac != wt;
3219            if inside_wt {
3220                if let Some(rel) = path_relative_to(&common, &ac) {
3221                    let s = rel.to_string_lossy().replace('\\', "/");
3222                    return if s.is_empty() || s == "." {
3223                        ".".to_owned()
3224                    } else {
3225                        s
3226                    };
3227                }
3228            }
3229        }
3230    }
3231
3232    if repo.discovery_via_gitfile && !repo.explicit_git_dir {
3233        return common.display().to_string();
3234    }
3235
3236    if repo.work_tree.is_none() && !repo.explicit_git_dir {
3237        let tc = trace_cwd
3238            .canonicalize_simplified()
3239            .unwrap_or_else(|_| trace_cwd.to_path_buf());
3240        if let Some(cp) = common.parent() {
3241            let cp = cp
3242                .canonicalize_simplified()
3243                .unwrap_or_else(|_| cp.to_path_buf());
3244            let comc = common
3245                .canonicalize_simplified()
3246                .unwrap_or_else(|_| common.clone());
3247            if tc.starts_with(&cp) && tc != cp && !tc.starts_with(&comc) {
3248                return comc.display().to_string();
3249            }
3250            if tc == cp {
3251                return rel_path_for_setup_trace(&common, trace_cwd);
3252            }
3253        }
3254    }
3255
3256    if trace_cwd_strictly_inside_git_parent(trace_cwd, &common) {
3257        rel_path_for_setup_trace(&common, trace_cwd)
3258    } else {
3259        common.display().to_string()
3260    }
3261}
3262
3263/// Resolve the common git directory for linked worktrees.
3264fn replace_dir_is_non_empty(replace_dir: &Path) -> bool {
3265    let Ok(entries) = fs::read_dir(replace_dir) else {
3266        return false;
3267    };
3268    entries.filter_map(|e| e.ok()).any(|e| {
3269        let Ok(ft) = e.file_type() else {
3270            return false;
3271        };
3272        ft.is_file()
3273    })
3274}
3275
3276/// Resolve the common git directory for linked worktrees.
3277fn resolve_common_dir(git_dir: &Path) -> Option<PathBuf> {
3278    let commondir_file = git_dir.join("commondir");
3279    if !commondir_file.is_file() {
3280        return None;
3281    }
3282    let common_raw = fs::read_to_string(commondir_file).ok()?;
3283    let common_rel = common_raw.trim();
3284    if common_rel.is_empty() {
3285        return None;
3286    }
3287    let common_dir = if Path::new(common_rel).is_absolute() {
3288        PathBuf::from(common_rel)
3289    } else {
3290        git_dir.join(common_rel)
3291    };
3292    Some(common_dir.canonicalize_simplified().unwrap_or(common_dir))
3293}
3294
3295/// Directory holding `config` for early-config reads (`commondir` when present).
3296#[must_use]
3297pub fn common_git_dir_for_config(git_dir: &Path) -> PathBuf {
3298    resolve_common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf())
3299}
3300
3301/// True when `extensions.worktreeConfig` is enabled in the common `config`.
3302pub fn worktree_config_enabled(common_dir: &Path) -> bool {
3303    let path = common_dir.join("config");
3304    let Ok(content) = crate::repo_caches::read_local_config_file(&path) else {
3305        return false;
3306    };
3307    worktree_config_enabled_for_text(Some(content.as_str()))
3308}
3309
3310/// Like [`worktree_config_enabled`] when `config` body is already in memory.
3311pub fn worktree_config_enabled_for_text(content: Option<&str>) -> bool {
3312    let Some(content) = content else {
3313        return false;
3314    };
3315    let mut in_extensions = false;
3316    for raw_line in content.lines() {
3317        let mut line = raw_line.trim();
3318        if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
3319            continue;
3320        }
3321        if line.starts_with('[') {
3322            let Some(end_idx) = line.find(']') else {
3323                continue;
3324            };
3325            let section = line[1..end_idx].trim();
3326            let section_name = section
3327                .split_whitespace()
3328                .next()
3329                .unwrap_or_default()
3330                .to_ascii_lowercase();
3331            in_extensions = section_name == "extensions";
3332            let remainder = line[end_idx + 1..].trim();
3333            if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
3334                continue;
3335            }
3336            line = remainder;
3337        }
3338        if in_extensions {
3339            let Some((key, value)) = line.split_once('=') else {
3340                continue;
3341            };
3342            if key.trim().eq_ignore_ascii_case("worktreeconfig") {
3343                let v = value.trim();
3344                return v.eq_ignore_ascii_case("true")
3345                    || v.eq_ignore_ascii_case("yes")
3346                    || v.eq_ignore_ascii_case("on")
3347                    || v == "1";
3348            }
3349        }
3350    }
3351    false
3352}
3353
3354fn open_or_create_config_file(path: &Path, scope: ConfigScope) -> Result<ConfigFile> {
3355    match ConfigFile::from_path(path, scope)? {
3356        Some(f) => Ok(f),
3357        None => {
3358            if let Some(parent) = path.parent() {
3359                fs::create_dir_all(parent).map_err(Error::Io)?;
3360            }
3361            ConfigFile::parse(path, "", scope)
3362        }
3363    }
3364}
3365
3366fn config_file_bool_true(cfg: &ConfigFile, key: &str) -> bool {
3367    cfg.get(key).is_some_and(|v| {
3368        matches!(
3369            v.trim().to_ascii_lowercase().as_str(),
3370            "true" | "yes" | "on" | "1"
3371        )
3372    })
3373}
3374
3375/// Enable per-worktree configuration (`extensions.worktreeConfig`) and create
3376/// `config.worktree`, matching Git's `init_worktree_config` in `worktree.c`.
3377///
3378/// When `core.bare` is true or `core.worktree` is set in the common config,
3379/// those keys are moved into `config.worktree` so linked worktrees keep working.
3380///
3381/// # Errors
3382///
3383/// Returns [`Error::Io`] or [`Error::Config`] if config files cannot be read or written.
3384pub fn init_worktree_config(git_dir: &Path) -> Result<()> {
3385    let common_dir = common_git_dir_for_config(git_dir);
3386    let common_config_path = common_dir.join("config");
3387    let worktree_config_path = git_dir.join("config.worktree");
3388
3389    if worktree_config_enabled(&common_dir) {
3390        if !worktree_config_path.exists() {
3391            if let Some(parent) = worktree_config_path.parent() {
3392                fs::create_dir_all(parent).map_err(Error::Io)?;
3393            }
3394            fs::write(&worktree_config_path, "").map_err(Error::Io)?;
3395        }
3396        return Ok(());
3397    }
3398
3399    let mut common_cfg = open_or_create_config_file(&common_config_path, ConfigScope::Local)?;
3400    common_cfg.set("extensions.worktreeConfig", "true")?;
3401
3402    let mut wt_cfg = open_or_create_config_file(&worktree_config_path, ConfigScope::Worktree)?;
3403
3404    if config_file_bool_true(&common_cfg, "core.bare") {
3405        wt_cfg.set("core.bare", "true")?;
3406        common_cfg.unset("core.bare")?;
3407    }
3408    if let Some(worktree) = common_cfg.get("core.worktree") {
3409        wt_cfg.set("core.worktree", &worktree)?;
3410        common_cfg.unset("core.worktree")?;
3411    }
3412
3413    common_cfg.write()?;
3414    wt_cfg.write()?;
3415    Ok(())
3416}
3417
3418/// If the common `config` declares a repository format newer than Git's
3419/// `GIT_REPO_VERSION_READ`, return the human message Git prints for
3420/// `discover_git_directory_reason` / t1309.
3421pub fn early_config_ignore_repo_reason(common_dir: &Path) -> Option<String> {
3422    let path = common_dir.join("config");
3423    let content = crate::repo_caches::read_local_config_file(&path).ok()?;
3424    early_config_ignore_repo_reason_for_text(Some(content.as_str()))
3425}
3426
3427/// Like [`early_config_ignore_repo_reason`] when `config` body is already in memory.
3428pub fn early_config_ignore_repo_reason_for_text(content: Option<&str>) -> Option<String> {
3429    const GIT_REPO_VERSION_READ: u32 = 1;
3430    let content = content?;
3431    let mut version = 0u32;
3432    let mut in_core = false;
3433    for raw_line in content.lines() {
3434        let mut line = raw_line.trim();
3435        if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
3436            continue;
3437        }
3438        if line.starts_with('[') {
3439            let Some(end_idx) = line.find(']') else {
3440                continue;
3441            };
3442            let section = line[1..end_idx].trim();
3443            let section_name = section
3444                .split_whitespace()
3445                .next()
3446                .unwrap_or_default()
3447                .to_ascii_lowercase();
3448            in_core = section_name == "core";
3449            let remainder = line[end_idx + 1..].trim();
3450            if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
3451                continue;
3452            }
3453            line = remainder;
3454        }
3455        if in_core {
3456            if let Some((key, value)) = line.split_once('=') {
3457                if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
3458                    if let Ok(v) = value.trim().parse::<u32>() {
3459                        version = v;
3460                    }
3461                }
3462            }
3463        }
3464    }
3465    if version > GIT_REPO_VERSION_READ {
3466        Some(format!(
3467            "Expected git repo version <= {GIT_REPO_VERSION_READ}, found {version}"
3468        ))
3469    } else {
3470        None
3471    }
3472}
3473
3474fn path_for_ceiling_compare(path: &Path) -> String {
3475    let path = path.to_string_lossy();
3476    #[cfg(windows)]
3477    {
3478        path.replace('\\', "/")
3479    }
3480    #[cfg(not(windows))]
3481    {
3482        path.into_owned()
3483    }
3484}
3485
3486fn offset_1st_component(path: &str) -> usize {
3487    if path.starts_with('/') {
3488        1
3489    } else {
3490        0
3491    }
3492}
3493
3494/// Git `longest_ancestor_length`: longest strict ancestor prefix among ceilings.
3495fn longest_ancestor_length(path: &str, ceilings: &[String]) -> Option<usize> {
3496    if path == "/" {
3497        return None;
3498    }
3499    let mut max_len: Option<usize> = None;
3500    for ceil in ceilings {
3501        let mut len = ceil.len();
3502        while len > 0 && ceil.as_bytes().get(len - 1) == Some(&b'/') {
3503            len -= 1;
3504        }
3505        if len == 0 {
3506            continue;
3507        }
3508        if path.len() <= len + 1 {
3509            continue;
3510        }
3511        if !path.starts_with(&ceil[..len]) {
3512            continue;
3513        }
3514        if path.as_bytes().get(len) != Some(&b'/') {
3515            continue;
3516        }
3517        if path.as_bytes().get(len + 1).is_none() {
3518            continue;
3519        }
3520        max_len = Some(max_len.map_or(len, |m| m.max(len)));
3521    }
3522    max_len
3523}
3524
3525/// Path to the repository-local config file (`.git/config`, or `commondir/config` for a linked
3526/// worktree admin directory that has no separate `config` file).
3527///
3528/// Used when writing `git config --local` semantics from library code.
3529pub fn local_config_write_path(git_dir: &Path) -> PathBuf {
3530    if let Some(path) = repository_config_path(git_dir) {
3531        return path;
3532    }
3533    if let Some(common) = resolve_common_dir(git_dir) {
3534        return common.join("config");
3535    }
3536    git_dir.join("config")
3537}
3538
3539/// Determine the config file path for a repository or linked worktree.
3540fn repository_config_path(git_dir: &Path) -> Option<PathBuf> {
3541    repository_config_path_for_caches(git_dir)
3542}
3543
3544/// Same as [`repository_config_path`] for [`crate::repo_caches::RepoCaches`].
3545pub(crate) fn repository_config_path_for_caches(git_dir: &Path) -> Option<PathBuf> {
3546    let local = git_dir.join("config");
3547    if local.is_file() {
3548        return Some(local);
3549    }
3550    let commondir = git_dir.join("commondir");
3551    if !commondir.is_file() {
3552        return None;
3553    }
3554    let common = resolve_common_dir(git_dir)?;
3555    let shared = common.join("config");
3556    if shared.exists() {
3557        Some(shared)
3558    } else {
3559        None
3560    }
3561}
3562
3563/// Validate core repository format/version compatibility.
3564///
3565/// Supports repository format versions 0 and 1, with extension handling that
3566/// matches Git's compatibility expectations in upstream repo-version tests.
3567/// Public wrapper for validate_repository_format.
3568pub fn validate_repo_format(git_dir: &Path) -> Result<()> {
3569    validate_repository_format(git_dir)
3570}
3571
3572/// Validate parsed repository format (version and extensions) before using the repo.
3573pub(crate) fn validate_repository_format_parsed(parsed: &RepositoryFormat) -> Result<()> {
3574    if parsed.repo_version > 1 {
3575        return Err(Error::UnsupportedRepositoryFormatVersion(
3576            parsed.repo_version,
3577        ));
3578    }
3579
3580    if let Some(raw) = parsed.ref_storage.as_deref() {
3581        crate::ref_storage::RefStorageFormat::validate_config_extension(raw)?;
3582    }
3583
3584    if let Some(msg) = parsed.format_error_message() {
3585        return Err(Error::Message(msg));
3586    }
3587
3588    Ok(())
3589}
3590
3591fn validate_repository_format(git_dir: &Path) -> Result<()> {
3592    let Some(config_path) = repository_config_path(git_dir) else {
3593        return Ok(());
3594    };
3595    let content = crate::repo_caches::read_local_config_file(&config_path)?;
3596    let parsed = parse_repository_format(&content, &config_path)?;
3597    validate_repository_format_parsed(&parsed)
3598}
3599
3600/// The result of parsing `core.repositoryformatversion` and `extensions.*` from a
3601/// repository's `config` file, using Git-compatible format parsing.
3602#[derive(Clone)]
3603pub(crate) struct RepositoryFormat {
3604    /// Declared `core.repositoryformatversion` (defaults to 0; invalid values ignored).
3605    pub(crate) repo_version: u32,
3606    /// All extension keys (lowercased) declared under `[extensions]`.
3607    pub(crate) extensions: BTreeSet<String>,
3608    /// Raw value of `extensions.refstorage`, if present.
3609    pub(crate) ref_storage: Option<String>,
3610    /// Raw value of `extensions.objectformat`, if present.
3611    pub(crate) object_format: Option<String>,
3612}
3613
3614/// Parse repository format from repository-local config (no global/system config).
3615///
3616/// # Errors
3617///
3618/// Returns [`Error::Io`] or [`Error::Config`] when config cannot be read or parsed.
3619pub(crate) fn read_repository_format_from_git_dir(git_dir: &Path) -> Result<RepositoryFormat> {
3620    let Some(config_path) = repository_config_path(git_dir) else {
3621        return Ok(RepositoryFormat {
3622            repo_version: 0,
3623            extensions: BTreeSet::new(),
3624            ref_storage: None,
3625            object_format: None,
3626        });
3627    };
3628    let content = crate::repo_caches::read_local_config_file(&config_path)?;
3629    parse_repository_format(&content, &config_path)
3630}
3631
3632impl RepositoryFormat {
3633    /// Object hash algorithm declared by `extensions.objectformat`, SHA-1 when absent or
3634    /// unrecognized (the same fallback as [`crate::odb::hash_algo_for_git_dir`]).
3635    pub(crate) fn hash_algo(&self) -> crate::objects::HashAlgo {
3636        self.object_format
3637            .as_deref()
3638            .and_then(crate::objects::HashAlgo::from_name)
3639            .unwrap_or(crate::objects::HashAlgo::Sha1)
3640    }
3641
3642    /// Build git's `verify_repository_format` warning/error message for unsupported
3643    /// extension declarations, or `None` if the extensions are all acceptable.
3644    ///
3645    /// This does not cover the `core.repositoryformatversion > 1` case; callers that
3646    /// need the version message handle it separately via
3647    /// [`repository_format_warning`].
3648    fn format_error_message(&self) -> Option<String> {
3649        // Mirror git/setup.c `check_repo_format` / `verify_repository_format`. Extensions
3650        // split into:
3651        //   * v0-compatible (`handle_extension_v0`): respected even in a v0 repository.
3652        //   * v1-only (`handle_extension`): legal only when `core.repositoryformatversion >= 1`.
3653        // A v0 repository that declares any v1-only extension is rejected (t0001 #60, #62);
3654        // an unknown extension is rejected only in a v1 repository.
3655        let mut v1_only_found: Vec<&str> = Vec::new();
3656        let mut unknown_found: Vec<&str> = Vec::new();
3657        for extension in &self.extensions {
3658            match extension.as_str() {
3659                // v0-compatible extensions — always allowed.
3660                "noop" | "preciousobjects" | "partialclone" | "worktreeconfig" => {}
3661                // v1-only extensions — only valid with repository format version >= 1.
3662                "noop-v1"
3663                | "objectformat"
3664                | "compatobjectformat"
3665                | "refstorage"
3666                | "relativeworktrees"
3667                | "submodulepathconfig" => {
3668                    if self.repo_version == 0 {
3669                        v1_only_found.push(extension);
3670                    }
3671                }
3672                // Unknown extension — rejected only in a v1 repository.
3673                _ => {
3674                    if self.repo_version >= 1 {
3675                        unknown_found.push(extension);
3676                    }
3677                }
3678            }
3679        }
3680
3681        if !unknown_found.is_empty() {
3682            let mut msg = if unknown_found.len() == 1 {
3683                "unknown repository extension found:".to_owned()
3684            } else {
3685                "unknown repository extensions found:".to_owned()
3686            };
3687            for ext in &unknown_found {
3688                msg.push_str(&format!("\n\t{ext}"));
3689            }
3690            return Some(msg);
3691        }
3692
3693        if !v1_only_found.is_empty() {
3694            let mut msg = if v1_only_found.len() == 1 {
3695                "repo version is 0, but v1-only extension found:".to_owned()
3696            } else {
3697                "repo version is 0, but v1-only extensions found:".to_owned()
3698            };
3699            for ext in &v1_only_found {
3700                msg.push_str(&format!("\n\t{ext}"));
3701            }
3702            return Some(msg);
3703        }
3704
3705        None
3706    }
3707}
3708
3709/// Parse `core.repositoryformatversion` and `[extensions]` entries out of raw
3710/// `config` file contents.
3711///
3712/// # Errors
3713///
3714/// Returns [`Error::Config`] if a section header is malformed (no closing `]`).
3715pub(crate) fn parse_repository_format(
3716    content: &str,
3717    config_path: &Path,
3718) -> Result<RepositoryFormat> {
3719    let mut in_core = false;
3720    let mut in_extensions = false;
3721    let mut repo_version = 0u32;
3722    let mut extensions = BTreeSet::new();
3723    let mut ref_storage: Option<String> = None;
3724    let mut object_format: Option<String> = None;
3725
3726    for raw_line in content.lines() {
3727        let mut line = raw_line.trim();
3728        if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
3729            continue;
3730        }
3731
3732        if line.starts_with('[') {
3733            let Some(end_idx) = line.find(']') else {
3734                return Err(Error::Config(
3735                    format!("invalid config in {}", config_path.display()).into(),
3736                ));
3737            };
3738
3739            let section = line[1..end_idx].trim();
3740            let section_name = section
3741                .split_whitespace()
3742                .next()
3743                .unwrap_or_default()
3744                .to_ascii_lowercase();
3745            in_core = section_name == "core";
3746            in_extensions = section_name == "extensions";
3747
3748            let remainder = line[end_idx + 1..].trim();
3749            if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
3750                continue;
3751            }
3752            line = remainder;
3753        }
3754
3755        if in_core {
3756            if let Some((key, value)) = line.split_once('=') {
3757                if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
3758                    // Match Git's `read_repository_format`: bad values are ignored (version stays 0).
3759                    if let Ok(v) = value.trim().parse::<u32>() {
3760                        repo_version = v;
3761                    }
3762                }
3763            }
3764        }
3765
3766        if in_extensions {
3767            let (key, value) = if let Some((key, value)) = line.split_once('=') {
3768                (key.trim(), Some(value.trim()))
3769            } else {
3770                (line, None)
3771            };
3772            if key.eq_ignore_ascii_case("refstorage") {
3773                ref_storage = value.map(str::to_owned);
3774            }
3775            if key.eq_ignore_ascii_case("objectformat") {
3776                object_format = value.map(str::to_owned);
3777            }
3778            if !key.is_empty() {
3779                extensions.insert(key.to_ascii_lowercase());
3780            }
3781        }
3782    }
3783
3784    Ok(RepositoryFormat {
3785        repo_version,
3786        extensions,
3787        ref_storage,
3788        object_format,
3789    })
3790}
3791
3792/// Return the warning message git would print for a repository whose `config`
3793/// declares an unsupported format, or `None` if the format is acceptable.
3794///
3795/// This matches Git's repository-format verification behavior:
3796/// commands that run with `RUN_SETUP_GENTLY` (e.g. `git config`) emit this text as a
3797/// `warning:` and then behave as if no repository were present, which makes the command
3798/// fail with a non-zero exit. The returned string is the bare message without the
3799/// `warning: ` prefix.
3800///
3801/// `git_dir` is the resolved git directory; its `config` (or the common-dir `config` for
3802/// linked worktrees) is parsed. A missing config yields `None` (git treats it as ok).
3803///
3804/// # Errors
3805///
3806/// Returns [`Error::Io`] if the config file exists but cannot be read, or
3807/// [`Error::Config`] if a section header is malformed.
3808pub fn repository_format_warning(git_dir: &Path) -> Result<Option<String>> {
3809    const GIT_REPO_VERSION_READ: u32 = 1;
3810    let Some(config_path) = repository_config_path(git_dir) else {
3811        return Ok(None);
3812    };
3813    let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
3814    let parsed = parse_repository_format(&content, &config_path)?;
3815
3816    if parsed.repo_version > GIT_REPO_VERSION_READ {
3817        return Ok(Some(format!(
3818            "Expected git repo version <= {GIT_REPO_VERSION_READ}, found {}",
3819            parsed.repo_version
3820        )));
3821    }
3822
3823    Ok(parsed.format_error_message())
3824}
3825
3826/// Try to open a repository rooted exactly at `dir`.
3827///
3828/// Returns `Ok(None)` when `dir` is not a repository root (the caller should
3829/// walk up); returns `Err` on a structural problem.
3830/// Result of probing a single directory during [`Repository::discover`].
3831struct DiscoveredAt {
3832    repo: Repository,
3833    /// When discovery used a `.git` gitfile, the path to that file (for ownership checks).
3834    gitfile: Option<PathBuf>,
3835}
3836
3837fn try_open_at(options: &RepositoryOptions, dir: &Path) -> Result<Option<DiscoveredAt>> {
3838    let env = &options.environment;
3839    let dot_git = dir.join(".git");
3840
3841    // Check for special file types (FIFO, socket, etc.) — reject them
3842    // instead of walking up to a parent repository.
3843    #[cfg(unix)]
3844    {
3845        use std::os::unix::fs::FileTypeExt;
3846        let is_special = |ft: fs::FileType| {
3847            ft.is_fifo() || ft.is_socket() || ft.is_block_device() || ft.is_char_device()
3848        };
3849        if let Ok(meta) = fs::symlink_metadata(&dot_git) {
3850            let ft = meta.file_type();
3851            let target_special = ft.is_symlink()
3852                && fs::metadata(&dot_git).is_ok_and(|target| is_special(target.file_type()));
3853            if is_special(ft) || target_special {
3854                return Err(Error::NotARepository(
3855                    NotARepositoryReason::GitfileNotRegularFile { path: dot_git },
3856                ));
3857            }
3858        }
3859    }
3860
3861    if dot_git.is_file() {
3862        // gitfile indirection: file contains "gitdir: <path>"
3863        let git_dir = resolve_gitfile(&dot_git)?;
3864        let mut opts = options.clone();
3865        opts.validate_repository_format = false;
3866        opts.work_tree = Some(dir.to_path_buf());
3867        let mut repo = Repository::open_internal(&opts, &git_dir)?;
3868        // Linked worktree: `core.worktree` in the common config may point at another directory
3869        // (t1501). When the process cwd is not inside that configured tree, Git uses the
3870        // discovery directory as the work tree (commondir overrides for ops under the real tree).
3871        if resolve_common_dir(&git_dir).is_some() {
3872            let cwd = env.discovery_cwd();
3873            if repo.work_tree.is_some() && !is_inside_work_tree(&repo, &cwd) {
3874                let root = if dir.is_absolute() {
3875                    dir.to_path_buf()
3876                } else {
3877                    cwd.join(dir)
3878                };
3879                repo.work_tree = Some(root.canonicalize_simplified().unwrap_or(root));
3880            }
3881        }
3882        let root = if dir.is_absolute() {
3883            dir.to_path_buf()
3884        } else {
3885            env.discovery_cwd().join(dir)
3886        };
3887        repo.discovery_root = Some(root.canonicalize_simplified().unwrap_or(root));
3888        repo.discovery_via_gitfile = true;
3889        return Ok(Some(DiscoveredAt {
3890            repo,
3891            gitfile: Some(dot_git.clone()),
3892        }));
3893    }
3894
3895    if dot_git.is_dir() {
3896        // If .git is a symlink to a directory, resolve the symlink target
3897        // for validation but keep the original .git path for user-facing output
3898        // (matches real git behavior: `rev-parse --git-dir` shows `.git`).
3899        let open_path = if dot_git.is_symlink() {
3900            // Resolve the symlink target for validation
3901            dot_git.read_link().unwrap_or_else(|_| dot_git.clone())
3902        } else {
3903            dot_git.clone()
3904        };
3905        // Try to open; if the directory is empty or invalid, continue
3906        // walking up (e.g. an empty .git/ directory should be ignored).
3907        let mut skip_opts = options.clone();
3908        skip_opts.validate_repository_format = false;
3909        skip_opts.work_tree = Some(dir.to_path_buf());
3910        match Repository::open_internal(&skip_opts, &open_path) {
3911            Ok(mut repo) => {
3912                // Restore the original path so rev-parse shows .git not the
3913                // resolved symlink target.
3914                if dot_git.is_symlink() {
3915                    let abs_dot_git = if dot_git.is_absolute() {
3916                        dot_git
3917                    } else {
3918                        dir.join(".git")
3919                    };
3920                    repo.git_dir = abs_dot_git;
3921                }
3922                let root = if dir.is_absolute() {
3923                    dir.to_path_buf()
3924                } else {
3925                    env.discovery_cwd().join(dir)
3926                };
3927                repo.discovery_root = Some(root.canonicalize_simplified().unwrap_or(root));
3928                repo.discovery_via_gitfile = false;
3929                return Ok(Some(DiscoveredAt {
3930                    repo,
3931                    gitfile: None,
3932                }));
3933            }
3934            Err(Error::NotARepository(_)) | Err(Error::Config(_)) => return Ok(None),
3935            Err(e) => return Err(e),
3936        }
3937    }
3938
3939    // Linked-worktree gitdir/admin directories contain HEAD and commondir,
3940    // and can be opened as repositories even without a local objects/ dir.
3941    if dir.join("HEAD").is_file() && dir.join("commondir").is_file() {
3942        maybe_trace_implicit_bare_repository(dir, env);
3943        let repo = Repository::open_with_work_tree(options, dir, None)?;
3944        return Ok(Some(DiscoveredAt {
3945            repo,
3946            gitfile: None,
3947        }));
3948    }
3949
3950    // Check if `dir` itself is a bare repo (has objects/ and HEAD directly)
3951    if dir.join("objects").is_dir() && dir.join("HEAD").is_file() {
3952        maybe_trace_implicit_bare_repository(dir, env);
3953        // Check safe.bareRepository policy before opening bare repos.
3954        // When set to "explicit", implicit bare repo discovery is forbidden
3955        // unless GIT_DIR was set (handled earlier in discover()).
3956        if !is_inside_dot_git(dir) {
3957            if let Ok(cfg) = crate::config::ConfigSet::load(env, None, true) {
3958                if let Some(val) = cfg.get("safe.bareRepository") {
3959                    if val.eq_ignore_ascii_case("explicit") {
3960                        return Err(Error::ForbiddenBareRepository(path_for_error_message(dir)));
3961                    }
3962                }
3963            }
3964        }
3965        let repo = Repository::open_with_work_tree(options, dir, None)?;
3966        return Ok(Some(DiscoveredAt {
3967            repo,
3968            gitfile: None,
3969        }));
3970    }
3971
3972    Ok(None)
3973}
3974
3975fn is_inside_dot_git(path: &Path) -> bool {
3976    path.components().any(|c| c.as_os_str() == ".git")
3977}
3978
3979fn maybe_trace_implicit_bare_repository(dir: &Path, environment: &Environment) {
3980    let Some(path) = environment
3981        .git_trace2_perf
3982        .as_deref()
3983        .filter(|p| !p.is_empty())
3984    else {
3985        return;
3986    };
3987
3988    if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
3989        let _ = writeln!(file, "setup: implicit-bare-repository:{}", dir.display());
3990    }
3991}
3992
3993/// Collect effective `safe.directory` values from protected config (system/global/command),
3994/// applying empty-value resets like Git.
3995fn safe_directory_effective_values(git_dir: &Path, env: &Environment) -> Vec<String> {
3996    let cfg = crate::config::ConfigSet::load(env, Some(git_dir), true)
3997        .unwrap_or_else(|_| crate::config::ConfigSet::new());
3998    let mut values: Vec<String> = Vec::new();
3999    for e in cfg.entries() {
4000        if e.key == "safe.directory"
4001            && e.scope != crate::config::ConfigScope::Local
4002            && e.scope != crate::config::ConfigScope::Worktree
4003        {
4004            values.push(e.value.clone().unwrap_or_else(|| "true".to_owned()));
4005        }
4006    }
4007    let mut effective: Vec<String> = Vec::new();
4008    for v in values {
4009        if v.is_empty() {
4010            effective.clear();
4011        } else {
4012            effective.push(v);
4013        }
4014    }
4015    effective
4016}
4017
4018fn ensure_safe_directory_allows(
4019    git_dir: &Path,
4020    checked: &Path,
4021    environment: &Environment,
4022) -> Result<()> {
4023    let effective = safe_directory_effective_values(git_dir, environment);
4024    let checked_s = checked.to_string_lossy().to_string();
4025    let _ = environment.grit_debug_safe_dir;
4026    if effective
4027        .iter()
4028        .any(|v| safe_directory_matches(v, &checked_s, &environment.discovery_cwd()))
4029    {
4030        return Ok(());
4031    }
4032    Err(Error::DubiousOwnership(path_for_error_message(checked)))
4033}
4034
4035#[cfg(unix)]
4036fn path_lstat_uid(path: &Path) -> std::io::Result<u32> {
4037    use std::os::unix::fs::MetadataExt;
4038    let meta = fs::symlink_metadata(path)?;
4039    Ok(meta.uid())
4040}
4041
4042/// Match Git's `ensure_valid_ownership`: check gitfile, worktree, and gitdir ownership,
4043/// then `safe.directory` when any path is not owned by the effective user.
4044#[cfg(unix)]
4045fn ensure_valid_ownership(
4046    environment: &Environment,
4047    gitfile: Option<&Path>,
4048    worktree: Option<&Path>,
4049    gitdir: &Path,
4050) -> Result<()> {
4051    const ROOT_UID: u32 = 0;
4052    let sudo_euid = environment
4053        .sudo_uid
4054        .as_deref()
4055        .and_then(|s| s.parse::<u32>().ok());
4056
4057    fn owned_by_effective_user(path: &Path, sudo_euid: Option<u32>) -> std::io::Result<bool> {
4058        let st_uid = path_lstat_uid(path)?;
4059        let mut euid = nix::unistd::geteuid().as_raw();
4060        if euid == ROOT_UID {
4061            if st_uid == ROOT_UID {
4062                return Ok(true);
4063            }
4064            if let Some(sudo_uid) = sudo_euid {
4065                euid = sudo_uid;
4066            }
4067        }
4068        Ok(st_uid == euid)
4069    }
4070
4071    let assume_different = environment.test_assume_different_owner();
4072    if !assume_different {
4073        let gitfile_ok = gitfile
4074            .map(|p| owned_by_effective_user(p, sudo_euid))
4075            .transpose()?
4076            .unwrap_or(true);
4077        // Git may use a `GIT_WORK_TREE` that does not exist yet (t1510); skip ownership when
4078        // the path is absent instead of failing discovery with ENOENT.
4079        let wt_ok = match worktree {
4080            None => true,
4081            Some(wt) => match owned_by_effective_user(wt, sudo_euid) {
4082                Ok(ok) => ok,
4083                Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
4084                Err(e) => return Err(Error::Io(e)),
4085            },
4086        };
4087        let gd_ok = owned_by_effective_user(gitdir, sudo_euid)?;
4088        if gitfile_ok && wt_ok && gd_ok {
4089            return Ok(());
4090        }
4091    }
4092
4093    let data_path = if let Some(wt) = worktree {
4094        wt.canonicalize_simplified()
4095            .unwrap_or_else(|_| wt.to_path_buf())
4096    } else {
4097        gitdir
4098            .canonicalize_simplified()
4099            .unwrap_or_else(|_| gitdir.to_path_buf())
4100    };
4101    ensure_safe_directory_allows(gitdir, &data_path, environment)
4102}
4103
4104#[cfg(not(unix))]
4105fn ensure_valid_ownership(
4106    _environment: &Environment,
4107    _gitfile: Option<&Path>,
4108    _worktree: Option<&Path>,
4109    _gitdir: &Path,
4110) -> Result<()> {
4111    Ok(())
4112}
4113
4114impl Repository {
4115    /// Enforce `safe.directory` ownership checks, matching upstream behavior.
4116    ///
4117    /// When `GIT_TEST_ASSUME_DIFFERENT_OWNER=1`, ownership is considered unsafe
4118    /// unless a matching `safe.directory` value is configured in system/global/
4119    /// command scopes (repository-local config is ignored).
4120    pub fn enforce_safe_directory(&self) -> Result<()> {
4121        let assume_different =
4122            self.test_assume_different_owner || self.environment.test_assume_different_owner();
4123        if !assume_different {
4124            return Ok(());
4125        }
4126
4127        if self.explicit_git_dir {
4128            return Ok(());
4129        }
4130
4131        // In normal discovery, ownership is checked against worktree paths
4132        // unless invocation starts inside the gitdir, in which case gitdir is
4133        // checked.
4134        let checked = if let Some(wt) = &self.work_tree {
4135            let cwd = Some(self.environment.discovery_cwd());
4136            if let Some(cwd) = cwd {
4137                if cwd
4138                    .canonicalize_simplified()
4139                    .ok()
4140                    .is_some_and(|c| c.starts_with(&self.git_dir))
4141                {
4142                    self.git_dir
4143                        .canonicalize_simplified()
4144                        .unwrap_or_else(|_| self.git_dir.clone())
4145                } else {
4146                    wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone())
4147                }
4148            } else {
4149                wt.canonicalize_simplified().unwrap_or_else(|_| wt.clone())
4150            }
4151        } else {
4152            self.git_dir
4153                .canonicalize_simplified()
4154                .unwrap_or_else(|_| self.git_dir.clone())
4155        };
4156
4157        let _ = self.environment.grit_debug_safe_dir;
4158        self.enforce_safe_directory_checked(&checked)
4159    }
4160
4161    /// Enforce safe.directory checks using the repository git-dir path.
4162    ///
4163    /// Used by operations that explicitly open another repository by path
4164    /// (e.g. local clone source).
4165    pub(crate) fn enforce_safe_directory_git_dir(&self) -> Result<()> {
4166        let assume_different =
4167            self.test_assume_different_owner || self.environment.test_assume_different_owner();
4168        if !assume_different {
4169            return Ok(());
4170        }
4171        let checked = self
4172            .git_dir
4173            .canonicalize_simplified()
4174            .unwrap_or_else(|_| self.git_dir.clone());
4175        let _ = self.environment.grit_debug_safe_dir;
4176        self.enforce_safe_directory_checked(&checked)
4177    }
4178
4179    fn enforce_safe_directory_checked(&self, checked: &Path) -> Result<()> {
4180        ensure_safe_directory_allows(&self.git_dir, checked, self.environment.as_ref())
4181    }
4182
4183    /// Verify the repository is safe to use as a `git clone` source (local clone).
4184    ///
4185    /// When `GIT_TEST_ASSUME_DIFFERENT_OWNER` is set, applies the same `safe.directory`
4186    /// rules as discovery. Otherwise checks filesystem ownership of the git directory
4187    /// only (matching Git's `die_upon_dubious_ownership` for clone).
4188    pub fn verify_safe_for_clone_source(&self) -> Result<()> {
4189        let assume_different =
4190            self.test_assume_different_owner || self.environment.test_assume_different_owner();
4191        if assume_different {
4192            self.enforce_safe_directory_git_dir()
4193        } else {
4194            #[cfg(unix)]
4195            {
4196                ensure_valid_ownership(self.environment.as_ref(), None, None, &self.git_dir)
4197            }
4198            #[cfg(not(unix))]
4199            {
4200                Ok(())
4201            }
4202        }
4203    }
4204}
4205
4206fn normalize_fs_path(raw: &str) -> String {
4207    use std::path::Component;
4208    let p = std::path::Path::new(raw);
4209    let mut parts: Vec<String> = Vec::new();
4210    let mut absolute = false;
4211    for c in p.components() {
4212        match c {
4213            Component::RootDir => {
4214                absolute = true;
4215                parts.clear();
4216            }
4217            Component::CurDir => {}
4218            Component::ParentDir => {
4219                if !parts.is_empty() {
4220                    parts.pop();
4221                }
4222            }
4223            Component::Normal(s) => parts.push(s.to_string_lossy().to_string()),
4224            Component::Prefix(_) => {}
4225        }
4226    }
4227    let mut out = if absolute {
4228        String::from("/")
4229    } else {
4230        String::new()
4231    };
4232    out.push_str(&parts.join("/"));
4233    out
4234}
4235
4236fn safe_directory_matches(config_value: &str, checked: &str, cwd: &Path) -> bool {
4237    if config_value == "*" {
4238        return true;
4239    }
4240    if config_value == "." {
4241        // CWD only.
4242        let cwd_s = normalize_fs_path(&cwd.to_string_lossy());
4243        let checked_s = normalize_fs_path(checked);
4244        return cwd_s == checked_s;
4245    }
4246
4247    let canonicalize_or_normalize = |raw: &str| -> String {
4248        let p = std::path::Path::new(raw);
4249        if p.exists() {
4250            p.canonicalize_simplified()
4251                .map(|c| c.to_string_lossy().to_string())
4252                .map(|s| normalize_fs_path(&s))
4253                .unwrap_or_else(|_| normalize_fs_path(raw))
4254        } else {
4255            normalize_fs_path(raw)
4256        }
4257    };
4258
4259    let config_norm = canonicalize_or_normalize(config_value);
4260    let checked_norm = normalize_fs_path(checked);
4261
4262    if config_norm.ends_with("/*") {
4263        let prefix_raw = &config_norm[..config_norm.len() - 2];
4264        let prefix_norm = canonicalize_or_normalize(prefix_raw);
4265        let mut prefix = prefix_norm;
4266        if !prefix.ends_with('/') {
4267            prefix.push('/');
4268        }
4269        return checked_norm.starts_with(&prefix);
4270    }
4271
4272    config_norm == checked_norm
4273}
4274
4275/// Warn when `repo`'s configuration sets both `core.bare` and `core.worktree`.
4276///
4277/// Reads the configuration snapshot `repo` just loaded rather than parsing the cascade again.
4278fn warn_core_bare_worktree_conflict(options: &RepositoryOptions, repo: &Repository) {
4279    if options
4280        .environment
4281        .git_work_tree
4282        .as_deref()
4283        .is_some_and(|s| !s.trim().is_empty())
4284    {
4285        return;
4286    }
4287    let Ok(cfg) = repo.ensure_config_arc_fast() else {
4288        return;
4289    };
4290    if let (true, Some(_)) = read_core_bare_and_worktree_from_config(&cfg) {
4291        options
4292            .diagnostics
4293            .warn(diagnostics::Warning::CoreBareWithWorktree);
4294    }
4295}
4296
4297fn read_core_bare_and_worktree_from_config(cfg: &ConfigSet) -> (bool, Option<String>) {
4298    let bare = cfg
4299        .get_bool("core.bare")
4300        .and_then(|r| r.ok())
4301        .unwrap_or(false);
4302    let worktree = cfg.get("core.worktree").map(|s| s.to_owned());
4303    (bare, worktree)
4304}
4305
4306fn read_core_bare_and_worktree(git_dir: &Path) -> Result<(bool, Option<String>)> {
4307    let cfg = ConfigSet::load(
4308        &crate::environment::Environment::empty(),
4309        Some(git_dir),
4310        true,
4311    )
4312    .unwrap_or_default();
4313    Ok(read_core_bare_and_worktree_from_config(&cfg))
4314}
4315
4316/// Reject impossible `GIT_WORK_TREE` values before repository setup (matches Git's
4317/// `validate_worktree` / `die` on bogus absolute paths, e.g. t1501).
4318fn validate_git_work_tree_path(path: &Path) -> Result<()> {
4319    if !path.is_absolute() {
4320        return Ok(());
4321    }
4322    let comps: Vec<Component<'_>> = path.components().collect();
4323    let Some(last_normal_idx) = comps
4324        .iter()
4325        .enumerate()
4326        .rev()
4327        .find_map(|(i, c)| matches!(c, Component::Normal(_)).then_some(i))
4328    else {
4329        return Ok(());
4330    };
4331    let mut cur = PathBuf::new();
4332    for (i, comp) in comps.iter().enumerate() {
4333        match comp {
4334            Component::Prefix(p) => cur.push(p.as_os_str()),
4335            Component::RootDir => cur.push(comp.as_os_str()),
4336            Component::CurDir => {}
4337            Component::ParentDir => {
4338                let _ = cur.pop();
4339            }
4340            Component::Normal(seg) => {
4341                cur.push(seg);
4342                if i != last_normal_idx && !cur.exists() {
4343                    return Err(Error::PathError(format!(
4344                        "Invalid path '{}': No such file or directory",
4345                        cur.display()
4346                    )));
4347                }
4348            }
4349        }
4350    }
4351    Ok(())
4352}
4353
4354fn resolve_core_worktree_path(git_dir: &Path, raw: &str) -> Result<PathBuf> {
4355    let p = Path::new(raw);
4356    if p.is_absolute() {
4357        return Ok(p
4358            .canonicalize_simplified()
4359            .unwrap_or_else(|_| p.to_path_buf()));
4360    }
4361    let joined = git_dir.join(p);
4362    Ok(joined.canonicalize_simplified().unwrap_or(joined))
4363}
4364
4365/// When `GIT_DIR` names a gitfile, resolve to the real git directory.
4366fn resolve_git_dir_env_path(git_dir: &Path) -> Result<PathBuf> {
4367    if git_dir.is_file() {
4368        return resolve_gitfile(git_dir);
4369    }
4370    Ok(git_dir.to_path_buf())
4371}
4372
4373/// Resolve an explicit git directory path the same way as `GIT_DIR` (including gitfile indirection).
4374///
4375/// # Errors
4376///
4377/// Returns [`Error::NotARepository`] for invalid gitfile content.
4378pub fn resolve_git_directory_arg(git_dir: &Path) -> Result<PathBuf> {
4379    resolve_git_dir_env_path(git_dir)
4380}
4381
4382/// Resolves a work tree's `.git` path (directory or gitfile) to the real git directory.
4383///
4384/// # Errors
4385///
4386/// Returns [`Error::NotARepository`] when `.git` is missing, invalid, or the gitfile target is absent.
4387pub fn resolve_dot_git(dot_git: &Path) -> Result<PathBuf> {
4388    if dot_git.is_dir() {
4389        return dot_git
4390            .canonicalize_simplified()
4391            .map_err(|_| not_a_repository_at(dot_git));
4392    }
4393    if dot_git.is_file() {
4394        return resolve_gitfile(dot_git);
4395    }
4396    Err(not_a_repository_at(dot_git))
4397}
4398
4399fn not_a_repository_at(path: &Path) -> Error {
4400    Error::NotARepository(NotARepositoryReason::NoRepositoryAt {
4401        path: path.to_path_buf(),
4402    })
4403}
4404
4405/// Canonicalize the longest existing prefix of `path` and append the rest unchanged, so a
4406/// path that does not exist yet is spelled the same way as its existing parent (through
4407/// symlinks such as macOS `/var` -> `/private/var`).
4408fn canonicalize_existing_prefix(path: &Path) -> Result<PathBuf> {
4409    let absolute = std::path::absolute(path)?;
4410    let mut missing = Vec::new();
4411    let mut existing = absolute.as_path();
4412    loop {
4413        if let Ok(canonical) = existing.canonicalize_simplified() {
4414            return Ok(missing
4415                .iter()
4416                .rev()
4417                .fold(canonical, |acc: PathBuf, part| acc.join(part)));
4418        }
4419        match (existing.parent(), existing.file_name()) {
4420            (Some(parent), Some(name)) => {
4421                missing.push(name.to_owned());
4422                existing = parent;
4423            }
4424            _ => return Ok(absolute),
4425        }
4426    }
4427}
4428
4429/// The repository's primary objects directory.
4430///
4431/// [`Environment::git_object_directory`] wins when set (a relative path resolves against
4432/// [`Environment::discovery_cwd`]); the main store is then reachable only through alternates,
4433/// as in Git. Otherwise `objects/` in `git_dir`, or in `common_git_dir` for a linked
4434/// worktree's admin directory.
4435///
4436/// Returns `None` when the variable is set but empty: Git treats that as a store that cannot
4437/// exist, so the repository fails to open.
4438fn resolve_objects_dir(
4439    environment: &Environment,
4440    git_dir: &Path,
4441    common_git_dir: &Path,
4442) -> Option<PathBuf> {
4443    if let Some(dir) = environment.git_object_directory.as_deref() {
4444        return (!dir.is_empty()).then(|| environment.discovery_cwd().join(dir));
4445    }
4446    let own = git_dir.join("objects");
4447    Some(if own.exists() {
4448        own
4449    } else {
4450        common_git_dir.join("objects")
4451    })
4452}
4453
4454fn head_file_present(git_dir: &Path) -> bool {
4455    let head_path = git_dir.join("HEAD");
4456    head_path.exists() || head_path.is_symlink()
4457}
4458
4459/// Use `injected` as the repository's ref store (registering it for `git_dir`), or open the
4460/// backend the git directory's config selects.
4461fn open_or_install_ref_store(
4462    caches: &RepoCaches,
4463    git_dir: &Path,
4464    environment: &Environment,
4465    injected: Option<Arc<dyn crate::refs::store::RefStore>>,
4466) -> Result<Arc<dyn crate::refs::store::RefStore>> {
4467    match injected {
4468        Some(store) => {
4469            let store = linked_worktree_view(git_dir, store);
4470            caches.install_ref_store(git_dir, Arc::clone(&store));
4471            Ok(store)
4472        }
4473        None => caches.open_ref_store(git_dir, environment),
4474    }
4475}
4476
4477/// The ref store a handle on `git_dir` uses when its refs live in `store`.
4478///
4479/// An injected store holds one `HEAD`, the main worktree's. When `git_dir` is a linked
4480/// worktree's admin directory the handle instead reads and writes that worktree's own `HEAD`
4481/// and `HEAD` reflog in `git_dir`, and every other ref in `store`. Files and reftable stores
4482/// already keep per-worktree refs apart and are returned unchanged.
4483fn linked_worktree_view(
4484    git_dir: &Path,
4485    store: Arc<dyn crate::refs::store::RefStore>,
4486) -> Arc<dyn crate::refs::store::RefStore> {
4487    use crate::refs::store::{LinkedWorktreeRefStore, RefStorageFormat};
4488    let on_disk = matches!(
4489        store.format(),
4490        RefStorageFormat::Files | RefStorageFormat::Reftable
4491    );
4492    if on_disk || !git_dir.join("commondir").is_file() {
4493        return store;
4494    }
4495    Arc::new(LinkedWorktreeRefStore::new(git_dir.to_path_buf(), store))
4496}
4497
4498/// Read `gitfile` and resolve its `gitdir:` target.
4499fn resolve_gitfile(gitfile: &Path) -> Result<PathBuf> {
4500    let content = fs::read_to_string(gitfile).map_err(|error| {
4501        Error::NotARepository(NotARepositoryReason::GitfileUnreadable {
4502            path: gitfile.to_path_buf(),
4503            error,
4504        })
4505    })?;
4506    parse_gitfile(&content, gitfile)
4507}
4508
4509/// Parse the `"gitdir: <path>"` line of `gitfile`'s `content`; a relative target resolves
4510/// against the directory containing `gitfile`.
4511fn parse_gitfile(content: &str, gitfile: &Path) -> Result<PathBuf> {
4512    let Some(target) = content
4513        .lines()
4514        .find_map(|line| line.strip_prefix("gitdir:"))
4515    else {
4516        return Err(Error::NotARepository(
4517            NotARepositoryReason::GitfileMissingGitdir {
4518                path: gitfile.to_path_buf(),
4519            },
4520        ));
4521    };
4522    let path = gitfile
4523        .parent()
4524        .unwrap_or_else(|| Path::new(""))
4525        .join(target.trim());
4526    if !path.exists() {
4527        return Err(not_a_repository_at(&path));
4528    }
4529    Ok(path)
4530}
4531
4532/// Resolve the initial branch name for a new repository.
4533///
4534/// When `explicit` is `Some`, that name is validated and returned. Otherwise
4535/// `init.defaultBranch` is read from the configuration cascade (global/system,
4536/// not any repository-local config) and falls back to `main`.
4537///
4538/// # Errors
4539///
4540/// Returns [`Error::InvalidRef`] when the branch name is not a valid short branch name.
4541pub fn resolve_initial_branch(env: &Environment, explicit: Option<&str>) -> Result<String> {
4542    let name = match explicit {
4543        Some(name) => name.to_owned(),
4544        None => ConfigSet::load(env, None, true)?
4545            .get_last_entry("init.defaultBranch")
4546            .and_then(|e| e.value.clone())
4547            .filter(|s| !s.is_empty())
4548            .unwrap_or_else(|| "main".to_owned()),
4549    };
4550    validate_branch_short_name(&name).map_err(|e| Error::InvalidRef(e.to_string()))?;
4551    Ok(name)
4552}
4553
4554fn resolve_git_dir_for_init(path: &Path, options: &InitOptions) -> Result<PathBuf> {
4555    if options.bare_with_worktree_at_path || options.bare {
4556        return Ok(path.to_path_buf());
4557    }
4558    if let Some(separate) = &options.separate_git_dir {
4559        return Ok(separate.clone());
4560    }
4561    let dot_git = path.join(".git");
4562    if dot_git.is_file() {
4563        let content = fs::read_to_string(&dot_git)?;
4564        return parse_gitfile(&content, &dot_git);
4565    }
4566    Ok(dot_git)
4567}
4568
4569/// Branch short name for init outcome reporting; tolerates detached `HEAD` on reinit.
4570fn head_branch_name_for_outcome(git_dir: &Path) -> Result<String> {
4571    if let Some(target) = crate::refs::read_symbolic_ref(git_dir, "HEAD")? {
4572        if let Some(short) = target.strip_prefix("refs/heads/") {
4573            return Ok(short.to_owned());
4574        }
4575        return Ok(target
4576            .rsplit('/')
4577            .next()
4578            .unwrap_or(target.as_str())
4579            .to_owned());
4580    }
4581    let head_text = fs::read_to_string(git_dir.join("HEAD"))?;
4582    let oid = head_text.trim();
4583    if crate::objects::ObjectId::from_hex(oid).is_ok() {
4584        let end = oid.len().min(7);
4585        return Ok(oid[..end].to_owned());
4586    }
4587    Ok("HEAD".to_owned())
4588}
4589
4590fn hash_algo_from_git_dir(git_dir: &Path) -> Result<HashAlgo> {
4591    let config_path = git_dir.join("config");
4592    if !config_path.is_file() {
4593        return Ok(HashAlgo::Sha1);
4594    }
4595    let content = fs::read_to_string(&config_path)?;
4596    Ok(parse_repository_format(&content, &config_path)?.hash_algo())
4597}
4598
4599/// Ensure standard git directory layout without overwriting `HEAD`, `config`, or refs.
4600fn ensure_git_directory_layout(
4601    git_dir: &Path,
4602    bare: bool,
4603    ref_storage: crate::ref_storage::RefStorageFormat,
4604    skip_hooks_and_info: bool,
4605) -> Result<()> {
4606    let mut subs = vec!["objects", "objects/info", "objects/pack", "refs"];
4607    if !ref_storage.is_reftable() {
4608        subs.extend(["refs/heads", "refs/tags"]);
4609    }
4610    if !bare && !skip_hooks_and_info {
4611        subs.push("info");
4612        subs.push("hooks");
4613    }
4614    for sub in subs {
4615        fs::create_dir_all(git_dir.join(sub))?;
4616    }
4617
4618    if ref_storage.is_reftable() {
4619        let reftable_dir = git_dir.join("reftable");
4620        fs::create_dir_all(&reftable_dir)?;
4621        let tables_list = reftable_dir.join("tables.list");
4622        if !tables_list.exists() {
4623            fs::write(&tables_list, "")?;
4624        }
4625    }
4626    Ok(())
4627}
4628
4629#[allow(clippy::too_many_arguments)]
4630fn write_fresh_git_directory(
4631    git_dir: &Path,
4632    bare: bool,
4633    initial_branch: &str,
4634    template_dir: Option<&Path>,
4635    ref_storage: crate::ref_storage::RefStorageFormat,
4636    object_format: HashAlgo,
4637    skip_hooks_and_info: bool,
4638    environment: &Environment,
4639) -> Result<()> {
4640    ensure_git_directory_layout(git_dir, bare, ref_storage, skip_hooks_and_info)?;
4641
4642    if let Some(tmpl) = template_dir {
4643        if tmpl.is_dir() {
4644            copy_template(tmpl, git_dir)?;
4645        }
4646    }
4647
4648    let needs_extensions = ref_storage.is_reftable() || object_format != HashAlgo::Sha1;
4649    let repo_version = if needs_extensions { 1 } else { 0 };
4650
4651    let mut config_content = String::from("[core]\n");
4652    config_content.push_str(&format!("\trepositoryformatversion = {repo_version}\n"));
4653    config_content.push_str("\tfilemode = true\n");
4654    if bare {
4655        config_content.push_str("\tbare = true\n");
4656    } else {
4657        config_content.push_str("\tbare = false\n");
4658        config_content.push_str("\tlogallrefupdates = true\n");
4659    }
4660    if needs_extensions {
4661        config_content.push_str("[extensions]\n");
4662        if ref_storage.is_reftable() {
4663            config_content.push_str("\trefStorage = reftable\n");
4664        }
4665        if object_format != HashAlgo::Sha1 {
4666            config_content.push_str(&format!("\tobjectformat = {}\n", object_format.name()));
4667        }
4668    }
4669    fs::write(git_dir.join("config"), config_content)?;
4670
4671    // Merge `config` from the template on top of the default (matches `git clone --template`).
4672    if let Some(tmpl) = template_dir {
4673        if tmpl.is_dir() {
4674            let tmpl_config = tmpl.join("config");
4675            if tmpl_config.is_file() {
4676                let tmpl_text = fs::read_to_string(&tmpl_config)?;
4677                let tmpl_parsed = ConfigFile::parse(&tmpl_config, &tmpl_text, ConfigScope::Local)?;
4678                let dest_path = git_dir.join("config");
4679                let dest_text = fs::read_to_string(&dest_path)?;
4680                let mut dest_parsed =
4681                    ConfigFile::parse(&dest_path, &dest_text, ConfigScope::Local)?;
4682                for e in &tmpl_parsed.entries {
4683                    // Git clone ignores `core.bare` from templates (non-bare clone must stay non-bare).
4684                    if e.key == "core.bare" {
4685                        continue;
4686                    }
4687                    if let Some(v) = &e.value {
4688                        let _ = dest_parsed.set(&e.key, v);
4689                    } else {
4690                        let _ = dest_parsed.set(&e.key, "true");
4691                    }
4692                }
4693                dest_parsed.write()?;
4694            }
4695        }
4696    }
4697
4698    crate::refs::store::initialize_head(
4699        git_dir,
4700        crate::refs::store::RawRef::Symbolic(format!("refs/heads/{initial_branch}")),
4701    )?;
4702
4703    apply_init_filesystem_config(git_dir, InitFilesystemConfigOptions::default(), environment)?;
4704
4705    fs::write(
4706        git_dir.join("description"),
4707        "Unnamed repository; edit this file 'description' to name the repository.\n",
4708    )?;
4709    Ok(())
4710}
4711
4712/// Initialise a non-bare repository with the git directory at `git_dir` and the work tree at `work_tree`.
4713///
4714/// Creates `work_tree/.git` as a gitfile pointing at `git_dir` (absolute path). Matches `git clone
4715/// --separate-git-dir` layout.
4716///
4717/// # Errors
4718///
4719/// Returns [`Error::WorktreePathIo`] or [`Error::GitDirPathIo`] on filesystem failures.
4720pub(crate) fn init_repository_separate_git_dir(
4721    work_tree: &Path,
4722    git_dir: &Path,
4723    initial_branch: &str,
4724    template_dir: Option<&Path>,
4725    ref_storage: crate::ref_storage::RefStorageFormat,
4726    object_format: HashAlgo,
4727    environment: &Environment,
4728) -> Result<Repository> {
4729    let skip_hooks_info = template_dir.is_some_and(|p| p.as_os_str().is_empty());
4730    fs::create_dir_all(work_tree).map_err(|source| {
4731        map_worktree_io(
4732            path_for_error_message(work_tree),
4733            WorktreePathOperation::Create,
4734            source,
4735        )
4736    })?;
4737    fs::create_dir_all(git_dir).map_err(|source| {
4738        map_git_dir_io(git_dir, git_dir, GitDirPathOperation::CreateDir, source)
4739    })?;
4740    let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
4741    if existing {
4742        ensure_git_directory_layout(git_dir, false, ref_storage, skip_hooks_info)?;
4743    } else {
4744        write_fresh_git_directory(
4745            git_dir,
4746            false,
4747            initial_branch,
4748            template_dir,
4749            ref_storage,
4750            object_format,
4751            skip_hooks_info,
4752            environment,
4753        )?;
4754    }
4755
4756    // Write an absolute `gitdir:` path, matching C Git's `init_db` →
4757    // `set_git_dir(real_git_dir, make_realpath=1)` → `separate_git_dir`, which records the
4758    // realpath of the separate git directory (`t5601` "clone separate gitdir: output"). This
4759    // path is only used by `git clone --separate-git-dir`; submodule layouts use a different
4760    // code path and are unaffected.
4761    let gitfile = work_tree.join(".git");
4762    let abs_git_dir = crate::git_path::absolute_path_for_gitfile_stored_path(git_dir);
4763    fs::write(gitfile, format!("gitdir: {abs_git_dir}\n"))?;
4764
4765    Repository::open(
4766        git_dir,
4767        &RepositoryOptions::with_environment(environment.clone()).with_work_tree(work_tree),
4768    )
4769}
4770
4771/// Initialise a **minimal** bare repository directory layout matching `git clone --template= --bare`.
4772///
4773/// Git's clone-with-empty-template omits `hooks/`, `info/`, `description`, and `branches/` until
4774/// something needs them; tests rely on `mkdir <repo>/info` succeeding afterward.
4775///
4776/// # Parameters
4777///
4778/// - `git_dir` — bare repository root (the destination `.git` directory for a bare clone).
4779/// - `initial_branch` — used only for the initial `HEAD` symref text before clone rewires it.
4780///
4781/// # Errors
4782///
4783/// Returns [`Error::Io`] on filesystem failures.
4784/// Ensure `core.bare = true` in the repository `config` (used after `git clone --bare`).
4785pub fn ensure_core_bare(git_dir: &Path) -> Result<()> {
4786    let path = git_dir.join("config");
4787    let text = fs::read_to_string(&path).unwrap_or_default();
4788    if text.lines().any(|l| {
4789        let t = l.trim();
4790        t == "bare = true" || t == "bare=true"
4791    }) {
4792        return Ok(());
4793    }
4794    let mut out = text;
4795    if !out.ends_with('\n') && !out.is_empty() {
4796        out.push('\n');
4797    }
4798    if !out.contains("[core]") {
4799        out.push_str("[core]\n");
4800    }
4801    out.push_str("\tbare = true\n");
4802    fs::write(path, out).map_err(Error::Io)
4803}
4804
4805pub(crate) fn init_repository(
4806    path: &Path,
4807    bare: bool,
4808    initial_branch: &str,
4809    template_dir: Option<&Path>,
4810    ref_storage: crate::ref_storage::RefStorageFormat,
4811) -> Result<Repository> {
4812    init_repository_with_object_format(
4813        path,
4814        bare,
4815        initial_branch,
4816        template_dir,
4817        ref_storage,
4818        HashAlgo::Sha1,
4819        &Environment::empty(),
4820    )
4821}
4822
4823pub(crate) fn init_repository_with_object_format(
4824    path: &Path,
4825    bare: bool,
4826    initial_branch: &str,
4827    template_dir: Option<&Path>,
4828    ref_storage: crate::ref_storage::RefStorageFormat,
4829    object_format: HashAlgo,
4830    environment: &Environment,
4831) -> Result<Repository> {
4832    let skip_hooks_info = !bare && template_dir.is_some_and(|p| p.as_os_str().is_empty());
4833    let git_dir = if bare {
4834        path.to_path_buf()
4835    } else {
4836        path.join(".git")
4837    };
4838
4839    if !bare {
4840        fs::create_dir_all(path).map_err(|source| {
4841            map_worktree_io(
4842                path_for_error_message(path),
4843                WorktreePathOperation::Create,
4844                source,
4845            )
4846        })?;
4847    }
4848    fs::create_dir_all(&git_dir).map_err(|source| {
4849        if bare {
4850            map_worktree_io(
4851                path_for_error_message(path),
4852                WorktreePathOperation::Create,
4853                source,
4854            )
4855        } else {
4856            map_git_dir_io(&git_dir, &git_dir, GitDirPathOperation::CreateDir, source)
4857        }
4858    })?;
4859    let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
4860    if existing {
4861        ensure_git_directory_layout(&git_dir, bare, ref_storage, skip_hooks_info)?;
4862    } else {
4863        write_fresh_git_directory(
4864            &git_dir,
4865            bare,
4866            initial_branch,
4867            template_dir,
4868            ref_storage,
4869            object_format,
4870            skip_hooks_info,
4871            environment,
4872        )?;
4873    }
4874
4875    if bare {
4876        Repository::open(
4877            &git_dir,
4878            &RepositoryOptions::with_environment(environment.clone()),
4879        )
4880    } else {
4881        Repository::open(
4882            &git_dir,
4883            &RepositoryOptions::with_environment(environment.clone())
4884                .with_work_tree(path.to_path_buf()),
4885        )
4886    }
4887}
4888
4889/// Initialise a **bare** repository at `git_dir` with `core.worktree` set to `work_tree`.
4890///
4891/// Used when `GIT_WORK_TREE` is set during `git clone`: the clone destination is the bare
4892/// git directory and checked-out files go under the environment work tree (matches upstream Git).
4893///
4894/// # Errors
4895///
4896/// Returns [`Error::WorktreePathIo`] or [`Error::GitDirPathIo`] on filesystem failures.
4897pub(crate) fn init_bare_with_env_worktree(
4898    git_dir: &Path,
4899    work_tree: &Path,
4900    initial_branch: &str,
4901    template_dir: Option<&Path>,
4902    ref_storage: crate::ref_storage::RefStorageFormat,
4903    object_format: HashAlgo,
4904    environment: &Environment,
4905) -> Result<Repository> {
4906    fs::create_dir_all(git_dir)?;
4907    fs::create_dir_all(work_tree)?;
4908    let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
4909    if existing {
4910        ensure_git_directory_layout(git_dir, true, ref_storage, false)?;
4911    } else {
4912        write_fresh_git_directory(
4913            git_dir,
4914            true,
4915            initial_branch,
4916            template_dir,
4917            ref_storage,
4918            object_format,
4919            false,
4920            environment,
4921        )?;
4922    }
4923    let work_tree_abs = work_tree
4924        .canonicalize_simplified()
4925        .unwrap_or_else(|_| work_tree.to_path_buf());
4926    let config_path = git_dir.join("config");
4927    let mut config = match ConfigFile::from_path(&config_path, ConfigScope::Local)? {
4928        Some(c) => c,
4929        None => ConfigFile::parse(&config_path, "", ConfigScope::Local)?,
4930    };
4931    config.set("core.worktree", &work_tree_abs.to_string_lossy())?;
4932    config.write()?;
4933    Repository::open(
4934        git_dir,
4935        &RepositoryOptions::with_environment(environment.clone()).with_work_tree(work_tree),
4936    )
4937}
4938
4939/// Recursively copy template files from `src` to `dst`.
4940fn copy_template(src: &Path, dst: &Path) -> Result<()> {
4941    for entry in fs::read_dir(src)? {
4942        let entry = entry?;
4943        let src_path = entry.path();
4944        let dst_path = dst.join(entry.file_name());
4945        if src_path.is_dir() {
4946            fs::create_dir_all(&dst_path)?;
4947            copy_template(&src_path, &dst_path)?;
4948        } else {
4949            fs::copy(&src_path, &dst_path)?;
4950        }
4951    }
4952    Ok(())
4953}
4954
4955/// Parse `GIT_CEILING_DIRECTORIES` into a list of absolute paths and whether
4956/// symlink resolution should be skipped.
4957///
4958/// The variable is colon-separated (`:`) on Unix.  Empty entries and
4959/// non-absolute paths are silently skipped, matching Git's behaviour.
4960///
4961/// A leading colon (`:path1:path2`) disables symlink resolution for all
4962/// ceiling paths AND the cwd used for comparison (Git `resolve_symlinks` flag).
4963fn parse_ceiling_directories(env: &Environment) -> (Vec<PathBuf>, bool) {
4964    let Some(raw) = env.git_ceiling_directories.as_deref() else {
4965        return (Vec::new(), false);
4966    };
4967    if raw.is_empty() {
4968        return (Vec::new(), false);
4969    }
4970    // A leading colon means "don't resolve symlinks".
4971    let (no_resolve, effective) = match raw.strip_prefix(':') {
4972        Some(rest) => (true, rest),
4973        None => (false, raw),
4974    };
4975    let paths = effective
4976        .split(':')
4977        .filter(|s| !s.is_empty())
4978        .filter_map(|s| {
4979            let p = PathBuf::from(s);
4980            if !p.is_absolute() {
4981                return None;
4982            }
4983            if no_resolve {
4984                // Strip trailing slashes for consistent comparison but don't resolve symlinks.
4985                let s = s.trim_end_matches('/');
4986                Some(PathBuf::from(s))
4987            } else {
4988                // Canonicalize to resolve symlinks; fall back to the raw path
4989                // (with trailing slashes stripped) when the directory doesn't exist.
4990                Some(p.canonicalize_simplified().unwrap_or_else(|_| {
4991                    let s = s.trim_end_matches('/');
4992                    PathBuf::from(s)
4993                }))
4994            }
4995        })
4996        .collect();
4997    (paths, no_resolve)
4998}
4999
5000/// Validate the repository format version from config text.
5001/// Returns Ok if the format is supported, Err with message if not.
5002pub fn validate_repo_config(config_text: &str) -> std::result::Result<(), String> {
5003    let mut version: u32 = 0;
5004    let mut in_core = false;
5005    for line in config_text.lines() {
5006        let trimmed = line.trim();
5007        if trimmed.starts_with('[') {
5008            in_core = trimmed.to_lowercase().starts_with("[core");
5009            continue;
5010        }
5011        if in_core {
5012            if let Some(rest) = trimmed.strip_prefix("repositoryformatversion") {
5013                let val = rest.trim_start_matches([' ', '=']).trim();
5014                if let Ok(v) = val.parse::<u32>() {
5015                    version = v;
5016                }
5017            }
5018        }
5019    }
5020    if version >= 2 {
5021        return Err(format!("unknown repository format version: {version}"));
5022    }
5023    Ok(())
5024}
5025
5026/// Per-repository caches shared across open handles.
5027pub mod caches {
5028    pub use crate::repo_caches::*;
5029}
5030
5031mod index_write_fail_inject {
5032    use std::cell::RefCell;
5033
5034    // hygiene: thread-local index write failure injection for tests
5035    thread_local! {
5036        pub static INJECT: RefCell<bool> = const { RefCell::new(false) };
5037        pub static INJECT_REMAINING: RefCell<Option<usize>> = const { RefCell::new(None) };
5038    }
5039}
5040
5041/// Debug-only: fail every [`Repository::write_index`] / index lock path while enabled.
5042#[doc(hidden)]
5043pub fn set_test_inject_index_write_fail(enabled: bool) {
5044    index_write_fail_inject::INJECT.with(|c| *c.borrow_mut() = enabled);
5045    if !enabled {
5046        index_write_fail_inject::INJECT_REMAINING.with(|c| *c.borrow_mut() = None);
5047    }
5048}
5049
5050/// Debug-only: fail the next `remaining` index write attempts, then allow writes.
5051#[doc(hidden)]
5052pub fn set_test_inject_index_write_fail_remaining(remaining: usize) {
5053    index_write_fail_inject::INJECT.with(|c| *c.borrow_mut() = true);
5054    index_write_fail_inject::INJECT_REMAINING.with(|c| *c.borrow_mut() = Some(remaining));
5055}
5056
5057mod rewrite_apply_inject {
5058    use std::cell::RefCell;
5059
5060    // hygiene: thread-local rewrite abort injection for tests
5061    thread_local! {
5062        pub static ABORT_AFTER_REFS: RefCell<bool> = const { RefCell::new(false) };
5063    }
5064}
5065
5066/// Debug-only: stop [`crate::history::apply_rewrite`] after ref updates without rolling back.
5067#[doc(hidden)]
5068pub fn set_test_inject_rewrite_apply_abort_after_refs(enabled: bool) {
5069    rewrite_apply_inject::ABORT_AFTER_REFS.with(|c| *c.borrow_mut() = enabled);
5070}
5071
5072pub(crate) fn test_inject_rewrite_apply_abort_after_refs() -> bool {
5073    rewrite_apply_inject::ABORT_AFTER_REFS.with(|c| *c.borrow())
5074}
5075
5076fn test_inject_index_write_fail() -> Result<()> {
5077    index_write_fail_inject::INJECT.with(|enabled| {
5078        if !*enabled.borrow() {
5079            return Ok(());
5080        }
5081        index_write_fail_inject::INJECT_REMAINING.with(|remaining| {
5082            if let Some(left) = remaining.borrow_mut().as_mut() {
5083                if *left == 0 {
5084                    return Ok(());
5085                }
5086                *left -= 1;
5087            }
5088            Err(Error::Io(std::io::Error::other(
5089                "injected index write failure",
5090            )))
5091        })
5092    })
5093}
5094
5095#[path = "repo_location.rs"]
5096mod location;
5097pub use location::RepositoryLocation;
5098
5099#[cfg(test)]
5100#[path = "repo_line_coverage_tests.rs"]
5101mod line_coverage_tests;
5102
5103#[cfg(test)]
5104mod config_session_tests {
5105    #![allow(clippy::unwrap_used)]
5106
5107    use super::*;
5108    use std::ffi::OsString;
5109
5110    fn options_with_global(home: &Path, global: &Path) -> RepositoryOptions {
5111        RepositoryOptions {
5112            environment: Environment::from_vars(
5113                [
5114                    ("HOME", home.as_os_str()),
5115                    ("GIT_CONFIG_NOSYSTEM", "1".as_ref()),
5116                    ("GIT_CONFIG_GLOBAL", global.as_os_str()),
5117                ]
5118                .map(|(k, v)| (OsString::from(k), v.to_owned())),
5119                home.to_path_buf(),
5120            ),
5121            ..RepositoryOptions::default()
5122        }
5123    }
5124
5125    #[test]
5126    fn config_is_reused_until_a_cascade_file_changes() {
5127        let dir = tempfile::tempdir().unwrap();
5128        let global = dir.path().join("global.cfg");
5129        fs::write(&global, "[user]\n\tname = First\n").unwrap();
5130        let work = dir.path().join("work");
5131        let options = options_with_global(dir.path(), &global);
5132        Repository::init(
5133            &work,
5134            &InitOptions {
5135                environment: Some(options.environment.clone()),
5136                ..InitOptions::default()
5137            },
5138        )
5139        .unwrap();
5140        let repo = Repository::discover(Some(&work), &options).unwrap();
5141
5142        let first = repo.config().unwrap();
5143        assert_eq!(first.get("user.name").as_deref(), Some("First"));
5144        assert!(Arc::ptr_eq(&first, &repo.config().unwrap()));
5145
5146        fs::write(&global, "[user]\n\tname = Second one\n").unwrap();
5147        assert_eq!(
5148            repo.config().unwrap().get("user.name").as_deref(),
5149            Some("Second one")
5150        );
5151
5152        let local = repo.git_dir().join("config");
5153        let mut text = fs::read_to_string(&local).unwrap();
5154        text.push_str("[user]\n\tname = Local\n");
5155        fs::write(&local, text).unwrap();
5156        let reloaded = repo.config().unwrap();
5157        assert_eq!(reloaded.get("user.name").as_deref(), Some("Local"));
5158        assert!(Arc::ptr_eq(&reloaded, &repo.config().unwrap()));
5159
5160        fs::remove_file(&global).unwrap();
5161        fs::write(&local, "[core]\n\trepositoryformatversion = 0\n").unwrap();
5162        assert_eq!(repo.config().unwrap().get("user.name"), None);
5163    }
5164
5165    #[test]
5166    fn a_replaced_environment_is_checked_in_full_once() {
5167        let dir = tempfile::tempdir().unwrap();
5168        let global = dir.path().join("global.cfg");
5169        fs::write(&global, "[user]\n\tname = Env\n").unwrap();
5170        let work = dir.path().join("work");
5171        let options = options_with_global(dir.path(), &global);
5172        Repository::init(
5173            &work,
5174            &InitOptions {
5175                environment: Some(options.environment.clone()),
5176                ..InitOptions::default()
5177            },
5178        )
5179        .unwrap();
5180        let mut repo = Repository::discover(Some(&work), &options).unwrap();
5181        let before = repo.config().unwrap();
5182
5183        // An equal snapshot in a new allocation passes the full check and keeps the session.
5184        repo.environment = Arc::new(options.environment.clone());
5185        assert!(Arc::ptr_eq(&before, &repo.config().unwrap()));
5186
5187        // A snapshot naming another global file must not reuse the old stamps.
5188        let other = dir.path().join("other.cfg");
5189        fs::write(&other, "[user]\n\tname = Other\n").unwrap();
5190        repo.environment = Arc::new(options_with_global(dir.path(), &other).environment);
5191        assert_eq!(
5192            repo.config().unwrap().get("user.name").as_deref(),
5193            Some("Other")
5194        );
5195    }
5196}