1use anyhow::*;
2use std::io::Write;
3use std::result::Result::Ok;
4
5use crate::{config, repo};
6
7#[allow(clippy::too_many_arguments)]
8pub fn switch<W: Write>(
9 wok_config: &mut config::Config,
10 umbrella: &repo::Repo,
11 config_path: &std::path::Path,
12 stdout: &mut W,
13 create: bool,
14 all: bool,
15 branch_name: &str,
16 target_repos: &[std::path::PathBuf],
17) -> Result<bool> {
18 let mut config_updated = false;
19 let mut submodule_changed = false;
20
21 let umbrella_branch = branch_name.to_string();
22
23 let switch_plan: Vec<SwitchPlanItem> = wok_config
24 .repos
25 .iter()
26 .filter_map(|config_repo| {
27 let explicitly_targeted = target_repos.contains(&config_repo.path);
28 if config_repo.is_skipped_for("switch") && !explicitly_targeted {
29 return None;
30 }
31
32 let desired_branch = if config_repo.head.trim().is_empty() {
33 umbrella_branch.clone()
34 } else {
35 config_repo.head.clone()
36 };
37 let forced = all || explicitly_targeted;
38 let effective_branch = if forced {
39 umbrella_branch.clone()
40 } else {
41 desired_branch.clone()
42 };
43
44 Some(SwitchPlanItem {
45 config_repo: config_repo.clone(),
46 effective_branch,
47 forced,
48 })
49 })
50 .collect();
51
52 if switch_plan.is_empty() {
53 writeln!(stdout, "No repositories to switch")?;
54 return Ok(config_updated);
55 }
56
57 writeln!(
58 stdout,
59 "Switching {} repositories for umbrella branch '{}'...",
60 switch_plan.len(),
61 umbrella_branch
62 )?;
63
64 for plan_item in &switch_plan {
66 let config_repo = &plan_item.config_repo;
67 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
68 match switch_repo(subrepo, &plan_item.effective_branch, create) {
69 Ok(result) => {
70 if plan_item.forced {
71 config_updated |= wok_config.set_repo_head(
72 config_repo.path.as_path(),
73 &umbrella_branch,
74 );
75 }
76
77 match result {
78 SwitchResult::Switched => {
79 writeln!(
80 stdout,
81 "- '{}': switched to '{}'",
82 config_repo.path.display(),
83 plan_item.effective_branch
84 )?;
85 submodule_changed = true;
86 },
87 SwitchResult::Created => {
88 writeln!(
89 stdout,
90 "- '{}': created and switched to '{}'",
91 config_repo.path.display(),
92 plan_item.effective_branch
93 )?;
94 submodule_changed = true;
95 },
96 SwitchResult::AlreadyOnBranch => {
97 writeln!(
98 stdout,
99 "- '{}': already on '{}'",
100 config_repo.path.display(),
101 plan_item.effective_branch
102 )?;
103 },
104 };
105 },
106 Err(e) => {
107 writeln!(
108 stdout,
109 "- '{}': failed to switch to '{}' - {}",
110 config_repo.path.display(),
111 plan_item.effective_branch,
112 e
113 )?;
114 },
115 }
116 }
117 }
118
119 if config_updated {
120 wok_config
121 .save(config_path)
122 .context("Cannot save updated wok file before locking")?;
123 }
124
125 if submodule_changed || config_updated {
126 writeln!(stdout, "Locking workspace state...")?;
128 let switched_repos: Vec<config::Repo> = switch_plan
129 .iter()
130 .map(|plan| plan.config_repo.clone())
131 .collect();
132 lock_switched_repos(umbrella, config_path, &switched_repos)?;
133
134 writeln!(
135 stdout,
136 "Successfully switched and locked {} repositories",
137 switch_plan.len()
138 )?;
139 } else {
140 writeln!(stdout, "No workspace changes detected; skipping lock")?;
141 writeln!(
142 stdout,
143 "Successfully processed {} repositories",
144 switch_plan.len()
145 )?;
146 }
147
148 Ok(config_updated)
149}
150
151#[derive(Debug, Clone)]
152struct SwitchPlanItem {
153 config_repo: config::Repo,
154 effective_branch: String,
155 forced: bool,
156}
157
158#[derive(Debug, Clone, PartialEq)]
159enum SwitchResult {
160 Switched,
161 Created,
162 AlreadyOnBranch,
163}
164
165fn switch_repo(
166 repo: &repo::Repo,
167 branch_name: &str,
168 create: bool,
169) -> Result<SwitchResult> {
170 let on_branch = repo_on_branch(repo, branch_name)?;
172 if on_branch {
173 repo.refresh_worktree_force()?;
174 return Ok(SwitchResult::AlreadyOnBranch);
175 }
176
177 match repo.switch_force(branch_name) {
179 Ok(_) => Ok(SwitchResult::Switched),
180 Err(_) => {
181 if create {
182 create_and_switch_branch(repo, branch_name)?;
184 Ok(SwitchResult::Created)
185 } else {
186 Err(anyhow!(
187 "Branch '{}' does not exist and --create not specified",
188 branch_name
189 ))
190 }
191 },
192 }
193}
194
195fn create_and_switch_branch(repo: &repo::Repo, branch_name: &str) -> Result<()> {
196 let head = repo.git_repo.head()?;
198 let current_commit = head.peel_to_commit()?;
199
200 let _branch_ref = repo.git_repo.branch(branch_name, ¤t_commit, false)?;
202
203 repo.git_repo
205 .set_head(&format!("refs/heads/{}", branch_name))?;
206 repo.git_repo.checkout_head(None)?;
207
208 Ok(())
209}
210
211fn repo_on_branch(repo: &repo::Repo, branch_name: &str) -> Result<bool> {
212 if repo.git_repo.head_detached().with_context(|| {
213 format!(
214 "Cannot determine head state for repo at `{}`",
215 repo.work_dir.display()
216 )
217 })? {
218 return Ok(false);
219 }
220
221 let current = repo
222 .git_repo
223 .head()
224 .with_context(|| {
225 format!(
226 "Cannot find the head branch for repo at `{}`",
227 repo.work_dir.display()
228 )
229 })?
230 .shorthand()
231 .with_context(|| {
232 format!(
233 "Cannot resolve the head reference for repo at `{}`",
234 repo.work_dir.display()
235 )
236 })?
237 .to_owned();
238
239 Ok(current == branch_name)
240}
241
242fn lock_switched_repos(
243 umbrella: &repo::Repo,
244 config_path: &std::path::Path,
245 switched_repos: &[config::Repo],
246) -> Result<()> {
247 let mut index = umbrella.git_repo.index()?;
249 for submodule in umbrella.git_repo.submodules()? {
250 let submodule_path = submodule.path();
251
252 if let Some(_submodule_oid) = submodule.head_id() {
254 index.add_path(submodule_path)?;
256 }
257 }
258 index.write()?;
259
260 let wokfile_path =
261 config_path
262 .strip_prefix(&umbrella.work_dir)
263 .with_context(|| {
264 format!(
265 "Wokfile must be inside umbrella repo to be committed: `{}`",
266 config_path.display()
267 )
268 })?;
269 index.add_path(wokfile_path)?;
270 index.write()?;
271
272 let signature = umbrella.git_repo.signature()?;
274 let tree_id = umbrella.git_repo.index()?.write_tree()?;
275 let tree = umbrella.git_repo.find_tree(tree_id)?;
276
277 let head_ref = umbrella.git_repo.head()?;
278 let parent_commit = head_ref.peel_to_commit()?;
279 let parent_tree = parent_commit.tree()?;
280
281 if tree.id() == parent_tree.id() {
282 return Ok(());
283 }
284
285 let (commit_message, _changed_submodules) =
287 build_switch_commit_message(umbrella, &parent_tree, &tree, switched_repos)?;
288
289 umbrella.git_repo.commit(
290 Some("HEAD"),
291 &signature,
292 &signature,
293 &commit_message,
294 &tree,
295 &[&parent_commit],
296 )?;
297
298 Ok(())
299}
300
301fn build_switch_commit_message(
304 umbrella: &repo::Repo,
305 parent_tree: &git2::Tree,
306 index_tree: &git2::Tree,
307 switched_repos: &[config::Repo],
308) -> Result<(String, Vec<(String, String)>)> {
309 let diff = umbrella.git_repo.diff_tree_to_tree(
311 Some(parent_tree),
312 Some(index_tree),
313 None,
314 )?;
315
316 let mut changed_submodules = Vec::new();
317
318 let switched_paths: std::collections::HashSet<_> = switched_repos
320 .iter()
321 .map(|r| r.path.to_string_lossy().to_string())
322 .collect();
323
324 for delta in diff.deltas() {
326 if let Some(file_path) = delta.new_file().path()
327 && let Some(file_path_str) = file_path.to_str()
328 {
329 match umbrella.git_repo.find_submodule(file_path_str) {
330 std::result::Result::Ok(submodule) => {
331 let submodule_name = submodule.path().to_string_lossy().to_string();
332
333 if switched_paths.contains(&submodule_name) {
335 let submodule_repo_path =
336 umbrella.work_dir.join(submodule.path());
337 match git2::Repository::open(&submodule_repo_path) {
338 std::result::Result::Ok(subrepo_git) => {
339 match subrepo_git.head() {
340 std::result::Result::Ok(head_ref) => {
341 if let Some(branch_name) = head_ref.shorthand()
342 {
343 changed_submodules.push((
344 submodule_name,
345 branch_name.to_string(),
346 ));
347 }
348 },
349 std::result::Result::Err(_) => continue,
350 }
351 },
352 std::result::Result::Err(_) => continue,
353 }
354 }
355 },
356 std::result::Result::Err(_) => continue,
357 }
358 }
359 }
360
361 let mut message = String::from("Switch and lock submodule state");
363
364 if !changed_submodules.is_empty() {
365 message.push_str("\n\nSwitched submodules:");
366 for (name, branch) in &changed_submodules {
367 message.push_str(&format!("\n- {}: {}", name, branch));
368 }
369 }
370
371 std::result::Result::Ok((message, changed_submodules))
372}