gix/worktree/
mod.rs

1use std::path::PathBuf;
2
3#[cfg(feature = "worktree-archive")]
4pub use gix_archive as archive;
5#[cfg(feature = "excludes")]
6pub use gix_worktree::*;
7#[cfg(feature = "worktree-mutation")]
8pub use gix_worktree_state as state;
9#[cfg(feature = "worktree-stream")]
10pub use gix_worktree_stream as stream;
11
12use crate::{
13    bstr::{BStr, BString},
14    Repository,
15};
16
17#[cfg(feature = "index")]
18pub(crate) type IndexStorage = gix_features::threading::OwnShared<gix_fs::SharedFileSnapshotMut<gix_index::File>>;
19/// A lazily loaded and auto-updated worktree index.
20#[cfg(feature = "index")]
21pub type Index = gix_fs::SharedFileSnapshot<gix_index::File>;
22
23/// A type to represent an index which either was loaded from disk as it was persisted there, or created on the fly in memory.
24#[cfg(feature = "index")]
25#[allow(clippy::large_enum_variant)]
26#[derive(Clone)]
27pub enum IndexPersistedOrInMemory {
28    /// The index as loaded from disk, and shared across clones of the owning `Repository`.
29    Persisted(Index),
30    /// A temporary index as created from the `HEAD^{tree}`, with the file path set to the place where it would be stored naturally.
31    ///
32    /// Note that unless saved explicitly, it will not persist.
33    InMemory(gix_index::File),
34}
35
36#[cfg(feature = "index")]
37impl From<Index> for IndexPersistedOrInMemory {
38    fn from(value: Index) -> Self {
39        IndexPersistedOrInMemory::Persisted(value)
40    }
41}
42
43#[cfg(feature = "index")]
44impl From<gix_index::File> for IndexPersistedOrInMemory {
45    fn from(value: gix_index::File) -> Self {
46        IndexPersistedOrInMemory::InMemory(value)
47    }
48}
49
50/// A stand-in to a worktree as result of a worktree iteration.
51///
52/// It provides access to typical worktree state, but may not actually point to a valid checkout as the latter has been moved or
53/// deleted.
54#[derive(Debug, Clone)]
55pub struct Proxy<'repo> {
56    pub(crate) parent: &'repo Repository,
57    pub(crate) git_dir: PathBuf,
58}
59
60/// Access
61impl<'repo> crate::Worktree<'repo> {
62    /// Read the location of the checkout, the base of the work tree
63    pub fn base(&self) -> &'repo std::path::Path {
64        self.path
65    }
66
67    /// Return true if this worktree is the main worktree associated with a non-bare git repository.
68    ///
69    /// It cannot be removed.
70    pub fn is_main(&self) -> bool {
71        self.id().is_none()
72    }
73
74    /// Return true if this worktree cannot be pruned, moved or deleted, which is useful if it is located on an external storage device.
75    ///
76    /// Always false for the main worktree.
77    pub fn is_locked(&self) -> bool {
78        Proxy::new(self.parent, self.parent.git_dir()).is_locked()
79    }
80
81    /// Provide a reason for the locking of this worktree, if it is locked at all.
82    ///
83    /// Note that we squelch errors in case the file cannot be read in which case the
84    /// reason is an empty string.
85    pub fn lock_reason(&self) -> Option<BString> {
86        Proxy::new(self.parent, self.parent.git_dir()).lock_reason()
87    }
88
89    /// Return the ID of the repository worktree, if it is a linked worktree, or `None` if it's a linked worktree.
90    pub fn id(&self) -> Option<&BStr> {
91        id(self.parent.git_dir(), self.parent.common_dir.is_some())
92    }
93
94    /// Returns true if the `.git` file or directory exists within the worktree.
95    ///
96    /// This is an indicator for the worktree to be checked out particularly if the parent repository is a submodule.
97    pub fn dot_git_exists(&self) -> bool {
98        self.path.join(gix_discover::DOT_GIT_DIR).exists()
99    }
100}
101
102pub(crate) fn id(git_dir: &std::path::Path, has_common_dir: bool) -> Option<&BStr> {
103    if !has_common_dir {
104        return None;
105    }
106    let candidate = gix_path::os_str_into_bstr(git_dir.file_name().expect("at least one directory level"))
107        .expect("no illformed UTF-8");
108    let maybe_worktrees = git_dir.parent()?;
109    (maybe_worktrees.file_name()?.to_str()? == "worktrees").then_some(candidate)
110}
111
112///
113pub mod proxy;
114
115///
116#[cfg(feature = "index")]
117pub mod open_index {
118    /// The error returned by [`Worktree::open_index()`][crate::Worktree::open_index()].
119    #[derive(Debug, thiserror::Error)]
120    #[allow(missing_docs)]
121    pub enum Error {
122        #[error(transparent)]
123        ConfigIndexThreads(#[from] crate::config::key::GenericErrorWithValue),
124        #[error(transparent)]
125        ConfigSkipHash(#[from] crate::config::boolean::Error),
126        #[error(transparent)]
127        IndexFile(#[from] gix_index::file::init::Error),
128        #[error(transparent)]
129        IndexCorrupt(#[from] gix_index::file::verify::Error),
130    }
131
132    impl crate::Worktree<'_> {
133        /// A shortcut to [`crate::Repository::open_index()`].
134        pub fn open_index(&self) -> Result<gix_index::File, Error> {
135            self.parent.open_index()
136        }
137
138        /// A shortcut to [`crate::Repository::index()`].
139        pub fn index(&self) -> Result<crate::worktree::Index, Error> {
140            self.parent.index()
141        }
142    }
143}
144
145///
146#[cfg(feature = "excludes")]
147pub mod excludes {
148    use crate::AttributeStack;
149
150    /// The error returned by [`Worktree::excludes()`][crate::Worktree::excludes()].
151    #[derive(Debug, thiserror::Error)]
152    #[allow(missing_docs)]
153    pub enum Error {
154        #[error(transparent)]
155        OpenIndex(#[from] crate::worktree::open_index::Error),
156        #[error(transparent)]
157        CreateCache(#[from] crate::config::exclude_stack::Error),
158    }
159
160    impl crate::Worktree<'_> {
161        /// Configure a file-system cache checking if files below the repository are excluded.
162        ///
163        /// This takes into consideration all the usual repository configuration, namely:
164        ///
165        /// * `$XDG_CONFIG_HOME/…/ignore` if `core.excludesFile` is *not* set, otherwise use the configured file.
166        /// * `$GIT_DIR/info/exclude` if present.
167        ///
168        /// When only excludes are desired, this is the most efficient way to obtain them. Otherwise use
169        /// [`Worktree::attributes()`][crate::Worktree::attributes()] for accessing both attributes and excludes.
170        pub fn excludes(&self, overrides: Option<gix_ignore::Search>) -> Result<AttributeStack<'_>, Error> {
171            let index = self.index()?;
172            Ok(self.parent.excludes(
173                &index,
174                overrides,
175                gix_worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped,
176            )?)
177        }
178    }
179}
180
181///
182#[cfg(feature = "attributes")]
183pub mod attributes {
184    use crate::{AttributeStack, Worktree};
185
186    /// The error returned by [`Worktree::attributes()`].
187    #[derive(Debug, thiserror::Error)]
188    #[allow(missing_docs)]
189    pub enum Error {
190        #[error(transparent)]
191        OpenIndex(#[from] crate::worktree::open_index::Error),
192        #[error(transparent)]
193        CreateCache(#[from] crate::repository::attributes::Error),
194    }
195
196    impl<'repo> Worktree<'repo> {
197        /// Configure a file-system cache checking if files below the repository are excluded or for querying their attributes.
198        ///
199        /// This takes into consideration all the usual repository configuration, namely:
200        ///
201        /// * `$XDG_CONFIG_HOME/…/ignore|attributes` if `core.excludesFile|attributesFile` is *not* set, otherwise use the configured file.
202        /// * `$GIT_DIR/info/exclude|attributes` if present.
203        pub fn attributes(&self, overrides: Option<gix_ignore::Search>) -> Result<AttributeStack<'repo>, Error> {
204            let index = self.index()?;
205            Ok(self.parent.attributes(
206                &index,
207                gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping,
208                gix_worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped,
209                overrides,
210            )?)
211        }
212
213        /// Like [attributes()][Self::attributes()], but without access to exclude/ignore information.
214        pub fn attributes_only(&self) -> Result<AttributeStack<'repo>, Error> {
215            let index = self.index()?;
216            self.parent
217                .attributes_only(
218                    &index,
219                    gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping,
220                )
221                .map_err(|err| Error::CreateCache(err.into()))
222        }
223    }
224}
225
226///
227#[cfg(feature = "attributes")]
228pub mod pathspec {
229    use crate::{
230        bstr::BStr,
231        config::{cache::util::ApplyLeniencyDefaultValue, tree::gitoxide},
232        Worktree,
233    };
234
235    /// The error returned by [`Worktree::pathspec()`].
236    #[derive(Debug, thiserror::Error)]
237    #[allow(missing_docs)]
238    pub enum Error {
239        #[error(transparent)]
240        Init(#[from] crate::pathspec::init::Error),
241        #[error(transparent)]
242        OpenIndex(#[from] crate::worktree::open_index::Error),
243    }
244
245    impl<'repo> Worktree<'repo> {
246        /// Configure pathspecs `patterns` to be matched against, with pathspec attributes read from the worktree and then from the index
247        /// if needed.
248        ///
249        /// Note that the `empty_patterns_match_prefix` flag of the [parent method](crate::Repository::pathspec()) defaults to `true`.
250        ///
251        /// ### Deviation
252        ///
253        /// Pathspec attributes match case-insensitively by default if the underlying filesystem is configured that way.
254        pub fn pathspec(
255            &self,
256            patterns: impl IntoIterator<Item = impl AsRef<BStr>>,
257        ) -> Result<crate::Pathspec<'repo>, Error> {
258            let index = self.index()?;
259            let inherit_ignore_case = self
260                .parent
261                .config
262                .resolved
263                .boolean("gitoxide.pathspec.inheritIgnoreCase")
264                .map(|res| {
265                    gitoxide::Pathspec::INHERIT_IGNORE_CASE
266                        .enrich_error(res)
267                        .with_lenient_default_value(
268                            self.parent.config.lenient_config,
269                            gitoxide::Pathspec::INHERIT_IGNORE_CASE_DEFAULT,
270                        )
271                })
272                .transpose()
273                .map_err(|err| Error::Init(crate::pathspec::init::Error::Defaults(err.into())))?
274                .unwrap_or(gitoxide::Pathspec::INHERIT_IGNORE_CASE_DEFAULT);
275            Ok(self.parent.pathspec(
276                true, /* empty patterns match prefix */
277                patterns,
278                inherit_ignore_case,
279                &index,
280                gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping,
281            )?)
282        }
283    }
284}