1use std::fs;
2use std::process::Command;
3
4use crate::finders::find_repo_root;
5use crate::schema::{Action, GitConfig, RunPreCommit};
6
7enum PreCommitTool {
9 Prek,
10 PreCommit,
11}
12
13fn 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
22fn 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 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 if command_available("pre-commit") {
41 return Some(PreCommitTool::PreCommit);
42 }
43
44 None
45}
46
47pub 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 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
102fn 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
119fn 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
126pub 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::Branch) {
138 git_checkout_new_branch(&branch_name)?;
139 }
140 if git_config.has(Action::AddAll) {
141 git_add_all()?;
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}