Skip to main content

endringer_git/
backend.rs

1//! [`GitBackend`] — wraps `gix::ThreadSafeRepository` and implements [`VcsBackend`].
2
3use std::time::SystemTime;
4
5use anyhow::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) -> 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
45impl VcsBackend for GitBackend {
46    fn status_digest(&self) -> Result<StatusDigest> {
47        commit::status_digest(&repo!(self))
48    }
49
50    fn local_branches(&self) -> Result<Vec<BranchInfo>> {
51        branch::local_branches(&repo!(self))
52    }
53
54    fn remote_branches(&self) -> Result<Vec<BranchInfo>> {
55        branch::remote_branches(&repo!(self))
56    }
57
58    fn list_commits(&self) -> Result<Vec<CommitInfo>> {
59        branch::list_commits(&repo!(self))
60    }
61
62    fn list_commits_sorted(&self, order: SortOrder) -> Result<Vec<CommitInfo>> {
63        branch::list_commits_sorted(&repo!(self), order)
64    }
65
66    fn log_since(&self, since: SystemTime, until: SystemTime) -> Result<Vec<CommitInfo>> {
67        branch::log_since(&repo!(self), since, until)
68    }
69
70    fn find_commit(&self, id: &CommitId) -> Result<CommitInfo> {
71        branch::find_commit(&repo!(self), id)
72    }
73
74    fn list_tags(&self) -> Result<Vec<TagInfo>> {
75        tag::list_tags(&repo!(self))
76    }
77
78    fn list_tags_sorted(&self, order: SortOrder) -> Result<Vec<TagInfo>> {
79        tag::list_tags_sorted(&repo!(self), order)
80    }
81
82    fn create_tag(&self, name: &str) -> Result<()> {
83        tag::create_tag(&repo!(self), name)
84    }
85
86    fn create_annotated_tag(&self, name: &str, message: &str) -> Result<()> {
87        tag::create_annotated_tag(&repo!(self), name, message)
88    }
89
90    fn delete_tag(&self, name: &str) -> Result<()> {
91        tag::delete_tag(&repo!(self), name)
92    }
93
94    fn diff(&self, from: &CommitId, to: &CommitId) -> Result<DiffSummary> {
95        diff::diff(&repo!(self), from, to)
96    }
97
98    fn remote_url(&self, name: &str) -> Option<String> {
99        let repo = repo!(self);
100        let remote = repo.find_remote(name).ok()?;
101        let url = remote.url(gix::remote::Direction::Fetch)?;
102        Some(url.to_bstring().to_string())
103    }
104
105    fn is_dirty(&self) -> Result<bool> {
106        status::is_dirty(&repo!(self))
107    }
108
109    fn merge_base(&self, a: &CommitId, b: &CommitId) -> Result<Option<CommitId>> {
110        graph::merge_base(&repo!(self), a, b)
111    }
112
113    fn is_ancestor(&self, candidate: &CommitId, descendant: &CommitId) -> Result<bool> {
114        graph::is_ancestor(&repo!(self), candidate, descendant)
115    }
116
117    fn ahead_behind(&self, local: &CommitId, upstream: &CommitId) -> Result<AheadBehind> {
118        graph::ahead_behind(&repo!(self), local, upstream)
119    }
120
121    fn branch_ahead_behind(&self, branch: &str) -> Result<Option<AheadBehind>> {
122        graph::branch_ahead_behind(&repo!(self), branch)
123    }
124
125    fn repository_info(&self) -> Result<RepositoryInfo> {
126        info::repository_info(&repo!(self), endringer_core::types::BackendKind::Git)
127    }
128
129    fn branch_tracking(&self, branch: &str) -> Result<BranchTrackingInfo> {
130        branch::branch_tracking(&repo!(self), branch)
131    }
132
133    fn local_branch_tracking(&self) -> Result<Vec<BranchTrackingInfo>> {
134        branch::local_branch_tracking(&repo!(self))
135    }
136
137    fn is_merged_into(&self, b: &str, target: &str) -> Result<bool> {
138        branch::is_merged_into(&repo!(self), b, target)
139    }
140
141    fn blame(&self, path: &std::path::Path) -> Result<Vec<BlameEntry>> {
142        blame::blame(&repo!(self), path)
143    }
144
145    fn worktree_status(&self) -> Result<WorktreeStatus> {
146        status::worktree_status(&repo!(self))
147    }
148
149    fn file_at_commit(&self, path: &std::path::Path, commit_id: &CommitId) -> Result<Vec<u8>> {
150        object::file_at_commit(&repo!(self), path, commit_id)
151    }
152
153    fn submodules(&self) -> Result<Vec<SubmoduleInfo>> {
154        submodule::submodules(&repo!(self))
155    }
156
157    fn stash_entries(&self) -> Result<Vec<StashEntry>> {
158        stash::stash_entries(&repo!(self))
159    }
160
161    fn worktrees(&self) -> Result<Vec<WorktreeInfo>> {
162        worktree::worktrees(&repo!(self))
163    }
164}