git_commit_helper/git.rs
1// ************************************************************************** //
2// //
3// ::: :::::::: //
4// git.rs :+: :+: :+: //
5// +:+ +:+ +:+ //
6// By: dfine <coding@dfine.tech> +#+ +:+ +#+ //
7// +#+#+#+#+#+ +#+ //
8// Created: 2025/05/10 19:12:46 by dfine #+# #+# //
9// Updated: 2025/05/10 19:12:48 by dfine ### ########.fr //
10// //
11// ************************************************************************** //
12
13use git2::{DiffOptions, Repository};
14use std::error::Error;
15
16/// Returns the staged diff of the current Git repository (i.e., changes staged for commit).
17///
18/// This compares the staged index against the current `HEAD`.
19///
20/// # Arguments
21///
22/// * `repo` - A reference to an open `git2::Repository` instance.
23///
24/// # Returns
25///
26/// A `String` containing the unified diff. If the diff cannot be generated, it returns `"None"`.
27///
28/// # Example
29///
30/// ```
31/// use git_commit_helper::get_staged_diff;
32/// use git2::Repository;
33///
34/// let repo = Repository::discover(".").expect("Not a git repository");
35/// let diff = get_staged_diff(&repo);
36/// println!("{}", diff);
37/// ```
38pub fn get_staged_diff(repo: &Repository) -> String {
39 let index = repo.index().expect("Can't get index");
40 let tree = repo.head().ok().and_then(|head| head.peel_to_tree().ok());
41 let mut diff_opts = DiffOptions::new();
42 let diff = repo
43 .diff_tree_to_index(tree.as_ref(), Some(&index), Some(&mut diff_opts))
44 .expect("Failed to generate diff");
45 let mut buf = Vec::new();
46 if let Err(e) = diff.print(git2::DiffFormat::Patch, |_d, _h, _l| {
47 buf.extend_from_slice(_l.content());
48 true
49 }) {
50 eprintln!("failed to print diff: {}", e);
51 return String::from("None");
52 }
53 String::from_utf8_lossy(&buf).to_string()
54}
55
56/// Returns the messages of the most recent commits (up to 3).
57///
58/// Useful for providing context to an LLM or for generating summaries.
59///
60/// # Arguments
61///
62/// * `repo` - A reference to an open `git2::Repository` instance.
63///
64/// # Returns
65///
66/// A newline-separated string of the latest commit messages. If no commits exist, returns `"None"`.
67///
68/// # Example
69///
70/// ```
71/// use git_commit_helper::get_recent_commit_message;
72/// use git2::Repository;
73///
74/// let repo = Repository::discover(".").expect("Not a git repository");
75/// let messages = get_recent_commit_message(&repo);
76/// println!("{}", messages);
77/// ```
78pub fn get_recent_commit_message(repo: &Repository) -> String {
79 let mut revwalk = repo.revwalk().expect("Failed to get revwalk");
80 if let Err(e) = revwalk.push_head() {
81 eprintln!("Warning: Cannot find HEAD. Possibly no commits yet: {}", e);
82 return String::from("None");
83 };
84 let commits: Vec<String> = revwalk
85 .take(3)
86 .filter_map(|oid| oid.ok())
87 .filter_map(|oid| repo.find_commit(oid).ok())
88 .map(|commit| commit.message().unwrap_or("").trim().replace('"', "\\\""))
89 .collect();
90 commits.join("\n\n")
91}
92
93/// Commits the currently staged changes with the provided commit message.
94///
95/// This function handles both initial and regular commits, constructing the commit tree
96/// and linking to the correct parent if available.
97///
98/// # Arguments
99///
100/// * `repo` - A reference to an open `git2::Repository` instance.
101/// * `message` - The commit message to use.
102///
103/// # Errors
104///
105/// Returns a boxed `Error` if Git operations (e.g., getting the index, writing tree, or committing) fail.
106///
107/// # Example
108///
109/// ```
110/// use git_commit_helper::commit_with_git;
111/// use git2::Repository;
112///
113/// let repo = Repository::discover(".").expect("Not a git repository");
114/// let message = "Add README and initial setup";
115/// if let Err(err) = commit_with_git(&repo, message) {
116/// eprintln!("Commit failed: {}", err);
117/// }
118/// ```
119pub fn commit_with_git(repo: &Repository, message: &str) -> Result<(), Box<dyn Error>> {
120 let sig = repo.signature()?;
121
122 let tree_oid = {
123 let mut index = repo.index()?;
124 let oid = index.write_tree()?;
125 repo.find_tree(oid)?
126 };
127
128 let head = repo.head().ok();
129 let parent_commit = head
130 .as_ref()
131 .and_then(|h| h.target())
132 .and_then(|oid| repo.find_commit(oid).ok());
133
134 let tree = repo.find_tree(tree_oid.id())?;
135
136 let commit_oid = match parent_commit {
137 Some(parent) => repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&parent])?,
138 None => repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[])?,
139 };
140
141 println!("✅ Commit created: {}", commit_oid);
142 Ok(())
143}