1use super::{git_cmd, info::current_branch};
4use anyhow::{bail, Result};
5use std::path::Path;
6
7fn run(cmd: &mut std::process::Command) -> Result<String> {
8 let out = super::output_with_timeout(cmd, std::time::Duration::from_secs(30))?;
9 let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
10 let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
11 if out.status.success() {
12 Ok(if stdout.is_empty() { stderr } else { stdout })
13 } else {
14 let msg = if !stderr.is_empty() { stderr } else { stdout };
15 bail!("{}", msg.lines().next().unwrap_or("git error"))
16 }
17}
18
19pub fn pull(path: &Path) -> Result<String> {
20 match run(git_cmd(path).args(["pull", "--rebase"])) {
21 Ok(output) => Ok(output),
22 Err(error) => {
23 if rebase_in_progress(path) {
24 let _ = run(git_cmd(path).args(["rebase", "--abort"]));
25 bail!("pull stopped on conflict; rebase aborted; resolve manually");
26 }
27 Err(error)
28 }
29 }
30}
31
32fn rebase_in_progress(path: &Path) -> bool {
33 ["rebase-merge", "rebase-apply"].iter().any(|state_dir| {
34 super::output_with_timeout(
35 git_cmd(path).args(["rev-parse", "--git-path", state_dir]),
36 std::time::Duration::from_secs(5),
37 )
38 .ok()
39 .filter(|output| output.status.success())
40 .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
41 .filter(|git_path| !git_path.is_empty())
42 .is_some_and(|git_path| path.join(git_path).exists())
43 })
44}
45
46pub fn push(path: &Path) -> Result<String> {
47 let result = run(git_cmd(path).args(["push"]));
48 match result {
49 Ok(s) => Ok(s),
50 Err(e) => {
51 let msg = e.to_string();
52 if msg.contains("no upstream") || msg.contains("--set-upstream") {
53 let branch = current_branch(path).unwrap_or_else(|| "HEAD".to_string());
54 run(git_cmd(path).args(["push", "-u", "origin", &branch]))
55 } else {
56 Err(e)
57 }
58 }
59 }
60}
61
62pub fn pull_rebase(path: &Path, branch: &str) -> Result<String> {
63 run(git_cmd(path).args(["pull", "--rebase", "origin", branch]))
64}
65
66pub fn merge_from(path: &Path, source: &str) -> Result<String> {
67 run(git_cmd(path).args(["merge", source]))
68}
69
70pub fn merge_into(path: &Path, target: &str) -> Result<String> {
71 let current = current_branch(path).ok_or_else(|| anyhow::anyhow!("not on a branch"))?;
72 run(git_cmd(path).args(["checkout", target]))?;
74 let merge_result = run(git_cmd(path).args(["merge", ¤t]));
76 run(git_cmd(path).args(["checkout", ¤t]))?;
78 merge_result.map(|_| {
79 format!(
80 "Merged {} into {}, returned to {}",
81 current, target, current
82 )
83 })
84}
85
86#[cfg(test)]
87mod tests {
88 use super::rebase_in_progress;
89 use std::path::PathBuf;
90 use std::process::Command;
91
92 #[test]
93 fn linked_git_state_path_detects_and_clears_rebase_marker() {
94 let repo = PathBuf::from("target").join(format!("rebase-state-{}", std::process::id()));
95 let _ = std::fs::remove_dir_all(&repo);
96 std::fs::create_dir_all(&repo).unwrap();
97 let initialized = Command::new("git")
98 .args(["init", "--quiet"])
99 .current_dir(&repo)
100 .status()
101 .unwrap();
102 assert!(initialized.success());
103
104 let marker = repo.join(".git/rebase-merge");
105 std::fs::create_dir_all(&marker).unwrap();
106 assert!(rebase_in_progress(&repo));
107
108 std::fs::remove_dir_all(marker).unwrap();
109 assert!(!rebase_in_progress(&repo));
110 std::fs::remove_dir_all(repo).unwrap();
111 }
112}