use std::fs;
use std::path::Path;
use std::process::Command;
use crate::finders::find_repo_root;
use crate::schema::{Action, GitConfig, RunPreCommit};
enum PreCommitTool {
Prek,
PreCommit,
}
fn command_available(cmd: &str) -> bool {
Command::new(cmd)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn detect_pre_commit_tool() -> Option<PreCommitTool> {
let repo_root = find_repo_root()?;
let hook_path = repo_root.join(".git/hooks/pre-commit");
if !hook_path.exists() {
return None;
}
if let Ok(content) = fs::read_to_string(&hook_path)
&& content.contains("File generated by prek")
&& command_available("prek")
{
return Some(PreCommitTool::Prek);
}
if command_available("pre-commit") {
return Some(PreCommitTool::PreCommit);
}
None
}
pub fn maybe_run_pre_commit(setting: RunPreCommit) -> Result<(), String> {
match setting {
RunPreCommit::Disabled => Ok(()),
RunPreCommit::Enabled => run_pre_commit(true),
RunPreCommit::WhenPresent => run_pre_commit(false),
}
}
fn run_pre_commit(required: bool) -> Result<(), String> {
let tool = match detect_pre_commit_tool() {
Some(t) => t,
None => {
if required {
return Err(
"pre-commit/prek is not installed but run-pre-commit is enabled".to_string(),
);
}
return Ok(());
}
};
let (cmd, name) = match tool {
PreCommitTool::Prek => ("prek", "prek"),
PreCommitTool::PreCommit => ("pre-commit", "pre-commit"),
};
println!("Running {} hooks...", name);
let status = Command::new(cmd)
.args(["run", "--all-files"])
.status()
.map_err(|e| format!("Failed to run {}: {}", name, e))?;
if status.success() {
println!("{} hooks passed.", name);
return Ok(());
}
println!("{} hooks failed, running again...", name);
let status = Command::new(cmd)
.args(["run", "--all-files"])
.status()
.map_err(|e| format!("Failed to run {}: {}", name, e))?;
if status.success() {
println!("{} hooks passed on second run.", name);
Ok(())
} else {
Err(format!("{} hooks failed twice, aborting bump", name))
}
}
fn git(args: &[&str]) -> Result<(), String> {
println!("Running: git {}", args.join(" "));
let output = Command::new("git")
.args(args)
.output()
.map_err(|e| format!("Failed to run git: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("git {} failed: {}", args[0], stderr.trim()));
}
Ok(())
}
fn apply_template(template: &str, current_version: &str, new_version: &str) -> String {
template
.replace("{current-version}", current_version)
.replace("{new-version}", new_version)
}
pub fn run_git_actions(
git_config: &GitConfig,
current_version: &str,
new_version: &str,
force: bool,
changed_files: &[&Path],
) -> Result<(), String> {
let tag_name = apply_template(&git_config.tag_template, current_version, new_version);
let commit_msg = apply_template(&git_config.commit_template, current_version, new_version);
let branch_name = apply_template(&git_config.branch_template, current_version, new_version);
if git_config.has(Action::Branch) {
git_checkout_new_branch(&branch_name)?;
}
if git_config.has(Action::AddAll) {
git_add_all()?;
} else if git_config.has(Action::Commit) {
git_add_files(changed_files)?;
}
if git_config.has(Action::Commit) {
git_commit(&commit_msg)?;
}
if git_config.has(Action::Tag) {
git_tag(&tag_name, new_version, force)?;
}
if git_config.has(Action::Push) {
let set_upstream = git_config.has(Action::Branch);
git_push(force, set_upstream, &branch_name)?;
if git_config.has(Action::Tag) {
git_push_tag(&tag_name, force)?;
}
}
if git_config.has(Action::Pr) {
gh_pr_create(&commit_msg)?;
}
Ok(())
}
fn git_add_all() -> Result<(), String> {
git(&["add", "--all"])
}
fn git_add_files(paths: &[&Path]) -> Result<(), String> {
for path in paths {
let path_str = path.to_str().ok_or_else(|| format!("Invalid path: {:?}", path))?;
git(&["add", path_str])?;
}
Ok(())
}
fn git_commit(msg: &str) -> Result<(), String> {
git(&["commit", "-m", msg])
}
fn git_tag(tag_name: &str, version: &str, force: bool) -> Result<(), String> {
let msg = format!("Release {}", version);
if force {
git(&["tag", "-a", tag_name, "-m", &msg, "-f"])
} else {
git(&["tag", "-a", tag_name, "-m", &msg])
}
}
fn git_push(force: bool, set_upstream: bool, branch: &str) -> Result<(), String> {
match (force, set_upstream) {
(true, true) => git(&["push", "-u", "origin", branch, "--force"]),
(true, false) => git(&["push", "--force"]),
(false, true) => git(&["push", "-u", "origin", branch]),
(false, false) => git(&["push"]),
}
}
fn git_push_tag(tag_name: &str, force: bool) -> Result<(), String> {
if force {
git(&["push", "origin", tag_name, "--force"])
} else {
git(&["push", "origin", tag_name])
}
}
fn git_checkout_new_branch(name: &str) -> Result<(), String> {
git(&["checkout", "-b", name])
}
fn gh_pr_create(title: &str) -> Result<(), String> {
println!("Running: gh pr create --title {:?} --body \"\"", title);
let output = Command::new("gh")
.args(["pr", "create", "--title", title, "--body", ""])
.output()
.map_err(|e| format!("Failed to run gh: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("gh pr create failed: {}", stderr.trim()));
}
Ok(())
}