Skip to main content

rskit_git/embedded/
manage.rs

1//! Management trait implementations for the libgit2 repository.
2
3use 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: Option<&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 let Some(message) = message {
124            let signature = self.repo.signature().map_err(GitError::Internal)?;
125            self.repo
126                .tag(name, &obj, &signature, message, false)
127                .map_err(|err| map_exists_error("tag", name, err))?;
128        } else {
129            self.repo
130                .reference(
131                    &format!("refs/tags/{name}"),
132                    obj.id(),
133                    false,
134                    "create lightweight tag",
135                )
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}