git_quick_add/git/
commit.rs1use dialoguer::Input;
2use git2::Repository;
3use regex::Regex;
4use std::process::{Command, Stdio};
5use std::str;
6
7pub fn commit(repo: &Repository) {
8 let commit_message: String = Input::new()
11 .with_prompt("Enter Commit Message")
12 .interact_text()
13 .unwrap();
14 let branch_name = repo
16 .head()
17 .ok()
18 .and_then(|head| head.shorthand().map(str::to_owned))
19 .unwrap_or_else(|| "HEAD".to_string());
20 let branch_segment = branch_name.rsplit('/').next().unwrap_or(&branch_name);
21 let reference_id = Regex::new(r"^([^\d]*)(\d+)")
22 .unwrap()
23 .captures(branch_segment)
24 .map(|captures| format!("{}{}", &captures[1], &captures[2]))
25 .unwrap_or(branch_name);
26 let full_commit_message = format!("{reference_id}: {commit_message}");
29 let mut index = repo.index().unwrap();
30 let tree_oid = index.write_tree().unwrap();
31 let tree = repo.find_tree(tree_oid).unwrap();
32 let signature = repo.signature().unwrap();
33 let parent_commit = repo
34 .head()
35 .ok()
36 .and_then(|head| head.target())
37 .and_then(|oid| repo.find_commit(oid).ok());
38 let parents = parent_commit.iter().collect::<Vec<_>>();
39 let commit_result = repo
40 .commit_create_buffer(
41 &signature,
42 &signature,
43 &full_commit_message,
44 &tree,
45 &parents,
46 )
47 .ok()
48 .and_then(|commit_buffer| {
49 let commit_content = str::from_utf8(&commit_buffer).ok()?;
50 let signed_commit = sign_commit_buffer(repo, commit_content).ok()?;
51 let commit_oid = repo.commit_signed(commit_content, &signed_commit, None).ok()?;
52 update_head_to_commit(repo, commit_oid, &full_commit_message).ok()?;
53 Some(commit_oid)
54 });
55
56 if commit_result.is_some() {
57 println!(
58 "{}",
59 console::style("Signed commit created successfully").green()
60 );
61 } else {
62 repo.commit(
63 Some("HEAD"),
64 &signature,
65 &signature,
66 &full_commit_message,
67 &tree,
68 &parents,
69 )
70 .unwrap();
71 println!(
72 "{}",
73 console::style("Commit created successfully (unsigned)").yellow()
74 );
75 }
76
77 println!("Commit message: {full_commit_message}");
78 }
80
81fn sign_commit_buffer(repo: &Repository, commit_content: &str) -> Result<String, git2::Error> {
82 let config = repo.config()?;
83 let signing_key = config.get_string("user.signingkey").ok();
84 let gpg_program = config
85 .get_string("gpg.program")
86 .unwrap_or_else(|_| "gpg".to_string());
87
88 let mut command = Command::new(gpg_program);
89 command.arg("--armor").arg("--detach-sign");
90
91 if let Some(signing_key) = signing_key.as_deref() {
92 command.arg("--local-user").arg(signing_key);
93 }
94
95 let mut child = command
96 .stdin(Stdio::piped())
97 .stdout(Stdio::piped())
98 .spawn()
99 .map_err(|err| git2::Error::from_str(&format!("failed to start gpg: {err}")))?;
100
101 {
102 let mut stdin = child
103 .stdin
104 .take()
105 .ok_or_else(|| git2::Error::from_str("failed to open gpg stdin"))?;
106 use std::io::Write;
107 stdin
108 .write_all(commit_content.as_bytes())
109 .map_err(|err| git2::Error::from_str(&format!("failed to write commit to gpg: {err}")))?;
110 }
111
112 let output = child
113 .wait_with_output()
114 .map_err(|err| git2::Error::from_str(&format!("failed to wait for gpg: {err}")))?;
115
116 if !output.status.success() {
117 let stderr = String::from_utf8_lossy(&output.stderr);
118 return Err(git2::Error::from_str(&format!(
119 "gpg failed to sign commit: {}",
120 stderr.trim()
121 )));
122 }
123
124 String::from_utf8(output.stdout)
125 .map_err(|err| git2::Error::from_str(&format!("invalid gpg output: {err}")))
126}
127
128fn update_head_to_commit(
129 repo: &Repository,
130 commit_oid: git2::Oid,
131 reflog_message: &str,
132) -> Result<(), git2::Error> {
133 let head = repo.head()?;
134
135 match head.resolve() {
136 Ok(mut direct_ref) => {
137 direct_ref.set_target(commit_oid, reflog_message)?;
138 Ok(())
139 }
140 Err(_) => repo.set_head_detached(commit_oid),
141 }
142}