Skip to main content

grit_lib/refs/
handle.rs

1//! Repository-scoped reference and reflog access via [`References`].
2use std::ops::ControlFlow;
3
4use crate::error::Result;
5use crate::objects::ObjectId;
6use crate::reflog::ReflogEntry;
7use crate::refs::listing::{query_refs, RefListing, RefQuery, RefQueryError, RefQueryOptions};
8use crate::refs::store::{Expected, RawRef, RefName, RefUpdate, RefUpdateFlags};
9use crate::refs::{
10    commit_ref_update_on_store, ensure_refname_safe_for_storage, list_refs_for_repository,
11    update_direct_ref_on_store,
12};
13use crate::repo::Repository;
14
15/// A ref returned by [`References::read_existing`].
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub(crate) struct ExistingRef {
18    /// Value as stored (a direct oid or a symbolic target).
19    pub(crate) raw: RawRef,
20    /// Object the ref resolves to.
21    pub(crate) oid: ObjectId,
22}
23
24/// Resolve, list, and update refs for one open repository.
25#[derive(Debug, Copy, Clone)]
26pub struct References<'repo> {
27    repo: &'repo Repository,
28}
29
30impl<'repo> References<'repo> {
31    /// Bind ref operations to `repo`.
32    #[inline]
33    #[must_use]
34    pub fn new(repo: &'repo Repository) -> Self {
35        Self { repo }
36    }
37
38    /// Underlying repository.
39    #[inline]
40    #[must_use]
41    pub fn repository(&self) -> &'repo Repository {
42        self.repo
43    }
44
45    /// Resolve a ref name to an object id, following symbolic refs.
46    ///
47    /// # Errors
48    ///
49    /// Propagates ref store and resolution errors.
50    ///
51    /// # Examples
52    ///
53    /// ```
54    /// # fn main() -> grit_lib::error::Result<()> {
55    /// use grit_lib::refs::store::RefName;
56    /// use grit_lib::repo::{InitOptions, Repository};
57    /// use grit_lib::ident::Signature;
58    /// use grit_lib::porcelain::commit::{create_commit, CommitRequest};
59    /// use grit_lib::progress::NullProgress;
60    /// let dir = std::env::temp_dir().join("grit-refs-resolve-doc");
61    /// let repo = Repository::init(&dir, &InitOptions::default())?;
62    /// let mut req = CommitRequest::new("doc");
63    /// req.author = Some(Signature::new("Doc", "doc@example.com", repo.identity_now())?);
64    /// req.committer = Some(Signature::new("Doc", "doc@example.com", repo.identity_now())?);
65    /// req.allow_empty = true;
66    /// let _ = create_commit(&repo, &req, &mut NullProgress, &grit_lib::progress::NeverCancel)?;
67    /// let main = RefName::try_from("refs/heads/main").expect("valid ref");
68    /// let head = repo.references().resolve(&main)?;
69    /// assert!(!head.to_hex().is_empty());
70    /// # Ok(())
71    /// # }
72    /// ```
73    #[inline]
74    pub fn resolve(&self, name: &RefName) -> Result<ObjectId> {
75        self.repo.resolve_ref_oid(name.as_str())
76    }
77
78    /// Read `name` when it exists: its stored value and the object it resolves to.
79    ///
80    /// Unlike [`resolve`](Self::resolve), a missing ref is `Ok(None)` rather than an error,
81    /// so read and corruption failures stay distinguishable from absence. A direct ref is
82    /// read once; only a symbolic ref is followed.
83    pub(crate) fn read_existing(&self, name: &str) -> Result<Option<ExistingRef>> {
84        let Some(raw) = self.repo.refs().read_raw(name)? else {
85            return Ok(None);
86        };
87        let oid = match &raw {
88            RawRef::Direct(oid) => *oid,
89            RawRef::Symbolic(_) => self.repo.resolve_ref_oid(name)?,
90        };
91        Ok(Some(ExistingRef { raw, oid }))
92    }
93
94    /// Resolve `name` when it exists, `Ok(None)` when it does not (see [`Self::read_existing`]).
95    pub(crate) fn resolve_if_exists(&self, name: &str) -> Result<Option<ObjectId>> {
96        Ok(self.read_existing(name)?.map(|existing| existing.oid))
97    }
98
99    /// Delete `name` and its reflog through this repository's ref store, provided it still
100    /// has the value `existing` was read with.
101    ///
102    /// The delete is a compare-and-swap, so a concurrent update between the caller's checks
103    /// and the delete fails instead of being lost. A symbolic ref is deleted itself, not its
104    /// target.
105    pub(crate) fn delete_verified(&self, name: &str, existing: &ExistingRef) -> Result<()> {
106        ensure_refname_safe_for_storage(name)?;
107        let (expected, no_deref) = match &existing.raw {
108            RawRef::Symbolic(target) => (Expected::Symref(target.clone()), true),
109            RawRef::Direct(oid) => (Expected::Oid(*oid), false),
110        };
111        let store = self.repo.refs();
112        commit_ref_update_on_store(
113            store,
114            RefUpdate {
115                name: name.to_owned(),
116                new_value: None,
117                expected,
118                reflog: None,
119                flags: RefUpdateFlags {
120                    no_deref,
121                    ..RefUpdateFlags::default()
122                },
123            },
124        )?;
125        store.delete_reflog(name)
126    }
127
128    /// List refs under `prefix` (for example `"refs/heads/"`), sorted by name.
129    ///
130    /// # Errors
131    ///
132    /// Propagates ref store iteration errors.
133    #[inline]
134    pub fn list(&self, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
135        list_refs_for_repository(self.repo, prefix)
136    }
137
138    /// List refs matching `query` (patterns, sort, limit) through this repository's ref store.
139    ///
140    /// # Errors
141    ///
142    /// Returns [`RefQueryError`] when the ref store cannot be read, when creator-date sort
143    /// is requested without object data, or when an object read fails.
144    pub fn query(
145        &self,
146        query: &RefQuery,
147        options: &RefQueryOptions,
148    ) -> std::result::Result<Vec<RefListing>, RefQueryError> {
149        query_refs(self.repo, query, options)
150    }
151
152    /// Point `name` at `oid`, creating parent directories as needed.
153    ///
154    /// Uses the [`RefStore`](crate::refs::store::RefStore) installed on this repository (including
155    /// injected in-memory stores), not a fresh open from `git_dir` alone.
156    ///
157    /// `oid` must be stored in this repository, and must be a commit when `name` is a branch
158    /// (`refs/heads/…`) or `HEAD`. This catches a commit that a concurrent prune deleted after
159    /// you wrote it, instead of leaving the ref pointing at a missing object.
160    ///
161    /// # Errors
162    ///
163    /// - [`RefStoreError::MissingObject`](crate::refs::store::RefStoreError::MissingObject)
164    ///   when the repository does not store `oid`.
165    /// - [`RefStoreError::NotACommit`](crate::refs::store::RefStoreError::NotACommit) when
166    ///   `name` is a branch or `HEAD` and `oid` is not a commit.
167    /// - Validation, locking, and I/O errors from the ref backend.
168    ///
169    /// The two object errors are returned as [`crate::error::Error::RefStore`]. The ref is
170    /// unchanged on error.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// # fn main() -> grit_lib::error::Result<()> {
176    /// use grit_lib::refs::store::RefName;
177    /// use grit_lib::repo::{InitOptions, Repository};
178    /// use grit_lib::ident::Signature;
179    /// use grit_lib::porcelain::commit::{create_commit, CommitRequest};
180    /// use grit_lib::progress::NullProgress;
181    /// let dir = std::env::temp_dir().join("grit-refs-write-doc");
182    /// let repo = Repository::init(&dir, &InitOptions::default())?;
183    /// let mut req = CommitRequest::new("doc");
184    /// req.author = Some(Signature::new("Doc", "doc@example.com", repo.identity_now())?);
185    /// req.committer = Some(Signature::new("Doc", "doc@example.com", repo.identity_now())?);
186    /// req.allow_empty = true;
187    /// let commit = create_commit(&repo, &req, &mut NullProgress, &grit_lib::progress::NeverCancel)?.oid;
188    /// let name = RefName::try_from("refs/heads/handle-demo").expect("valid ref");
189    /// repo.references().update(&name, &commit)?;
190    /// assert_eq!(repo.references().resolve(&name)?, commit);
191    /// # Ok(())
192    /// # }
193    /// ```
194    #[inline]
195    pub fn update(&self, name: &RefName, oid: &ObjectId) -> Result<()> {
196        update_direct_ref_on_store(self.repo.refs(), self.repo.git_dir(), name.as_str(), oid)
197    }
198
199    /// Read reflog entries for `name` (oldest first).
200    ///
201    /// # Errors
202    ///
203    /// Propagates reflog parse and backend errors.
204    #[inline]
205    pub fn reflog(&self, name: &RefName) -> Result<Vec<ReflogEntry>> {
206        let mut entries = Vec::new();
207        self.repo
208            .refs()
209            .for_each_reflog_entry(name.as_str(), false, &mut |entry| {
210                entries.push(entry.clone());
211                ControlFlow::Continue(())
212            })?;
213        Ok(entries)
214    }
215
216    /// Pack loose refs into `packed-refs` (files backend) or compact reftable tables.
217    ///
218    /// # Errors
219    ///
220    /// Propagates ref-store, locking, and I/O failures.
221    pub fn pack(
222        &self,
223        opts: &crate::maintenance::PackRefsOptions,
224    ) -> Result<crate::maintenance::PackRefsReport> {
225        crate::maintenance::pack_refs(self.repo, opts)
226    }
227}