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