Skip to main content

git_quick_add/git/
commit.rs

1use dialoguer::Input;
2use git2::Repository;
3use regex::Regex;
4use std::process::{Command, Stdio};
5use std::str;
6
7pub fn commit(repo: &Repository) {
8    // 0: We have our staged changes
9    // 1: Prompt the user for a commit message
10    let commit_message: String = Input::new()
11        .with_prompt("Enter Commit Message")
12        .interact_text()
13        .unwrap();
14    // 2: Get branch id
15    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    // 3: Create the commit
27    // git commit -S -m "{reference_id}: {commit_message}"
28    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            repo.commit_signed(commit_content, &signed_commit, None).ok()
52        });
53
54    if commit_result.is_some() {
55        println!(
56            "{}",
57            console::style("Signed commit created successfully").green()
58        );
59    } else {
60        repo.commit(
61            Some("HEAD"),
62            &signature,
63            &signature,
64            &full_commit_message,
65            &tree,
66            &parents,
67        )
68        .unwrap();
69        println!(
70            "{}",
71            console::style("Commit created successfully (unsigned)").yellow()
72        );
73    }
74
75    println!("Commit message: {full_commit_message}");
76    // 5: Optional push
77}
78
79fn sign_commit_buffer(repo: &Repository, commit_content: &str) -> Result<String, git2::Error> {
80    let config = repo.config()?;
81    let signing_key = config.get_string("user.signingkey").ok();
82    let gpg_program = config
83        .get_string("gpg.program")
84        .unwrap_or_else(|_| "gpg".to_string());
85
86    let mut command = Command::new(gpg_program);
87    command.arg("--armor").arg("--detach-sign");
88
89    if let Some(signing_key) = signing_key.as_deref() {
90        command.arg("--local-user").arg(signing_key);
91    }
92
93    let mut child = command
94        .stdin(Stdio::piped())
95        .stdout(Stdio::piped())
96        .spawn()
97        .map_err(|err| git2::Error::from_str(&format!("failed to start gpg: {err}")))?;
98
99    {
100        let mut stdin = child
101            .stdin
102            .take()
103            .ok_or_else(|| git2::Error::from_str("failed to open gpg stdin"))?;
104        use std::io::Write;
105        stdin
106            .write_all(commit_content.as_bytes())
107            .map_err(|err| git2::Error::from_str(&format!("failed to write commit to gpg: {err}")))?;
108    }
109
110    let output = child
111        .wait_with_output()
112        .map_err(|err| git2::Error::from_str(&format!("failed to wait for gpg: {err}")))?;
113
114    if !output.status.success() {
115        let stderr = String::from_utf8_lossy(&output.stderr);
116        return Err(git2::Error::from_str(&format!(
117            "gpg failed to sign commit: {}",
118            stderr.trim()
119        )));
120    }
121
122    String::from_utf8(output.stdout)
123        .map_err(|err| git2::Error::from_str(&format!("invalid gpg output: {err}")))
124}