1use 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
15pub struct GitBackend {
22 inner: gix::ThreadSafeRepository,
23}
24
25impl GitBackend {
26 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
36macro_rules! repo {
40 ($self:expr) => {
41 $self.inner.to_thread_local()
42 };
43}
44
45macro_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 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}