Skip to main content

_bver/
git.rs

1use std::fs;
2use std::process::Command;
3
4use crate::finders::find_repo_root;
5use crate::schema::{Action, GitConfig, RunPreCommit};
6
7/// Detected pre-commit tool type
8enum PreCommitTool {
9    Prek,
10    PreCommit,
11}
12
13/// Check if a command is available
14fn command_available(cmd: &str) -> bool {
15    Command::new(cmd)
16        .arg("--version")
17        .output()
18        .map(|o| o.status.success())
19        .unwrap_or(false)
20}
21
22/// Detect which pre-commit tool is available and configured
23fn detect_pre_commit_tool() -> Option<PreCommitTool> {
24    let repo_root = find_repo_root()?;
25    let hook_path = repo_root.join(".git/hooks/pre-commit");
26
27    if !hook_path.exists() {
28        return None;
29    }
30
31    // Check if the hook is a prek hook
32    if let Ok(content) = fs::read_to_string(&hook_path)
33        && content.contains("File generated by prek")
34        && command_available("prek")
35    {
36        return Some(PreCommitTool::Prek);
37    }
38
39    // Fall back to pre-commit if available
40    if command_available("pre-commit") {
41        return Some(PreCommitTool::PreCommit);
42    }
43
44    None
45}
46
47/// Run pre-commit hooks based on config setting
48pub fn maybe_run_pre_commit(setting: RunPreCommit) -> Result<(), String> {
49    match setting {
50        RunPreCommit::Disabled => Ok(()),
51        RunPreCommit::Enabled => run_pre_commit(true),
52        RunPreCommit::WhenPresent => run_pre_commit(false),
53    }
54}
55
56fn run_pre_commit(required: bool) -> Result<(), String> {
57    let tool = match detect_pre_commit_tool() {
58        Some(t) => t,
59        None => {
60            if required {
61                return Err(
62                    "pre-commit/prek is not installed but run-pre-commit is enabled".to_string(),
63                );
64            }
65            return Ok(());
66        }
67    };
68
69    let (cmd, name) = match tool {
70        PreCommitTool::Prek => ("prek", "prek"),
71        PreCommitTool::PreCommit => ("pre-commit", "pre-commit"),
72    };
73
74    println!("Running {} hooks...", name);
75
76    let status = Command::new(cmd)
77        .args(["run", "--all-files"])
78        .status()
79        .map_err(|e| format!("Failed to run {}: {}", name, e))?;
80
81    if status.success() {
82        println!("{} hooks passed.", name);
83        return Ok(());
84    }
85
86    // Failed, run it a second time (it may have auto-fixed files)
87    println!("{} hooks failed, running again...", name);
88
89    let status = Command::new(cmd)
90        .args(["run", "--all-files"])
91        .status()
92        .map_err(|e| format!("Failed to run {}: {}", name, e))?;
93
94    if status.success() {
95        println!("{} hooks passed on second run.", name);
96        Ok(())
97    } else {
98        Err(format!("{} hooks failed twice, aborting bump", name))
99    }
100}
101
102/// Run a git command and return the result
103fn git(args: &[&str]) -> Result<(), String> {
104    println!("Running: git {}", args.join(" "));
105
106    let output = Command::new("git")
107        .args(args)
108        .output()
109        .map_err(|e| format!("Failed to run git: {e}"))?;
110
111    if !output.status.success() {
112        let stderr = String::from_utf8_lossy(&output.stderr);
113        return Err(format!("git {} failed: {}", args[0], stderr.trim()));
114    }
115
116    Ok(())
117}
118
119/// Apply template substitutions for version strings
120fn apply_template(template: &str, current_version: &str, new_version: &str) -> String {
121    template
122        .replace("{current-version}", current_version)
123        .replace("{new-version}", new_version)
124}
125
126/// Run git operations based on config setting
127pub fn run_git_actions(
128    git_config: &GitConfig,
129    current_version: &str,
130    new_version: &str,
131    force: bool,
132) -> Result<(), String> {
133    let tag_name = apply_template(&git_config.tag_template, current_version, new_version);
134    let commit_msg = apply_template(&git_config.commit_template, current_version, new_version);
135    let branch_name = apply_template(&git_config.branch_template, current_version, new_version);
136
137    if git_config.has(Action::AddAll) {
138        git_add_all()?;
139    }
140    if git_config.has(Action::Branch) {
141        git_checkout_new_branch(&branch_name)?;
142    }
143    if git_config.has(Action::Commit) {
144        git_commit(&commit_msg)?;
145    }
146    if git_config.has(Action::Tag) {
147        git_tag(&tag_name, new_version, force)?;
148    }
149    if git_config.has(Action::Push) {
150        git_push(force)?;
151        if git_config.has(Action::Tag) {
152            git_push_tag(&tag_name, force)?;
153        }
154    }
155    if git_config.has(Action::Pr) {
156        gh_pr_create(&commit_msg)?;
157    }
158
159    Ok(())
160}
161
162fn git_add_all() -> Result<(), String> {
163    git(&["add", "--all"])
164}
165
166fn git_commit(msg: &str) -> Result<(), String> {
167    git(&["commit", "-m", msg])
168}
169
170fn git_tag(tag_name: &str, version: &str, force: bool) -> Result<(), String> {
171    let msg = format!("Release {}", version);
172    if force {
173        git(&["tag", "-a", tag_name, "-m", &msg, "-f"])
174    } else {
175        git(&["tag", "-a", tag_name, "-m", &msg])
176    }
177}
178
179fn git_push(force: bool) -> Result<(), String> {
180    if force {
181        git(&["push", "--force"])
182    } else {
183        git(&["push"])
184    }
185}
186
187fn git_push_tag(tag_name: &str, force: bool) -> Result<(), String> {
188    if force {
189        git(&["push", "origin", tag_name, "--force"])
190    } else {
191        git(&["push", "origin", tag_name])
192    }
193}
194
195fn git_checkout_new_branch(name: &str) -> Result<(), String> {
196    git(&["checkout", "-b", name])
197}
198
199fn gh_pr_create(title: &str) -> Result<(), String> {
200    println!("Running: gh pr create --title {:?} --body \"\"", title);
201
202    let output = Command::new("gh")
203        .args(["pr", "create", "--title", title, "--body", ""])
204        .output()
205        .map_err(|e| format!("Failed to run gh: {e}"))?;
206
207    if !output.status.success() {
208        let stderr = String::from_utf8_lossy(&output.stderr);
209        return Err(format!("gh pr create failed: {}", stderr.trim()));
210    }
211
212    Ok(())
213}