Skip to main content

endringer_git/
backend.rs

1//! [`GitBackend`] — wraps `gix::ThreadSafeRepository` and implements [`VcsBackend`].
2
3use std::time::SystemTime;
4
5use endringer_core::error::{anyhow_to_backend, Error as CrateError, NotFoundKind, Result};
6use endringer_core::backend::VcsBackend;
7use endringer_core::types::{
8    AheadBehind, BlameEntry, BranchInfo, BranchTrackingInfo, CommitId, CommitInfo,
9    CommitQuery, CommitQueryResult, ConflictSummary, DiffEntry, DiffOptions,
10    DiffSummary, OperationState, RefInfo, RefKind, RemoteInfo,
11    RepositoryInfo, RichWorktreeStatus,
12    SortOrder, StashDetail, StashEntry, StatusDigest, StatusOptions, SubmoduleInfo,
13    SubmoduleSummary, TagInfo, TreeEntry,
14    WorktreeDetail, WorktreeInfo, WorktreeStatus,
15};
16
17use crate::{blame, branch, commit, conflict, diff, graph, info, object, operation,
18            refs, stash, stash_detail, status, submodule, submodule_summary,
19            tag, tree, worktree, worktree_detail};
20
21/// Git backend.
22///
23/// Uses [`gix::ThreadSafeRepository`] so the struct is natively `Send + Sync`
24/// without any mutex. Each method obtains a cheap thread-local repository view
25/// via [`gix::ThreadSafeRepository::to_thread_local`], eliminating serialization
26/// under concurrent async load.
27pub struct GitBackend {
28    inner: gix::ThreadSafeRepository,
29}
30
31impl GitBackend {
32    /// Opens or discovers a Git repository at or above `path`.
33    ///
34    /// Traverses parent directories until it finds a `.git` directory (or a
35    /// bare repository), so callers may pass any subdirectory of the worktree.
36    pub fn open(path: &std::path::Path) -> anyhow::Result<Self> {
37        let inner = gix::discover(path)?.into_sync();
38        Ok(GitBackend { inner })
39    }
40}
41
42/// Obtains a thread-local [`gix::Repository`] view from the shared handle.
43///
44/// This is a zero-copy operation: no re-opening of files, no locking.
45macro_rules! repo {
46    ($self:expr) => {
47        $self.inner.to_thread_local()
48    };
49}
50
51/// Converts an `anyhow::Result` to `endringer_core::Result` at the dispatch boundary.
52macro_rules! be {
53    ($e:expr) => {
54        $e.map_err(anyhow_to_backend)
55    };
56}
57
58impl VcsBackend for GitBackend {
59    fn status_digest(&self) -> Result<StatusDigest> {
60        be!(commit::status_digest(&repo!(self)))
61    }
62
63    fn local_branches(&self) -> Result<Vec<BranchInfo>> {
64        be!(branch::local_branches(&repo!(self)))
65    }
66
67    fn remote_branches(&self) -> Result<Vec<BranchInfo>> {
68        be!(branch::remote_branches(&repo!(self)))
69    }
70
71    fn list_commits(&self) -> Result<Vec<CommitInfo>> {
72        be!(branch::list_commits(&repo!(self)))
73    }
74
75    fn list_commits_sorted(&self, order: SortOrder) -> Result<Vec<CommitInfo>> {
76        be!(branch::list_commits_sorted(&repo!(self), order))
77    }
78
79    fn log_since(&self, since: SystemTime, until: SystemTime) -> Result<Vec<CommitInfo>> {
80        be!(branch::log_since(&repo!(self), since, until))
81    }
82
83    fn find_commit(&self, id: &CommitId) -> Result<CommitInfo> {
84        branch::find_commit(&repo!(self), id).map_err(|e| {
85            let msg = e.to_string();
86            if msg.contains("not found") || msg.contains("commit '") && msg.contains("not found") {
87                CrateError::NotFound { kind: NotFoundKind::Commit, name: id.to_string() }
88            } else if msg.contains("not a commit") {
89                CrateError::NotACommit { id: id.clone() }
90            } else {
91                anyhow_to_backend(e)
92            }
93        })
94    }
95
96    fn query_commits(&self, query: CommitQuery) -> Result<CommitQueryResult> {
97        be!(branch::query_commits(&repo!(self), query))
98    }
99
100    fn list_tags(&self) -> Result<Vec<TagInfo>> {
101        be!(tag::list_tags(&repo!(self)))
102    }
103
104    fn list_tags_sorted(&self, order: SortOrder) -> Result<Vec<TagInfo>> {
105        be!(tag::list_tags_sorted(&repo!(self), order))
106    }
107
108    fn create_tag(&self, name: &str) -> Result<()> {
109        be!(tag::create_tag(&repo!(self), name))
110    }
111
112    fn create_annotated_tag(&self, name: &str, message: &str) -> Result<()> {
113        be!(tag::create_annotated_tag(&repo!(self), name, message))
114    }
115
116    fn delete_tag(&self, name: &str) -> Result<()> {
117        be!(tag::delete_tag(&repo!(self), name))
118    }
119
120    fn diff(&self, from: &CommitId, to: &CommitId) -> Result<DiffSummary> {
121        be!(diff::diff(&repo!(self), from, to))
122    }
123
124    fn diff_entries(&self, from: &CommitId, to: &CommitId, options: DiffOptions) -> Result<Vec<DiffEntry>> {
125        be!(diff::diff_entries(&repo!(self), from, to, options))
126    }
127
128    fn remote_url(&self, name: &str) -> Result<Option<String>> {
129        let repo = repo!(self);
130        let remote = match repo.find_remote(name) {
131            Ok(r) => r,
132            Err(e) => {
133                let msg = e.to_string();
134                // gix reports "The remote named \"X\" did not exist" for
135                // unknown remotes — treat as absent, not an error.
136                if msg.contains("did not exist")
137                    || msg.contains("not found")
138                    || msg.contains("does not exist")
139                {
140                    return Ok(None);
141                }
142                return Err(anyhow_to_backend(anyhow::anyhow!(msg)));
143            }
144        };
145        let url = remote.url(gix::remote::Direction::Fetch);
146        Ok(url.map(|u| u.to_bstring().to_string()))
147    }
148
149    fn is_dirty(&self) -> Result<bool> {
150        be!(status::is_dirty(&repo!(self)))
151    }
152
153    fn merge_base(&self, a: &CommitId, b: &CommitId) -> Result<Option<CommitId>> {
154        be!(graph::merge_base(&repo!(self), a, b))
155    }
156
157    fn is_ancestor(&self, candidate: &CommitId, descendant: &CommitId) -> Result<bool> {
158        be!(graph::is_ancestor(&repo!(self), candidate, descendant))
159    }
160
161    fn ahead_behind(&self, local: &CommitId, upstream: &CommitId) -> Result<AheadBehind> {
162        be!(graph::ahead_behind(&repo!(self), local, upstream))
163    }
164
165    fn branch_ahead_behind(&self, branch: &str) -> Result<Option<AheadBehind>> {
166        be!(graph::branch_ahead_behind(&repo!(self), branch))
167    }
168
169    fn repository_info(&self) -> Result<RepositoryInfo> {
170        be!(info::repository_info(&repo!(self), endringer_core::types::BackendKind::Git))
171    }
172
173    fn branch_tracking(&self, branch: &str) -> Result<BranchTrackingInfo> {
174        be!(branch::branch_tracking(&repo!(self), branch))
175    }
176
177    fn local_branch_tracking(&self) -> Result<Vec<BranchTrackingInfo>> {
178        be!(branch::local_branch_tracking(&repo!(self)))
179    }
180
181    fn is_merged_into(&self, b: &str, target: &str) -> Result<bool> {
182        be!(branch::is_merged_into(&repo!(self), b, target))
183    }
184
185    fn blame(&self, path: &std::path::Path) -> Result<Vec<BlameEntry>> {
186        be!(blame::blame(&repo!(self), path))
187    }
188
189    fn worktree_status(&self) -> Result<WorktreeStatus> {
190        be!(status::worktree_status(&repo!(self)))
191    }
192
193    fn rich_worktree_status(&self, options: StatusOptions) -> Result<RichWorktreeStatus> {
194        be!(status::rich_worktree_status(&repo!(self), options))
195    }
196
197    fn file_at_commit(&self, path: &std::path::Path, commit_id: &CommitId) -> Result<Vec<u8>> {
198        object::file_at_commit(&repo!(self), path, commit_id).map_err(|e| {
199            let msg = e.to_string();
200            if msg.contains("not found") || msg.contains("does not exist") {
201                CrateError::PathNotFound {
202                    path: path.to_path_buf(),
203                    commit: Some(commit_id.clone()),
204                }
205            } else {
206                anyhow_to_backend(e)
207            }
208        })
209    }
210
211    fn submodules(&self) -> Result<Vec<SubmoduleInfo>> {
212        be!(submodule::submodules(&repo!(self)))
213    }
214
215    fn stash_entries(&self) -> Result<Vec<StashEntry>> {
216        be!(stash::stash_entries(&repo!(self)))
217    }
218
219    fn worktrees(&self) -> Result<Vec<WorktreeInfo>> {
220        be!(worktree::worktrees(&repo!(self)))
221    }
222
223    fn operation_state(&self) -> Result<OperationState> {
224        be!(operation::operation_state(repo!(self).git_dir()))
225    }
226
227    fn unmerged_paths(&self) -> Result<Vec<std::path::PathBuf>> {
228        be!(conflict::unmerged_paths(&repo!(self)))
229    }
230
231    fn conflict_summary(&self) -> Result<ConflictSummary> {
232        be!(conflict::conflict_summary(&repo!(self)))
233    }
234
235    fn blame_at(&self, path: &std::path::Path, commit_id: &CommitId) -> Result<Vec<BlameEntry>> {
236        be!(blame::blame_at(&repo!(self), path, commit_id))
237    }
238
239    fn tree_at_commit(&self, commit_id: &CommitId) -> Result<Vec<TreeEntry>> {
240        be!(tree::tree_at_commit(&repo!(self), commit_id))
241    }
242
243    fn tree_at_path(&self, commit_id: &CommitId, path: &std::path::Path) -> Result<Vec<TreeEntry>> {
244        be!(tree::tree_at_path(&repo!(self), commit_id, path))
245    }
246
247    fn remotes(&self) -> Result<Vec<RemoteInfo>> {
248        be!(refs::remotes(&repo!(self)))
249    }
250
251    fn references(&self) -> Result<Vec<RefInfo>> {
252        be!(refs::references(&repo!(self)))
253    }
254
255    fn references_by_kind(&self, kind: RefKind) -> Result<Vec<RefInfo>> {
256        be!(refs::references_by_kind(&repo!(self), kind))
257    }
258
259    fn submodule_summaries(&self) -> Result<Vec<SubmoduleSummary>> {
260        be!(submodule_summary::submodule_summaries(&repo!(self)))
261    }
262
263    fn stash_detail(&self, index: usize) -> Result<StashDetail> {
264        be!(stash_detail::stash_detail(&repo!(self), index))
265    }
266
267    fn stash_diff(&self, index: usize) -> Result<DiffSummary> {
268        be!(stash_detail::stash_diff(&repo!(self), index))
269    }
270
271    fn worktree_details(&self) -> Result<Vec<WorktreeDetail>> {
272        be!(worktree_detail::worktree_details(&repo!(self)))
273    }
274}