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 if repo_on_branch(repo, branch_name)? {
172 return Ok(SwitchResult::AlreadyOnBranch);
173 }
174
175 match repo.switch(branch_name) {
177 Ok(_) => Ok(SwitchResult::Switched),
178 Err(_) => {
179 if create {
180 create_and_switch_branch(repo, branch_name)?;
182 Ok(SwitchResult::Created)
183 } else {
184 Err(anyhow!(
185 "Branch '{}' does not exist and --create not specified",
186 branch_name
187 ))
188 }
189 },
190 }
191}
192
193fn create_and_switch_branch(repo: &repo::Repo, branch_name: &str) -> Result<()> {
194 let head = repo.git_repo.head()?;
196 let current_commit = head.peel_to_commit()?;
197
198 let _branch_ref = repo.git_repo.branch(branch_name, ¤t_commit, false)?;
200
201 repo.git_repo
203 .set_head(&format!("refs/heads/{}", branch_name))?;
204 repo.git_repo.checkout_head(None)?;
205
206 Ok(())
207}
208
209fn repo_on_branch(repo: &repo::Repo, branch_name: &str) -> Result<bool> {
210 if repo.git_repo.head_detached().with_context(|| {
211 format!(
212 "Cannot determine head state for repo at `{}`",
213 repo.work_dir.display()
214 )
215 })? {
216 return Ok(false);
217 }
218
219 let current = repo
220 .git_repo
221 .head()
222 .with_context(|| {
223 format!(
224 "Cannot find the head branch for repo at `{}`",
225 repo.work_dir.display()
226 )
227 })?
228 .shorthand()
229 .with_context(|| {
230 format!(
231 "Cannot resolve the head reference for repo at `{}`",
232 repo.work_dir.display()
233 )
234 })?
235 .to_owned();
236
237 Ok(current == branch_name)
238}
239
240fn lock_switched_repos(
241 umbrella: &repo::Repo,
242 config_path: &std::path::Path,
243 switched_repos: &[config::Repo],
244) -> Result<()> {
245 let mut index = umbrella.git_repo.index()?;
247 for submodule in umbrella.git_repo.submodules()? {
248 let submodule_path = submodule.path();
249
250 if let Some(_submodule_oid) = submodule.head_id() {
252 index.add_path(submodule_path)?;
254 }
255 }
256 index.write()?;
257
258 let wokfile_path =
259 config_path
260 .strip_prefix(&umbrella.work_dir)
261 .with_context(|| {
262 format!(
263 "Wokfile must be inside umbrella repo to be committed: `{}`",
264 config_path.display()
265 )
266 })?;
267 index.add_path(wokfile_path)?;
268 index.write()?;
269
270 let signature = umbrella.git_repo.signature()?;
272 let tree_id = umbrella.git_repo.index()?.write_tree()?;
273 let tree = umbrella.git_repo.find_tree(tree_id)?;
274
275 let head_ref = umbrella.git_repo.head()?;
276 let parent_commit = head_ref.peel_to_commit()?;
277 let parent_tree = parent_commit.tree()?;
278
279 if tree.id() == parent_tree.id() {
280 return Ok(());
281 }
282
283 let (commit_message, _changed_submodules) =
285 build_switch_commit_message(umbrella, &parent_tree, &tree, switched_repos)?;
286
287 umbrella.git_repo.commit(
288 Some("HEAD"),
289 &signature,
290 &signature,
291 &commit_message,
292 &tree,
293 &[&parent_commit],
294 )?;
295
296 Ok(())
297}
298
299fn build_switch_commit_message(
302 umbrella: &repo::Repo,
303 parent_tree: &git2::Tree,
304 index_tree: &git2::Tree,
305 switched_repos: &[config::Repo],
306) -> Result<(String, Vec<(String, String)>)> {
307 let diff = umbrella.git_repo.diff_tree_to_tree(
309 Some(parent_tree),
310 Some(index_tree),
311 None,
312 )?;
313
314 let mut changed_submodules = Vec::new();
315
316 let switched_paths: std::collections::HashSet<_> = switched_repos
318 .iter()
319 .map(|r| r.path.to_string_lossy().to_string())
320 .collect();
321
322 for delta in diff.deltas() {
324 if let Some(file_path) = delta.new_file().path()
325 && let Some(file_path_str) = file_path.to_str()
326 {
327 match umbrella.git_repo.find_submodule(file_path_str) {
328 std::result::Result::Ok(submodule) => {
329 let submodule_name = submodule.path().to_string_lossy().to_string();
330
331 if switched_paths.contains(&submodule_name) {
333 let submodule_repo_path =
334 umbrella.work_dir.join(submodule.path());
335 match git2::Repository::open(&submodule_repo_path) {
336 std::result::Result::Ok(subrepo_git) => {
337 match subrepo_git.head() {
338 std::result::Result::Ok(head_ref) => {
339 if let Some(branch_name) = head_ref.shorthand()
340 {
341 changed_submodules.push((
342 submodule_name,
343 branch_name.to_string(),
344 ));
345 }
346 },
347 std::result::Result::Err(_) => continue,
348 }
349 },
350 std::result::Result::Err(_) => continue,
351 }
352 }
353 },
354 std::result::Result::Err(_) => continue,
355 }
356 }
357 }
358
359 let mut message = String::from("Switch and lock submodule state");
361
362 if !changed_submodules.is_empty() {
363 message.push_str("\n\nSwitched submodules:");
364 for (name, branch) in &changed_submodules {
365 message.push_str(&format!("\n- {}: {}", name, branch));
366 }
367 }
368
369 std::result::Result::Ok((message, changed_submodules))
370}