Skip to main content

grit_lib/
repo.rs

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