rskit_git/embedded/
write.rs1use 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 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}