1use crate::git::error::git_base_command;
17use anyhow::{Context, Result, bail};
18use std::path::Path;
19
20pub(crate) fn current_branch(repo_root: &Path) -> Result<String> {
21 let output = git_base_command(repo_root)
22 .arg("rev-parse")
23 .arg("--abbrev-ref")
24 .arg("HEAD")
25 .output()
26 .with_context(|| {
27 format!(
28 "run git rev-parse --abbrev-ref HEAD in {}",
29 repo_root.display()
30 )
31 })?;
32
33 if !output.status.success() {
34 let stderr = String::from_utf8_lossy(&output.stderr);
35 bail!(
36 "Failed to determine current branch: git rev-parse error: {}",
37 stderr.trim()
38 );
39 }
40
41 let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
42 if branch.is_empty() {
43 bail!("Failed to determine current branch: empty branch name.");
44 }
45
46 if branch == "HEAD" {
47 bail!("Parallel run requires a named branch (detached HEAD detected).");
48 }
49
50 Ok(branch)
51}
52
53#[allow(dead_code)]
54pub(crate) fn fast_forward_branch_to_origin(repo_root: &Path, branch: &str) -> Result<()> {
55 let branch = branch.trim();
56 if branch.is_empty() {
57 bail!("Cannot fast-forward: branch name is empty.");
58 }
59
60 let checkout_output = git_base_command(repo_root)
61 .args(["checkout", branch])
62 .output()
63 .with_context(|| format!("run git checkout {} in {}", branch, repo_root.display()))?;
64 if !checkout_output.status.success() {
65 let stderr = String::from_utf8_lossy(&checkout_output.stderr);
66 bail!(
67 "Failed to check out branch {} before fast-forward: {}",
68 branch,
69 stderr.trim()
70 );
71 }
72
73 let fetch_output = git_base_command(repo_root)
74 .args(["fetch", "origin", "--prune"])
75 .output()
76 .with_context(|| format!("run git fetch origin --prune in {}", repo_root.display()))?;
77 if !fetch_output.status.success() {
78 let stderr = String::from_utf8_lossy(&fetch_output.stderr);
79 bail!(
80 "Failed to fetch origin before fast-forwarding {}: {}",
81 branch,
82 stderr.trim()
83 );
84 }
85
86 let remote_ref = format!("origin/{}", branch);
87 let merge_output = git_base_command(repo_root)
88 .args(["merge", "--ff-only", &remote_ref])
89 .output()
90 .with_context(|| {
91 format!(
92 "run git merge --ff-only {} in {}",
93 remote_ref,
94 repo_root.display()
95 )
96 })?;
97 if !merge_output.status.success() {
98 let stderr = String::from_utf8_lossy(&merge_output.stderr);
99 bail!(
100 "Failed to fast-forward branch {} to {}: {}",
101 branch,
102 remote_ref,
103 stderr.trim()
104 );
105 }
106
107 Ok(())
108}
109
110#[cfg(test)]
111mod tests {
112 use super::{current_branch, fast_forward_branch_to_origin};
113 use crate::testsupport::git as git_test;
114 use anyhow::Result;
115 use tempfile::TempDir;
116
117 #[test]
118 fn current_branch_returns_branch_name() -> Result<()> {
119 let temp = TempDir::new()?;
120 git_test::init_repo(temp.path())?;
121 std::fs::write(temp.path().join("init.txt"), "init")?;
122 git_test::commit_all(temp.path(), "init")?;
123 let expected = git_test::git_output(temp.path(), &["rev-parse", "--abbrev-ref", "HEAD"])?;
124 let branch = current_branch(temp.path())?;
125 assert_eq!(branch, expected);
126 Ok(())
127 }
128
129 #[test]
130 fn current_branch_errors_on_detached_head() -> Result<()> {
131 let temp = TempDir::new()?;
132 git_test::init_repo(temp.path())?;
133 std::fs::write(temp.path().join("init.txt"), "init")?;
134 git_test::commit_all(temp.path(), "init")?;
135 git_test::git_run(temp.path(), &["checkout", "--detach", "HEAD"])?;
136 let err = current_branch(temp.path()).unwrap_err();
137 assert!(err.to_string().contains("detached HEAD"));
138 Ok(())
139 }
140
141 #[test]
142 fn fast_forward_branch_to_origin_updates_local_branch() -> Result<()> {
143 let temp = TempDir::new()?;
144 let remote = temp.path().join("remote.git");
145 std::fs::create_dir_all(&remote)?;
146 git_test::init_bare_repo(&remote)?;
147
148 let seed = temp.path().join("seed");
149 std::fs::create_dir_all(&seed)?;
150 git_test::init_repo(&seed)?;
151 std::fs::write(seed.join("seed.txt"), "v1")?;
152 git_test::commit_all(&seed, "seed init")?;
153 let branch = git_test::git_output(&seed, &["rev-parse", "--abbrev-ref", "HEAD"])?;
154 git_test::add_remote(&seed, "origin", &remote)?;
155 git_test::push_branch(&seed, &branch)?;
156 git_test::git_run(
157 &remote,
158 &["symbolic-ref", "HEAD", &format!("refs/heads/{}", branch)],
159 )?;
160
161 let local = temp.path().join("local");
162 git_test::clone_repo(&remote, &local)?;
163 git_test::configure_user(&local)?;
164
165 let upstream = temp.path().join("upstream");
166 git_test::clone_repo(&remote, &upstream)?;
167 git_test::configure_user(&upstream)?;
168 std::fs::write(upstream.join("seed.txt"), "v2")?;
169 git_test::commit_all(&upstream, "remote ahead")?;
170 git_test::push_branch(&upstream, &branch)?;
171
172 let old_head = git_test::git_output(&local, &["rev-parse", "HEAD"])?;
173 fast_forward_branch_to_origin(&local, &branch)?;
174 let new_head = git_test::git_output(&local, &["rev-parse", "HEAD"])?;
175 let remote_head =
176 git_test::git_output(&local, &["rev-parse", &format!("origin/{}", branch)])?;
177
178 assert_ne!(old_head, new_head);
179 assert_eq!(new_head, remote_head);
180 Ok(())
181 }
182
183 #[test]
184 fn fast_forward_branch_to_origin_errors_on_divergence() -> Result<()> {
185 let temp = TempDir::new()?;
186 let remote = temp.path().join("remote.git");
187 std::fs::create_dir_all(&remote)?;
188 git_test::init_bare_repo(&remote)?;
189
190 let seed = temp.path().join("seed");
191 std::fs::create_dir_all(&seed)?;
192 git_test::init_repo(&seed)?;
193 std::fs::write(seed.join("seed.txt"), "v1")?;
194 git_test::commit_all(&seed, "seed init")?;
195 let branch = git_test::git_output(&seed, &["rev-parse", "--abbrev-ref", "HEAD"])?;
196 git_test::add_remote(&seed, "origin", &remote)?;
197 git_test::push_branch(&seed, &branch)?;
198 git_test::git_run(
199 &remote,
200 &["symbolic-ref", "HEAD", &format!("refs/heads/{}", branch)],
201 )?;
202
203 let local = temp.path().join("local");
204 git_test::clone_repo(&remote, &local)?;
205 git_test::configure_user(&local)?;
206
207 let upstream = temp.path().join("upstream");
208 git_test::clone_repo(&remote, &upstream)?;
209 git_test::configure_user(&upstream)?;
210
211 std::fs::write(local.join("local.txt"), "local-only")?;
212 git_test::commit_all(&local, "local ahead")?;
213
214 std::fs::write(upstream.join("remote.txt"), "remote-only")?;
215 git_test::commit_all(&upstream, "remote ahead")?;
216 git_test::push_branch(&upstream, &branch)?;
217
218 let err = fast_forward_branch_to_origin(&local, &branch).unwrap_err();
219 assert!(
220 err.to_string().contains("fast-forward"),
221 "unexpected error: {err}"
222 );
223 Ok(())
224 }
225}