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