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/11 00:38: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) -> Option<String> {
39    let index = repo.index().ok()?;
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        .ok()?;
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 None;
52    }
53    let result = String::from_utf8_lossy(&buf).to_string();
54    if result.trim().is_empty() {
55        return None;
56    }
57    Some(result)
58}
59
60/// Returns the messages of the most recent commits (up to 3).
61///
62/// Useful for providing context to an LLM or for generating summaries.
63///
64/// # Arguments
65///
66/// * `repo` - A reference to an open `git2::Repository` instance.
67///
68/// # Returns
69///
70/// A newline-separated string of the latest commit messages. If no commits exist, returns `"None"`.
71///
72/// # Example
73///
74/// ```
75/// use git_commit_helper::get_recent_commit_message;
76/// use git2::Repository;
77///
78/// let repo = Repository::discover(".").expect("Not a git repository");
79/// let messages = get_recent_commit_message(&repo);
80/// println!("{:?}", messages);
81/// ```
82pub fn get_recent_commit_message(repo: &Repository) -> Option<String> {
83    let mut revwalk = repo.revwalk().ok()?;
84    revwalk.push_head().ok()?;
85    let commits: Vec<String> = revwalk
86        .take(3)
87        .filter_map(|oid| oid.ok())
88        .filter_map(|oid| repo.find_commit(oid).ok())
89        .map(|commit| commit.message().unwrap_or("").trim().replace('"', "\\\""))
90        .collect();
91    if commits.is_empty() {
92        return None;
93    }
94    Some(commits.join("\n\n"))
95}
96
97/// Commits the currently staged changes with the provided commit message.
98///
99/// This function handles both initial and regular commits, constructing the commit tree
100/// and linking to the correct parent if available.
101///
102/// # Arguments
103///
104/// * `repo` - A reference to an open `git2::Repository` instance.
105/// * `message` - The commit message to use.
106///
107/// # Errors
108///
109/// Returns a boxed `Error` if Git operations (e.g., getting the index, writing tree, or committing) fail.
110///
111/// # Example
112///
113/// ```
114/// use git_commit_helper::commit_with_git;
115/// use git2::Repository;
116///
117/// let repo = Repository::discover(".").expect("Not a git repository");
118/// let message = "Add README and initial setup";
119/// if let Err(err) = commit_with_git(&repo, message) {
120///     eprintln!("Commit failed: {}", err);
121/// }
122/// ```
123pub fn commit_with_git(repo: &Repository, message: &str) -> Result<(), Box<dyn Error>> {
124    let sig = repo.signature()?;
125
126    let tree_oid = {
127        let mut index = repo.index()?;
128        let oid = index.write_tree()?;
129        repo.find_tree(oid)?
130    };
131
132    let head = repo.head().ok();
133    let parent_commit = head
134        .as_ref()
135        .and_then(|h| h.target())
136        .and_then(|oid| repo.find_commit(oid).ok());
137
138    let tree = repo.find_tree(tree_oid.id())?;
139
140    let commit_oid = match parent_commit {
141        Some(parent) => repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&parent])?,
142        None => repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[])?,
143    };
144
145    println!("✅ Commit created: {}", commit_oid);
146    Ok(())
147}