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