1use std::path::Path;
9use std::time::Duration;
10
11use procpilot::{Cmd, RetryPolicy, RunError, RunOutput};
12
13pub fn run_jj(repo_path: &Path, args: &[&str]) -> Result<RunOutput, RunError> {
15 Cmd::new("jj").in_dir(repo_path).args(args).run()
16}
17
18pub fn run_git(repo_path: &Path, args: &[&str]) -> Result<RunOutput, RunError> {
20 Cmd::new("git").in_dir(repo_path).args(args).run()
21}
22
23pub fn run_jj_utf8(repo_path: &Path, args: &[&str]) -> Result<String, RunError> {
28 let out = run_jj(repo_path, args)?;
29 Ok(out.stdout_lossy().trim().to_string())
30}
31
32pub fn run_git_utf8(repo_path: &Path, args: &[&str]) -> Result<String, RunError> {
34 let out = run_git(repo_path, args)?;
35 Ok(out.stdout_lossy().trim().to_string())
36}
37
38pub fn run_jj_utf8_with_timeout(
40 repo_path: &Path,
41 args: &[&str],
42 timeout: Duration,
43) -> Result<String, RunError> {
44 let out = run_jj_with_timeout(repo_path, args, timeout)?;
45 Ok(out.stdout_lossy().trim().to_string())
46}
47
48pub fn run_git_utf8_with_timeout(
50 repo_path: &Path,
51 args: &[&str],
52 timeout: Duration,
53) -> Result<String, RunError> {
54 let out = run_git_with_timeout(repo_path, args, timeout)?;
55 Ok(out.stdout_lossy().trim().to_string())
56}
57
58pub fn run_jj_utf8_with_retry(
60 repo_path: &Path,
61 args: &[&str],
62 is_transient: impl Fn(&RunError) -> bool + Send + Sync + 'static,
63) -> Result<String, RunError> {
64 let out = run_jj_with_retry(repo_path, args, is_transient)?;
65 Ok(out.stdout_lossy().trim().to_string())
66}
67
68pub fn run_git_utf8_with_retry(
70 repo_path: &Path,
71 args: &[&str],
72 is_transient: impl Fn(&RunError) -> bool + Send + Sync + 'static,
73) -> Result<String, RunError> {
74 let out = run_git_with_retry(repo_path, args, is_transient)?;
75 Ok(out.stdout_lossy().trim().to_string())
76}
77
78pub fn run_jj_with_timeout(
80 repo_path: &Path,
81 args: &[&str],
82 timeout: Duration,
83) -> Result<RunOutput, RunError> {
84 Cmd::new("jj")
85 .in_dir(repo_path)
86 .args(args)
87 .timeout(timeout)
88 .run()
89}
90
91pub fn run_git_with_timeout(
93 repo_path: &Path,
94 args: &[&str],
95 timeout: Duration,
96) -> Result<RunOutput, RunError> {
97 Cmd::new("git")
98 .in_dir(repo_path)
99 .args(args)
100 .timeout(timeout)
101 .run()
102}
103
104pub fn run_jj_with_retry(
106 repo_path: &Path,
107 args: &[&str],
108 is_transient: impl Fn(&RunError) -> bool + Send + Sync + 'static,
109) -> Result<RunOutput, RunError> {
110 Cmd::new("jj")
111 .in_dir(repo_path)
112 .args(args)
113 .retry(RetryPolicy::default().when(is_transient))
114 .run()
115}
116
117pub fn run_git_with_retry(
119 repo_path: &Path,
120 args: &[&str],
121 is_transient: impl Fn(&RunError) -> bool + Send + Sync + 'static,
122) -> Result<RunOutput, RunError> {
123 Cmd::new("git")
124 .in_dir(repo_path)
125 .args(args)
126 .retry(RetryPolicy::default().when(is_transient))
127 .run()
128}
129
130pub fn jj_merge_base(
136 repo_path: &Path,
137 a: &str,
138 b: &str,
139) -> Result<Option<String>, RunError> {
140 let revset = format!("latest(::({a}) & ::({b}))");
141 let id = run_jj_utf8(
142 repo_path,
143 &[
144 "log", "-r", &revset, "--no-graph", "--limit", "1", "-T", "commit_id",
145 ],
146 )?;
147 Ok(if id.is_empty() { None } else { Some(id) })
148}
149
150pub fn git_merge_base(
155 repo_path: &Path,
156 a: &str,
157 b: &str,
158) -> Result<Option<String>, RunError> {
159 match run_git_utf8(repo_path, &["merge-base", a, b]) {
160 Ok(id) => Ok(if id.is_empty() { None } else { Some(id) }),
161 Err(RunError::NonZeroExit { status, .. }) if status.code() == Some(1) => Ok(None),
162 Err(e) => Err(e),
163 }
164}
165
166pub fn is_transient_error(err: &RunError) -> bool {
172 procpilot::default_transient(err)
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 fn jj_installed() -> bool {
180 procpilot::binary_available("jj")
181 }
182
183 fn git_installed() -> bool {
184 procpilot::binary_available("git")
185 }
186
187 #[test]
188 fn is_transient_matches_stale_and_lock() {
189 #[cfg(unix)]
190 let status = {
191 use std::os::unix::process::ExitStatusExt;
192 std::process::ExitStatus::from_raw(256)
193 };
194 #[cfg(windows)]
195 let status = {
196 use std::os::windows::process::ExitStatusExt;
197 std::process::ExitStatus::from_raw(1)
198 };
199 let err = RunError::NonZeroExit {
200 command: Cmd::new("jj").display(),
201 status,
202 stdout: vec![],
203 stderr: "The working copy is stale".into(),
204 };
205 assert!(is_transient_error(&err));
206 }
207
208 #[test]
209 fn run_jj_fails_gracefully_when_not_installed() {
210 if jj_installed() {
211 return;
212 }
213 let tmp = tempfile::tempdir().expect("tempdir");
214 let err = run_jj(tmp.path(), &["status"]).expect_err("jj not installed");
215 assert!(err.is_spawn_failure());
216 }
217
218 #[test]
219 fn git_merge_base_returns_none_for_unrelated() {
220 if !git_installed() {
221 return;
222 }
223 let tmp = tempfile::tempdir().expect("tempdir");
224 std::process::Command::new("git")
225 .args(["init", "--quiet"])
226 .current_dir(tmp.path())
227 .status()
228 .expect("git init");
229 let _ = git_merge_base(tmp.path(), "HEAD", "HEAD");
231 }
232}