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            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    // 5: Optional push
79}
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}