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