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