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