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