1use git2::{message_prettify, Commit, ErrorCode, ObjectType, Oid, Repository, Signature};
4
5use serde::{Deserialize, Serialize};
6use serde_json;
7use log::debug;
10use scopetime::scope_time;
11
12use super::{CommitId, RepoPath};
13use crate::{
14 error::{Error, Result},
15 sync::{
16 repository::repo,
17 sign::{SignBuilder, SignError},
18 utils::get_head_repo,
19 },
20};
21
22pub fn amend(repo_path: &RepoPath, id: CommitId, msg: &str) -> Result<CommitId> {
24 scope_time!("amend");
25
26 let repo = repo(repo_path)?;
27 let config = repo.config()?;
28
29 let commit = repo.find_commit(id.into())?;
30
31 let mut index = repo.index()?;
32 let tree_id = index.write_tree()?;
33 let tree = repo.find_tree(tree_id)?;
34
35 if config.get_bool("commit.gpgsign").unwrap_or(false) {
36 use crate::sync::utils::undo_last_commit;
38
39 let head = get_head_repo(&repo)?;
40 if head == commit.id().into() {
41 undo_last_commit(repo_path)?;
42 return self::commit(repo_path, msg);
43 }
44
45 return Err(Error::SignAmendNonLastCommit);
46 }
47
48 let new_id = commit.amend(Some("HEAD"), None, None, None, Some(msg), Some(&tree))?;
49
50 Ok(CommitId::new(new_id))
51}
52
53#[allow(clippy::redundant_pub_crate)]
57pub(crate) fn signature_allow_undefined_name(
58 repo: &Repository,
59) -> std::result::Result<Signature<'_>, git2::Error> {
60 let signature = repo.signature();
61
62 if let Err(ref e) = signature {
63 if e.code() == ErrorCode::NotFound {
64 let config = repo.config()?;
65
66 if let (Err(_), Ok(email_entry)) = (
67 config.get_entry("user.name"),
68 config.get_entry("user.email"),
69 ) {
70 if let Some(email) = email_entry.value() {
71 return Signature::now("unknown", email);
72 }
73 };
74 }
75 }
76
77 signature
78}
79
80#[derive(Serialize, Deserialize, Debug)]
82pub struct SerializableCommit {
83 id: String,
84 tree: String,
85 parents: Vec<String>,
86 author_name: String,
87 author_email: String,
88 committer_name: String,
89 committer_email: String,
90 message: String,
91 time: i64,
92}
93pub fn serialize_commit(commit: &Commit) -> Result<String> {
95 let id = commit.id().to_string();
96 let tree = commit.tree_id().to_string();
97 let parents = commit.parent_ids().map(|oid| oid.to_string()).collect();
98 let author = commit.author();
99 let committer = commit.committer();
100 let message = commit
101 .message()
102 .ok_or(log::debug!("No commit message"))
103 .expect("")
104 .to_string();
105 log::debug!("message:\n{:?}", message);
106 let time = commit.time().seconds();
107 debug!("time: {:?}", time);
108
109 let serializable_commit = SerializableCommit {
110 id,
111 tree,
112 parents,
113 author_name: author.name().unwrap_or_default().to_string(),
114 author_email: author.email().unwrap_or_default().to_string(),
115 committer_name: committer.name().unwrap_or_default().to_string(),
116 committer_email: committer.email().unwrap_or_default().to_string(),
117 message,
118 time,
119 };
120
121 let serialized = serde_json::to_string(&serializable_commit).expect("");
122 debug!("serialized_commit: {:?}", serialized);
123 Ok(serialized)
124}
125pub fn deserialize_commit<'a>(repo: &'a Repository, data: &'a str) -> Result<Commit<'a>> {
127 let serializable_commit: SerializableCommit = serde_json::from_str(data).expect("");
130 let oid = Oid::from_str(&serializable_commit.id)?;
132 let commit_obj = repo.find_object(oid, Some(ObjectType::Commit))?;
134 let commit = commit_obj.peel_to_commit()?;
136 Ok(commit)
142}
143
144pub fn commit(repo_path: &RepoPath, msg: &str) -> Result<CommitId> {
147 scope_time!("commit");
148
149 let repo = repo(repo_path)?;
150 let config = repo.config()?;
151 let signature = signature_allow_undefined_name(&repo)?;
152 let mut index = repo.index()?;
153 let tree_id = index.write_tree()?;
154 let tree = repo.find_tree(tree_id)?;
155
156 let parents = if let Ok(id) = get_head_repo(&repo) {
157 vec![repo.find_commit(id.into())?]
158 } else {
159 Vec::new()
160 };
161
162 let parents = parents.iter().collect::<Vec<_>>();
163
164 let commit_id = if config.get_bool("commit.gpgsign").unwrap_or(false) {
165 let buffer =
166 repo.commit_create_buffer(&signature, &signature, msg, &tree, parents.as_slice())?;
167
168 let commit = std::str::from_utf8(&buffer)
169 .map_err(|_e| SignError::Shellout("utf8 conversion error".to_string()))?;
170
171 let signer = SignBuilder::from_gitconfig(&repo, &config)?;
172 let (signature, signature_field) = signer.sign(&buffer)?;
173 let commit_id = repo.commit_signed(commit, &signature, signature_field.as_deref())?;
174
175 if let Ok(mut head) = repo.head() {
180 head.set_target(commit_id, msg)?;
181 } else {
182 let default_branch_name = config.get_str("init.defaultBranch").unwrap_or("master");
183 repo.reference(
184 &format!("refs/heads/{default_branch_name}"),
185 commit_id,
186 true,
187 msg,
188 )?;
189 }
190
191 commit_id
192 } else {
193 repo.commit(
194 Some("HEAD"),
195 &signature,
196 &signature,
197 msg,
198 &tree,
199 parents.as_slice(),
200 )?
201 };
202
203 Ok(commit_id.into())
204}
205pub fn padded_commit_id(commit_id: String) -> String {
207 format!("{:0>64}", commit_id)
208}
209pub fn tag_commit(
214 repo_path: &RepoPath,
215 commit_id: &CommitId,
216 tag: &str,
217 message: Option<&str>,
218) -> Result<CommitId> {
219 scope_time!("tag_commit");
220
221 let repo = repo(repo_path)?;
222
223 let object_id = commit_id.get_oid();
224 let target = repo.find_object(object_id, Some(ObjectType::Commit))?;
225
226 let c = if let Some(message) = message {
227 let signature = signature_allow_undefined_name(&repo)?;
228 repo.tag(tag, &target, &signature, message, false)?.into()
229 } else {
230 repo.tag_lightweight(tag, &target, false)?.into()
231 };
232
233 Ok(c)
234}
235
236pub fn commit_message_prettify(repo_path: &RepoPath, message: String) -> Result<String> {
239 let comment_char = repo(repo_path)?
240 .config()?
241 .get_string("core.commentChar")
242 .ok()
243 .and_then(|char_string| char_string.chars().next())
244 .unwrap_or('#') as u8;
245
246 Ok(message_prettify(message, Some(comment_char))?)
247}
248
249#[cfg(test)]
250mod tests {
251 use std::{fs::File, io::Write, path::Path};
252
253 use commit::{amend, commit_message_prettify, tag_commit};
254 use git2::Repository;
255
256 use crate::{
257 error::Result,
258 sync::{
259 commit, get_commit_details, get_commit_files, stage_add_file,
260 tags::{get_tags, Tag},
261 tests::{get_statuses, repo_init, repo_init_empty},
262 utils::get_head,
263 LogWalker, RepoPath,
264 },
265 };
266
267 fn count_commits(repo: &Repository, max: usize) -> usize {
268 let mut items = Vec::new();
269 let mut walk = LogWalker::new(repo, max).unwrap();
270 walk.read(&mut items).unwrap();
271 items.len()
272 }
273
274 #[test]
275 fn test_commit() {
276 let file_path = Path::new("foo");
277 let (_td, repo) = repo_init().unwrap();
278 let root = repo.path().parent().unwrap();
279 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
280
281 File::create(root.join(file_path))
282 .unwrap()
283 .write_all(b"test\nfoo")
284 .unwrap();
285
286 assert_eq!(get_statuses(repo_path), (1, 0));
287
288 stage_add_file(repo_path, file_path).unwrap();
289
290 assert_eq!(get_statuses(repo_path), (0, 1));
291
292 commit(repo_path, "commit msg").unwrap();
293
294 assert_eq!(get_statuses(repo_path), (0, 0));
295 }
296
297 #[test]
298 fn test_commit_in_empty_repo() {
299 let file_path = Path::new("foo");
300 let (_td, repo) = repo_init_empty().unwrap();
301 let root = repo.path().parent().unwrap();
302 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
303
304 assert_eq!(get_statuses(repo_path), (0, 0));
305
306 File::create(root.join(file_path))
307 .unwrap()
308 .write_all(b"test\nfoo")
309 .unwrap();
310
311 assert_eq!(get_statuses(repo_path), (1, 0));
312
313 stage_add_file(repo_path, file_path).unwrap();
314
315 assert_eq!(get_statuses(repo_path), (0, 1));
316
317 commit(repo_path, "commit msg").unwrap();
318
319 assert_eq!(get_statuses(repo_path), (0, 0));
320 }
321
322 #[test]
323 fn test_amend() -> Result<()> {
324 let file_path1 = Path::new("foo");
325 let file_path2 = Path::new("foo2");
326 let (_td, repo) = repo_init_empty()?;
327 let root = repo.path().parent().unwrap();
328 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
329
330 File::create(root.join(file_path1))?.write_all(b"test1")?;
331
332 stage_add_file(repo_path, file_path1)?;
333 let id = commit(repo_path, "commit msg")?;
334
335 assert_eq!(count_commits(&repo, 10), 1);
336
337 File::create(root.join(file_path2))?.write_all(b"test2")?;
338
339 stage_add_file(repo_path, file_path2)?;
340
341 let new_id = amend(repo_path, id, "amended")?;
342
343 assert_eq!(count_commits(&repo, 10), 1);
344
345 let details = get_commit_details(repo_path, new_id)?;
346 assert_eq!(details.message.unwrap().subject, "amended");
347
348 let files = get_commit_files(repo_path, new_id, None)?;
349
350 assert_eq!(files.len(), 2);
351
352 let head = get_head(repo_path)?;
353
354 assert_eq!(head, new_id);
355
356 Ok(())
357 }
358
359 #[test]
360 fn test_tag() -> Result<()> {
361 let file_path = Path::new("foo");
362 let (_td, repo) = repo_init_empty().unwrap();
363 let root = repo.path().parent().unwrap();
364 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
365
366 File::create(root.join(file_path))?.write_all(b"test\nfoo")?;
367
368 stage_add_file(repo_path, file_path)?;
369
370 let new_id = commit(repo_path, "commit msg")?;
371
372 tag_commit(repo_path, &new_id, "tag", None)?;
373
374 assert_eq!(get_tags(repo_path).unwrap()[&new_id], vec![Tag::new("tag")]);
375
376 assert!(matches!(
377 tag_commit(repo_path, &new_id, "tag", None),
378 Err(_)
379 ));
380
381 assert_eq!(get_tags(repo_path).unwrap()[&new_id], vec![Tag::new("tag")]);
382
383 tag_commit(repo_path, &new_id, "second-tag", None)?;
384
385 assert_eq!(
386 get_tags(repo_path).unwrap()[&new_id],
387 vec![Tag::new("second-tag"), Tag::new("tag")]
388 );
389
390 Ok(())
391 }
392
393 #[test]
394 fn test_tag_with_message() -> Result<()> {
395 let file_path = Path::new("foo");
396 let (_td, repo) = repo_init_empty().unwrap();
397 let root = repo.path().parent().unwrap();
398 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
399
400 File::create(root.join(file_path))?.write_all(b"test\nfoo")?;
401
402 stage_add_file(repo_path, file_path)?;
403
404 let new_id = commit(repo_path, "commit msg")?;
405
406 tag_commit(repo_path, &new_id, "tag", Some("tag-message"))?;
407
408 assert_eq!(
409 get_tags(repo_path).unwrap()[&new_id][0]
410 .annotation
411 .as_ref()
412 .unwrap(),
413 "tag-message"
414 );
415
416 Ok(())
417 }
418
419 #[test]
428 fn test_empty_email() -> Result<()> {
429 let file_path = Path::new("foo");
430 let (_td, repo) = repo_init_empty().unwrap();
431 let root = repo.path().parent().unwrap();
432 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
433
434 File::create(root.join(file_path))?.write_all(b"test\nfoo")?;
435
436 stage_add_file(repo_path, file_path)?;
437
438 repo.config()?.remove("user.email")?;
439
440 let error = commit(repo_path, "commit msg");
441
442 assert!(matches!(error, Err(_)));
443
444 repo.config()?.set_str("user.email", "email")?;
445
446 let success = commit(repo_path, "commit msg");
447
448 assert!(matches!(success, Ok(_)));
449 assert_eq!(count_commits(&repo, 10), 1);
450
451 let details = get_commit_details(repo_path, success.unwrap()).unwrap();
452
453 assert_eq!(details.author.name, "name");
454 assert_eq!(details.author.email, "email");
455
456 Ok(())
457 }
458
459 #[test]
461 fn test_empty_name() -> Result<()> {
462 let file_path = Path::new("foo");
463 let (_td, repo) = repo_init_empty().unwrap();
464 let root = repo.path().parent().unwrap();
465 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
466
467 File::create(root.join(file_path))?.write_all(b"test\nfoo")?;
468
469 stage_add_file(repo_path, file_path)?;
470
471 repo.config()?.remove("user.name")?;
472
473 let mut success = commit(repo_path, "commit msg");
474
475 assert!(matches!(success, Ok(_)));
476 assert_eq!(count_commits(&repo, 10), 1);
477
478 let mut details = get_commit_details(repo_path, success.unwrap()).unwrap();
479
480 assert_eq!(details.author.name, "unknown");
481 assert_eq!(details.author.email, "email");
482
483 repo.config()?.set_str("user.name", "name")?;
484
485 success = commit(repo_path, "commit msg");
486
487 assert!(matches!(success, Ok(_)));
488 assert_eq!(count_commits(&repo, 10), 2);
489
490 details = get_commit_details(repo_path, success.unwrap()).unwrap();
491
492 assert_eq!(details.author.name, "name");
493 assert_eq!(details.author.email, "email");
494
495 Ok(())
496 }
497
498 #[test]
499 fn test_empty_comment_char() -> Result<()> {
500 let (_td, repo) = repo_init_empty().unwrap();
501
502 let root = repo.path().parent().unwrap();
503 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
504
505 let message =
506 commit_message_prettify(repo_path, "#This is a test message\nTest".to_owned())?;
507
508 assert_eq!(message, "Test\n");
509 Ok(())
510 }
511
512 #[test]
513 fn test_with_comment_char() -> Result<()> {
514 let (_td, repo) = repo_init_empty().unwrap();
515
516 let root = repo.path().parent().unwrap();
517 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
518
519 repo.config()?.set_str("core.commentChar", ";")?;
520
521 let message =
522 commit_message_prettify(repo_path, ";This is a test message\nTest".to_owned())?;
523
524 assert_eq!(message, "Test\n");
525
526 Ok(())
527 }
528}