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