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}