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