Skip to main content

rskit_git/embedded/
write.rs

1//! Write trait implementations for the libgit2 repository.
2
3use std::path::Path;
4use std::time::{Duration, SystemTime};
5
6use rskit_errors::AppResult;
7
8use crate::error::GitError;
9use crate::options::CommitOptions;
10use crate::types::{EntryState, Oid, Signature, StatusEntry};
11use crate::write::{Committer, IndexManager};
12
13use super::{Git2Repository, oid_from_git2};
14
15impl IndexManager for Git2Repository {
16    fn stage(&self, paths: &[&str]) -> AppResult<()> {
17        if self.repo.is_bare() {
18            return Err(GitError::NotImplemented {
19                operation: "stage on bare repository",
20            }
21            .into());
22        }
23        let mut index = self.repo.index().map_err(GitError::Internal)?;
24        for path in paths {
25            let p = Path::new(path);
26            // Reject absolute paths and paths with parent (..) components
27            if p.is_absolute() || p.components().any(|c| c == std::path::Component::ParentDir) {
28                return Err(GitError::InvalidPath {
29                    path: path.to_string(),
30                }
31                .into());
32            }
33            if self.root.join(p).exists() {
34                index.add_path(p).map_err(GitError::Internal)?;
35            } else {
36                index.remove_path(p).map_err(GitError::Internal)?;
37            }
38        }
39        index.write().map_err(GitError::Internal)?;
40        Ok(())
41    }
42
43    fn unstage(&self, paths: &[&str]) -> AppResult<()> {
44        if paths.is_empty() {
45            return Ok(());
46        }
47
48        let head = self.repo.revparse_single("HEAD").ok();
49        self.repo
50            .reset_default(head.as_ref(), paths.iter().copied())
51            .map_err(GitError::Internal)?;
52        Ok(())
53    }
54
55    fn staged_entries(&self) -> AppResult<Vec<StatusEntry>> {
56        let mut opts = git2::StatusOptions::new();
57        opts.include_untracked(false)
58            .include_ignored(false)
59            .recurse_untracked_dirs(false)
60            .renames_head_to_index(true);
61
62        let statuses = self
63            .repo
64            .statuses(Some(&mut opts))
65            .map_err(GitError::Internal)?;
66        let mut entries = Vec::new();
67
68        for entry in statuses.iter() {
69            let status = entry.status();
70            let path = entry.path().map_err(GitError::Internal)?;
71
72            if status.is_conflicted() {
73                entries.push(StatusEntry {
74                    path: path.to_string(),
75                    state: EntryState::Conflicted,
76                });
77            } else if is_index_status(status) {
78                entries.push(StatusEntry {
79                    path: path.to_string(),
80                    state: EntryState::Staged,
81                });
82            }
83        }
84
85        Ok(entries)
86    }
87}
88
89impl Committer for Git2Repository {
90    fn commit(&self, message: &str, opts: Option<&CommitOptions>) -> AppResult<Oid> {
91        let opts = opts.cloned().unwrap_or_default();
92        if opts.sign {
93            return Err(GitError::SigningNotSupported.into());
94        }
95
96        let mut index = self.repo.index().map_err(GitError::Internal)?;
97        let tree_id = index.write_tree().map_err(GitError::Internal)?;
98        let tree = self.repo.find_tree(tree_id).map_err(GitError::Internal)?;
99
100        let oid = if opts.amend {
101            let head = self.resolve_commit("HEAD")?;
102            let author = opts.author.as_ref().map(signature_to_git2).transpose()?;
103            let committer = match opts.committer.as_ref() {
104                Some(signature) => Some(signature_to_git2(signature)?),
105                None => Some(self.repo.signature().map_err(GitError::Internal)?),
106            };
107
108            head.amend(
109                Some("HEAD"),
110                author.as_ref(),
111                committer.as_ref(),
112                None,
113                Some(message),
114                Some(&tree),
115            )
116            .map_err(GitError::Internal)?
117        } else {
118            let author = match opts.author.as_ref() {
119                Some(signature) => signature_to_git2(signature)?,
120                None => self.repo.signature().map_err(GitError::Internal)?,
121            };
122            let committer = match opts.committer.as_ref() {
123                Some(signature) => signature_to_git2(signature)?,
124                None => self.repo.signature().map_err(GitError::Internal)?,
125            };
126
127            let parents = self
128                .resolve_commit("HEAD")
129                .ok()
130                .into_iter()
131                .collect::<Vec<_>>();
132            let parent_refs = parents.iter().collect::<Vec<_>>();
133
134            self.repo
135                .commit(
136                    Some("HEAD"),
137                    &author,
138                    &committer,
139                    message,
140                    &tree,
141                    &parent_refs,
142                )
143                .map_err(GitError::Internal)?
144        };
145
146        Ok(oid_from_git2(oid))
147    }
148}
149
150fn signature_to_git2(signature: &Signature) -> AppResult<git2::Signature<'static>> {
151    let time = git2_time_from_system_time(signature.when);
152    git2::Signature::new(&signature.name, &signature.email, &time)
153        .map_err(GitError::Internal)
154        .map_err(Into::into)
155}
156
157fn git2_time_from_system_time(time: SystemTime) -> git2::Time {
158    match time.duration_since(SystemTime::UNIX_EPOCH) {
159        Ok(duration) => git2::Time::new(duration.as_secs() as i64, 0),
160        Err(error) => {
161            let duration: Duration = error.duration();
162            git2::Time::new(-(duration.as_secs() as i64), 0)
163        }
164    }
165}
166
167fn is_index_status(status: git2::Status) -> bool {
168    status.intersects(
169        git2::Status::INDEX_NEW
170            | git2::Status::INDEX_MODIFIED
171            | git2::Status::INDEX_DELETED
172            | git2::Status::INDEX_RENAMED
173            | git2::Status::INDEX_TYPECHANGE,
174    )
175}