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