1use git2::{BranchType, FetchPrune};
4use rskit_errors::{AppError, AppResult};
5
6use crate::error::GitError;
7use crate::manage::{ConfigReader, RefManager, RemoteManager};
8use crate::options::{FetchOptions, PushOptions};
9use crate::types::{Branch, BranchFilter, Remote, Tag};
10
11use super::{Git2Repository, map_remote_error, oid_from_git2, signature_from_git2};
12
13impl RefManager for Git2Repository {
14 fn list_branches(&self, filter: BranchFilter) -> AppResult<Vec<Branch>> {
15 let branch_filter = match filter {
16 BranchFilter::Local => Some(BranchType::Local),
17 BranchFilter::Remote => Some(BranchType::Remote),
18 BranchFilter::All => None,
19 };
20
21 let mut branches = self
22 .repo
23 .branches(branch_filter)
24 .map_err(GitError::Internal)?
25 .map(|item| {
26 let (branch, kind) = item.map_err(GitError::Internal)?;
27 let name = branch
28 .name()
29 .map_err(GitError::Internal)?
30 .unwrap_or_default()
31 .to_string();
32 let target = branch
33 .get()
34 .target()
35 .map(oid_from_git2)
36 .unwrap_or_else(|| oid_from_git2(git2::Oid::ZERO_SHA1));
37 let upstream = if kind == BranchType::Local {
38 branch
39 .upstream()
40 .ok()
41 .and_then(|upstream| upstream.name().ok().flatten().map(str::to_string))
42 } else {
43 None
44 };
45 Ok(Branch {
46 name,
47 target,
48 upstream,
49 })
50 })
51 .collect::<Result<Vec<_>, GitError>>()
52 .map_err::<rskit_errors::AppError, _>(Into::into)?;
53 branches.sort_by(|a, b| a.name.cmp(&b.name));
54 Ok(branches)
55 }
56
57 fn list_tags(&self) -> AppResult<Vec<Tag>> {
58 let mut tags = Vec::new();
59 let names = self.repo.tag_names(None).map_err(GitError::Internal)?;
60 for name in collect_git_strings(names.iter())? {
61 let reference = self
62 .repo
63 .find_reference(&format!("refs/tags/{}", name))
64 .map_err(GitError::Internal)?;
65 let target = reference.target().ok_or_else(|| GitError::RefNotFound {
66 refname: name.clone(),
67 })?;
68
69 if let Ok(tag) = self.repo.find_tag(target) {
70 tags.push(Tag {
71 name: name.clone(),
72 target: oid_from_git2(tag.target_id()),
73 tagger: tag.tagger().map(|sig| signature_from_git2(&sig)),
74 message: tag
75 .message()
76 .ok()
77 .flatten()
78 .unwrap_or_default()
79 .trim_end_matches('\n')
80 .to_string(),
81 });
82 } else {
83 tags.push(Tag {
84 name,
85 target: oid_from_git2(target),
86 tagger: None,
87 message: String::new(),
88 });
89 }
90 }
91 tags.sort_by(|a, b| a.name.cmp(&b.name));
92 Ok(tags)
93 }
94
95 fn create_branch(&self, name: &str, target: &str) -> AppResult<()> {
96 let commit = self.resolve_commit(target)?;
97 self.repo
98 .branch(name, &commit, false)
99 .map_err(|err| map_exists_error("branch", name, err))?;
100 Ok(())
101 }
102
103 fn delete_branch(&self, name: &str) -> AppResult<()> {
104 let mut branch = self
105 .repo
106 .find_branch(name, BranchType::Local)
107 .map_err(|_| GitError::RefNotFound {
108 refname: name.to_string(),
109 })?;
110 branch
111 .delete()
112 .map_err(|err| map_branch_delete_error(name, err))?;
113 Ok(())
114 }
115
116 fn create_tag(&self, name: &str, target: &str, message: &str) -> AppResult<()> {
117 let obj = self
118 .repo
119 .revparse_single(target)
120 .map_err(|_| GitError::RefNotFound {
121 refname: target.to_string(),
122 })?;
123 if message.is_empty() {
124 self.repo
125 .reference(
126 &format!("refs/tags/{name}"),
127 obj.id(),
128 false,
129 "create lightweight tag",
130 )
131 .map_err(|err| map_exists_error("tag", name, err))?;
132 } else {
133 let signature = self.repo.signature().map_err(GitError::Internal)?;
134 self.repo
135 .tag(name, &obj, &signature, message, false)
136 .map_err(|err| map_exists_error("tag", name, err))?;
137 }
138 Ok(())
139 }
140
141 fn delete_tag(&self, name: &str) -> AppResult<()> {
142 self.repo.tag_delete(name).map_err(|e| {
143 if e.code() == git2::ErrorCode::NotFound {
144 GitError::RefNotFound {
145 refname: name.to_string(),
146 }
147 } else {
148 GitError::Internal(e)
149 }
150 })?;
151 Ok(())
152 }
153}
154
155impl RemoteManager for Git2Repository {
156 fn list_remotes(&self) -> AppResult<Vec<Remote>> {
157 let remotes = self.repo.remotes().map_err(GitError::Internal)?;
158 let mut items = Vec::new();
159
160 for name in collect_git_strings(remotes.iter())? {
161 let remote = self.repo.find_remote(&name).map_err(GitError::Internal)?;
162 items.push(Remote {
163 name,
164 url: remote.url().unwrap_or_default().to_string(),
165 fetch_specs: collect_git_strings(
166 remote.fetch_refspecs().map_err(GitError::Internal)?.iter(),
167 )?,
168 push_specs: collect_git_strings(
169 remote.push_refspecs().map_err(GitError::Internal)?.iter(),
170 )?,
171 });
172 }
173
174 items.sort_by(|a, b| a.name.cmp(&b.name));
175 Ok(items)
176 }
177
178 fn fetch(&self, remote: &str, opts: Option<&FetchOptions>) -> AppResult<()> {
179 let mut handle = self
180 .repo
181 .find_remote(remote)
182 .map_err(|_| GitError::RemoteNotFound {
183 name: remote.to_string(),
184 })?;
185 let refspecs = opts
186 .map(|opts| opts.refspecs.iter().map(String::as_str).collect::<Vec<_>>())
187 .unwrap_or_default();
188
189 let mut fetch_opts = opts.map(fetch_options_to_git2).transpose()?;
190 handle
191 .fetch(&refspecs, fetch_opts.as_mut(), None)
192 .map_err(map_remote_error)?;
193 Ok(())
194 }
195
196 fn push(&self, remote: &str, opts: Option<&PushOptions>) -> AppResult<()> {
197 let mut handle = self
198 .repo
199 .find_remote(remote)
200 .map_err(|_| GitError::RemoteNotFound {
201 name: remote.to_string(),
202 })?;
203 let refspecs = push_refspecs(&handle, opts)?;
204 let mut push_opts = git2::PushOptions::new();
205 handle
206 .push(&refspecs, Some(&mut push_opts))
207 .map_err(map_remote_error)?;
208 Ok(())
209 }
210
211 fn tracking_branch(&self, branch: &str) -> AppResult<String> {
212 let branch = self
213 .repo
214 .find_branch(branch, BranchType::Local)
215 .map_err(|_| GitError::RefNotFound {
216 refname: branch.to_string(),
217 })?;
218 let upstream = branch.upstream().map_err(|_| GitError::RefNotFound {
219 refname: format!(
220 "{branch}@{{upstream}}",
221 branch = branch.name().ok().flatten().unwrap_or_default()
222 ),
223 })?;
224 upstream
225 .name()
226 .map_err(GitError::Internal)?
227 .map(str::to_string)
228 .ok_or_else(|| {
229 AppError::invalid_input("branch", "upstream branch name is not valid utf-8")
230 })
231 }
232}
233
234impl ConfigReader for Git2Repository {
235 fn config_get(&self, key: &str) -> AppResult<String> {
236 let config = self.repo.config().map_err(GitError::Internal)?;
237 config
238 .get_string(key)
239 .map_err(|err| map_config_error(key, err))
240 }
241
242 fn config_get_all(&self, key: &str) -> AppResult<Vec<String>> {
243 let config = self.repo.config().map_err(GitError::Internal)?;
244 let mut entries = config
245 .multivar(key, None)
246 .map_err(|err| map_config_error(key, err))?;
247 let mut values = Vec::new();
248
249 while let Some(entry) = entries.next() {
250 let entry = entry.map_err(GitError::Internal)?;
251 let value = entry.value().map_err(GitError::Internal)?;
252 values.push(value.to_string());
253 }
254
255 Ok(values)
256 }
257
258 fn config_set(&self, key: &str, value: &str) -> AppResult<()> {
259 let mut config = self.repo.config().map_err(GitError::Internal)?;
260 config
261 .set_str(key, value)
262 .map_err(|err| map_config_error(key, err))?;
263 Ok(())
264 }
265}
266
267fn fetch_options_to_git2(opts: &FetchOptions) -> AppResult<git2::FetchOptions<'static>> {
268 let mut fetch_opts = git2::FetchOptions::new();
269 fetch_opts.prune(if opts.prune {
270 FetchPrune::On
271 } else {
272 FetchPrune::Off
273 });
274
275 if let Some(depth) = opts.depth {
276 let depth = i32::try_from(depth)
277 .map_err(|_| AppError::invalid_input("depth", "fetch depth exceeds supported range"))?;
278 fetch_opts.depth(depth);
279 }
280
281 Ok(fetch_opts)
282}
283
284fn push_refspecs(remote: &git2::Remote<'_>, opts: Option<&PushOptions>) -> AppResult<Vec<String>> {
285 let mut refspecs = match opts {
286 Some(o) if !o.refspecs.is_empty() => o.refspecs.clone(),
287 _ => collect_git_strings(remote.push_refspecs().map_err(GitError::Internal)?.iter())?,
288 };
289
290 let force = opts.is_some_and(|o| o.force);
291 if force {
292 for refspec in &mut refspecs {
293 if !refspec.starts_with('+') {
294 refspec.insert(0, '+');
295 }
296 }
297 }
298
299 Ok(refspecs)
300}
301
302fn collect_git_strings<'a>(
303 iter: impl IntoIterator<Item = Result<Option<&'a str>, git2::Error>>,
304) -> AppResult<Vec<String>> {
305 let mut values = Vec::new();
306 for item in iter {
307 let Some(value) = item.map_err(GitError::Internal)? else {
308 return Err(AppError::invalid_format("git string array", "utf-8 string"));
309 };
310 values.push(value.to_string());
311 }
312 Ok(values)
313}
314
315fn map_config_error(key: &str, err: git2::Error) -> rskit_errors::AppError {
316 if err.code() == git2::ErrorCode::NotFound {
317 GitError::ConfigNotFound {
318 key: key.to_string(),
319 }
320 .into()
321 } else {
322 GitError::Internal(err).into()
323 }
324}
325
326fn map_exists_error(kind: &'static str, name: &str, err: git2::Error) -> GitError {
327 if err.code() == git2::ErrorCode::Exists {
328 GitError::AlreadyExists {
329 kind,
330 name: name.to_string(),
331 }
332 } else {
333 GitError::Internal(err)
334 }
335}
336
337fn map_branch_delete_error(name: &str, err: git2::Error) -> GitError {
338 if err.code() == git2::ErrorCode::NotFound {
339 GitError::RefNotFound {
340 refname: name.to_string(),
341 }
342 } else if err.message().contains("is checked out") {
343 GitError::CheckedOutBranch {
344 name: name.to_string(),
345 }
346 } else {
347 GitError::Internal(err)
348 }
349}