use crate::tools::{ToolError, truncate_tool_output};
use gix::ObjectId;
use schemars::JsonSchema;
use serde::Deserialize;
use std::io;
use super::{collect_cached_diff_lines, load_mutable_index, open_repo, path_from_bytes};
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GitCommitArgs {
pub repo_path: Option<String>,
pub message: String,
pub allow_empty: Option<bool>,
}
pub fn execute_git_commit_tool(
args: &GitCommitArgs,
working_dir: Option<&std::path::Path>,
) -> Result<String, ToolError> {
let output = git_commit_impl(
args.repo_path.as_deref(),
&args.message,
args.allow_empty.unwrap_or(false),
working_dir,
)?;
Ok(truncate_tool_output(&output))
}
fn git_commit_impl(
repo_path: Option<&str>,
message: &str,
allow_empty: bool,
working_dir: Option<&std::path::Path>,
) -> Result<String, ToolError> {
let repo = open_repo(repo_path, working_dir)?;
let message = message.trim();
if message.is_empty() {
return Err(ToolError::Other(
"commit message must not be empty".to_string(),
));
}
let index = load_mutable_index(&repo)?;
ensure_index_has_no_conflicts(&index)?;
if !allow_empty && collect_cached_diff_lines(&repo, &[] as &[String])?.is_empty() {
return Err(ToolError::Other("no staged changes to commit".to_string()));
}
let tree_id = write_tree_from_index(&repo, &index)?;
let parents = current_head_parents(&repo)?;
repo.commit("HEAD", message, tree_id, parents)
.map_err(io::Error::other)?;
super::log::git_log_impl(repo_path, 1, working_dir)
}
fn ensure_index_has_no_conflicts(index: &gix::index::File) -> Result<(), ToolError> {
if let Some(path) = index
.entries()
.iter()
.find(|entry| entry.stage() != gix::index::entry::Stage::Unconflicted)
.map(|entry| path_from_bytes(entry.path(index).as_ref()))
{
Err(ToolError::Other(format!(
"cannot commit with unresolved index conflicts at {path}"
)))
} else {
Ok(())
}
}
fn write_tree_from_index(
repo: &gix::Repository,
index: &gix::index::File,
) -> Result<ObjectId, ToolError> {
let mut editor = repo.empty_tree().edit().map_err(io::Error::other)?;
for entry in index.entries() {
if entry.stage() != gix::index::entry::Stage::Unconflicted {
return Err(ToolError::Other(format!(
"cannot write tree with conflicted index entry at {}",
path_from_bytes(entry.path(index).as_ref())
)));
}
let kind = entry.mode.to_tree_entry_mode().ok_or_else(|| {
ToolError::Other(format!(
"unsupported index entry mode {} at {}",
entry.mode.bits(),
path_from_bytes(entry.path(index).as_ref())
))
})?;
editor
.upsert(entry.path(index).to_owned(), kind.into(), entry.id)
.map_err(io::Error::other)?;
}
editor
.write()
.map(|id| id.detach())
.map_err(io::Error::other)
.map_err(ToolError::from)
}
fn current_head_parents(repo: &gix::Repository) -> Result<Vec<ObjectId>, ToolError> {
match repo.head_id() {
Ok(head) => Ok(vec![head.detach()]),
Err(_) => Ok(Vec::new()),
}
}
pub fn describe_git_commit_invocation(args: &GitCommitArgs) -> String {
let mut parts = vec![format!("Committing: {}.", args.message)];
if args.allow_empty.unwrap_or(false) {
parts.push(" Allow empty commit.".to_string());
}
parts.concat()
}
pub(crate) struct GitCommit;
define_tool!(
GitCommit,
"git_commit",
"Create a Git commit from the current index.",
GitCommitArgs,
execute_git_commit_tool,
"git",
describe_git_commit_invocation
);