1 use super::{get_head, utils::repo, CommitId};
2 use crate::error::Result;
3 use git2::{ErrorCode, ObjectType, Repository, Signature};
4 use scopetime::scope_time;
5 
6 ///
amend( repo_path: &str, id: CommitId, msg: &str, ) -> Result<CommitId>7 pub fn amend(
8     repo_path: &str,
9     id: CommitId,
10     msg: &str,
11 ) -> Result<CommitId> {
12     scope_time!("amend");
13 
14     let repo = repo(repo_path)?;
15     let commit = repo.find_commit(id.into())?;
16 
17     let mut index = repo.index()?;
18     let tree_id = index.write_tree()?;
19     let tree = repo.find_tree(tree_id)?;
20 
21     let new_id = commit.amend(
22         Some("HEAD"),
23         None,
24         None,
25         None,
26         Some(msg),
27         Some(&tree),
28     )?;
29 
30     Ok(CommitId::new(new_id))
31 }
32 
33 /// Wrap Repository::signature to allow unknown user.name.
34 ///
35 /// See <https://github.com/extrawurst/gitui/issues/79>.
signature_allow_undefined_name( repo: &Repository, ) -> std::result::Result<Signature<'_>, git2::Error>36 fn signature_allow_undefined_name(
37     repo: &Repository,
38 ) -> std::result::Result<Signature<'_>, git2::Error> {
39     match repo.signature() {
40         Err(e) if e.code() == ErrorCode::NotFound => {
41             let config = repo.config()?;
42             Signature::now(
43                 config.get_str("user.name").unwrap_or("unknown"),
44                 config.get_str("user.email")?,
45             )
46         }
47 
48         v => v,
49     }
50 }
51 
52 /// this does not run any git hooks
commit(repo_path: &str, msg: &str) -> Result<CommitId>53 pub fn commit(repo_path: &str, msg: &str) -> Result<CommitId> {
54     scope_time!("commit");
55 
56     let repo = repo(repo_path)?;
57 
58     let signature = signature_allow_undefined_name(&repo)?;
59     let mut index = repo.index()?;
60     let tree_id = index.write_tree()?;
61     let tree = repo.find_tree(tree_id)?;
62 
63     let parents = if let Ok(id) = get_head(repo_path) {
64         vec![repo.find_commit(id.into())?]
65     } else {
66         Vec::new()
67     };
68 
69     let parents = parents.iter().collect::<Vec<_>>();
70 
71     Ok(repo
72         .commit(
73             Some("HEAD"),
74             &signature,
75             &signature,
76             msg,
77             &tree,
78             parents.as_slice(),
79         )?
80         .into())
81 }
82 
83 /// Tag a commit.
84 ///
85 /// This function will return an `Err(…)` variant if the tag’s name is refused
86 /// by git or if the tag already exists.
tag( repo_path: &str, commit_id: &CommitId, tag: &str, ) -> Result<CommitId>87 pub fn tag(
88     repo_path: &str,
89     commit_id: &CommitId,
90     tag: &str,
91 ) -> Result<CommitId> {
92     scope_time!("tag");
93 
94     let repo = repo(repo_path)?;
95 
96     let signature = signature_allow_undefined_name(&repo)?;
97     let object_id = commit_id.get_oid();
98     let target =
99         repo.find_object(object_id, Some(ObjectType::Commit))?;
100 
101     Ok(repo.tag(tag, &target, &signature, "", false)?.into())
102 }
103 
104 #[cfg(test)]
105 mod tests {
106 
107     use crate::error::Result;
108     use crate::sync::{
109         commit, get_commit_details, get_commit_files, stage_add_file,
110         tags::get_tags,
111         tests::{get_statuses, repo_init, repo_init_empty},
112         utils::get_head,
113         LogWalker,
114     };
115     use commit::{amend, tag};
116     use git2::Repository;
117     use std::{fs::File, io::Write, path::Path};
118 
count_commits(repo: &Repository, max: usize) -> usize119     fn count_commits(repo: &Repository, max: usize) -> usize {
120         let mut items = Vec::new();
121         let mut walk = LogWalker::new(&repo);
122         walk.read(&mut items, max).unwrap();
123         items.len()
124     }
125 
126     #[test]
test_commit()127     fn test_commit() {
128         let file_path = Path::new("foo");
129         let (_td, repo) = repo_init().unwrap();
130         let root = repo.path().parent().unwrap();
131         let repo_path = root.as_os_str().to_str().unwrap();
132 
133         File::create(&root.join(file_path))
134             .unwrap()
135             .write_all(b"test\nfoo")
136             .unwrap();
137 
138         assert_eq!(get_statuses(repo_path), (1, 0));
139 
140         stage_add_file(repo_path, file_path).unwrap();
141 
142         assert_eq!(get_statuses(repo_path), (0, 1));
143 
144         commit(repo_path, "commit msg").unwrap();
145 
146         assert_eq!(get_statuses(repo_path), (0, 0));
147     }
148 
149     #[test]
test_commit_in_empty_repo()150     fn test_commit_in_empty_repo() {
151         let file_path = Path::new("foo");
152         let (_td, repo) = repo_init_empty().unwrap();
153         let root = repo.path().parent().unwrap();
154         let repo_path = root.as_os_str().to_str().unwrap();
155 
156         assert_eq!(get_statuses(repo_path), (0, 0));
157 
158         File::create(&root.join(file_path))
159             .unwrap()
160             .write_all(b"test\nfoo")
161             .unwrap();
162 
163         assert_eq!(get_statuses(repo_path), (1, 0));
164 
165         stage_add_file(repo_path, file_path).unwrap();
166 
167         assert_eq!(get_statuses(repo_path), (0, 1));
168 
169         commit(repo_path, "commit msg").unwrap();
170 
171         assert_eq!(get_statuses(repo_path), (0, 0));
172     }
173 
174     #[test]
test_amend() -> Result<()>175     fn test_amend() -> Result<()> {
176         let file_path1 = Path::new("foo");
177         let file_path2 = Path::new("foo2");
178         let (_td, repo) = repo_init_empty()?;
179         let root = repo.path().parent().unwrap();
180         let repo_path = root.as_os_str().to_str().unwrap();
181 
182         File::create(&root.join(file_path1))?.write_all(b"test1")?;
183 
184         stage_add_file(repo_path, file_path1)?;
185         let id = commit(repo_path, "commit msg")?;
186 
187         assert_eq!(count_commits(&repo, 10), 1);
188 
189         File::create(&root.join(file_path2))?.write_all(b"test2")?;
190 
191         stage_add_file(repo_path, file_path2)?;
192 
193         let new_id = amend(repo_path, id, "amended")?;
194 
195         assert_eq!(count_commits(&repo, 10), 1);
196 
197         let details = get_commit_details(repo_path, new_id)?;
198         assert_eq!(details.message.unwrap().subject, "amended");
199 
200         let files = get_commit_files(repo_path, new_id)?;
201 
202         assert_eq!(files.len(), 2);
203 
204         let head = get_head(repo_path)?;
205 
206         assert_eq!(head, new_id);
207 
208         Ok(())
209     }
210 
211     #[test]
test_tag() -> Result<()>212     fn test_tag() -> Result<()> {
213         let file_path = Path::new("foo");
214         let (_td, repo) = repo_init_empty().unwrap();
215         let root = repo.path().parent().unwrap();
216         let repo_path = root.as_os_str().to_str().unwrap();
217 
218         File::create(&root.join(file_path))?
219             .write_all(b"test\nfoo")?;
220 
221         stage_add_file(repo_path, file_path)?;
222 
223         let new_id = commit(repo_path, "commit msg")?;
224 
225         tag(repo_path, &new_id, "tag")?;
226 
227         assert_eq!(
228             get_tags(repo_path).unwrap()[&new_id],
229             vec!["tag"]
230         );
231 
232         assert!(matches!(tag(repo_path, &new_id, "tag"), Err(_)));
233 
234         assert_eq!(
235             get_tags(repo_path).unwrap()[&new_id],
236             vec!["tag"]
237         );
238 
239         tag(repo_path, &new_id, "second-tag")?;
240 
241         assert_eq!(
242             get_tags(repo_path).unwrap()[&new_id],
243             vec!["second-tag", "tag"]
244         );
245 
246         Ok(())
247     }
248 }
249