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