1use anyhow::*;
2use std::io::Write;
3
4use crate::{config, repo};
5
6pub fn update<W: Write>(
7 wok_config: &mut config::Config,
8 umbrella: &repo::Repo,
9 stdout: &mut W,
10 no_commit: bool,
11 include_umbrella: bool,
12) -> Result<()> {
13 writeln!(stdout, "Updating repositories...")?;
14
15 let mut saw_subrepo_updates = false;
16 let mut saw_conflicts = false;
17 let mut updated_repos = Vec::new(); if include_umbrella {
20 let (_, conflicts) =
21 update_repo(umbrella, &umbrella.head, "umbrella", true, stdout)?;
22 saw_conflicts |= conflicts;
23 }
24
25 for config_repo in &wok_config.repos {
27 if config_repo.is_skipped_for("update") {
28 continue;
29 }
30
31 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
32 let label = config_repo.path.display().to_string();
33 let target_branch = if config_repo.head.trim().is_empty() {
34 umbrella.head.clone()
35 } else {
36 config_repo.head.clone()
37 };
38 let (updated, conflicts) =
39 update_repo(subrepo, &target_branch, &label, false, stdout)?;
40 saw_subrepo_updates |= updated;
41 saw_conflicts |= conflicts;
42
43 if updated {
45 let commit_hash = get_current_commit_hash(&subrepo.git_repo)?;
46 updated_repos.push((
47 config_repo.path.to_string_lossy().to_string(),
48 target_branch,
49 commit_hash,
50 ));
51 }
52 }
53 }
54
55 let staged_changes = stage_submodule_changes(&umbrella.git_repo)?;
57
58 if saw_conflicts {
59 writeln!(
60 stdout,
61 "Skipped committing umbrella repo due to merge conflicts"
62 )?;
63 return Ok(());
64 }
65
66 if no_commit {
67 if staged_changes || saw_subrepo_updates {
68 writeln!(
69 stdout,
70 "Changes staged; commit skipped because --no-commit was provided"
71 )?;
72 } else {
73 writeln!(stdout, "No submodule updates detected; nothing to commit")?;
74 }
75 return Ok(());
76 }
77
78 if !staged_changes {
80 writeln!(stdout, "No submodule updates detected; nothing to commit")?;
81 return Ok(());
82 }
83
84 commit_submodule_updates(&umbrella.git_repo, &updated_repos)?;
85
86 writeln!(stdout, "Updated submodule state committed")?;
87 Ok(())
88}
89
90fn update_repo<W: Write>(
91 repo: &repo::Repo,
92 branch_name: &str,
93 label: &str,
94 allow_dirty: bool,
95 stdout: &mut W,
96) -> Result<(bool, bool)> {
97 let switch_result = if allow_dirty {
99 repo.switch(branch_name)
100 } else {
101 repo.ensure_on_branch(branch_name)
102 };
103 if let Err(err) = switch_result {
104 writeln!(stdout, "- '{}': skipped '{}': {}", label, branch_name, err)?;
105 return Ok((false, false));
106 }
107
108 let merge_result = repo.merge(branch_name)?;
110
111 let current_commit = get_current_commit_hash(&repo.git_repo)?;
113 let short_commit = ¤t_commit[..std::cmp::min(8, current_commit.len())];
114
115 let mut updated = false;
116 let mut conflicts = false;
117
118 match merge_result {
119 repo::MergeResult::UpToDate => {
120 writeln!(
121 stdout,
122 "- '{}': already up to date on '{}' ({})",
123 label, branch_name, short_commit
124 )?;
125 },
126 repo::MergeResult::FastForward => {
127 updated = true;
128 writeln!(
129 stdout,
130 "- '{}': fast-forwarded '{}' to {}",
131 label, branch_name, short_commit
132 )?;
133 },
134 repo::MergeResult::Merged => {
135 updated = true;
136 writeln!(
137 stdout,
138 "- '{}': merged '{}' to {}",
139 label, branch_name, short_commit
140 )?;
141 },
142 repo::MergeResult::Rebased => {
143 updated = true;
144 writeln!(
145 stdout,
146 "- '{}': rebased '{}' to {}",
147 label, branch_name, short_commit
148 )?;
149 },
150 repo::MergeResult::Conflicts => {
151 conflicts = true;
152 writeln!(
153 stdout,
154 "- '{}': merge conflicts in '{}' ({}), manual resolution required",
155 label, branch_name, short_commit
156 )?;
157 },
158 }
159
160 Ok((updated, conflicts))
161}
162
163fn get_current_commit_hash(git_repo: &git2::Repository) -> Result<String> {
164 let head = git_repo.head()?;
165 let commit = head.peel_to_commit()?;
166 Ok(commit.id().to_string())
167}
168
169fn stage_submodule_changes(git_repo: &git2::Repository) -> Result<bool> {
170 let head_tree = git_repo
171 .head()
172 .ok()
173 .and_then(|head| head.peel_to_tree().ok());
174 let mut index = git_repo.index()?;
175
176 for submodule in git_repo.submodules()? {
177 let submodule_path = submodule.path();
178
179 if let Some(_submodule_oid) = submodule.head_id() {
181 index.add_path(submodule_path)?;
182 }
183 }
184
185 index.write()?;
186
187 if let Some(tree) = head_tree.as_ref() {
188 let diff = git_repo.diff_tree_to_index(Some(tree), Some(&index), None)?;
189 Ok(diff.deltas().len() > 0)
190 } else {
191 Ok(!index.is_empty())
192 }
193}
194
195fn commit_submodule_updates(
196 git_repo: &git2::Repository,
197 updated_repos: &[(String, String, String)], ) -> Result<()> {
199 let signature = git_repo.signature()?;
200 let tree_id = git_repo.index()?.write_tree()?;
201 let tree = git_repo.find_tree(tree_id)?;
202
203 let head_ref = git_repo.head()?;
204 let parent_commit = head_ref.peel_to_commit()?;
205 let parent_tree = parent_commit.tree()?;
206
207 let commit_message =
209 build_update_commit_message(git_repo, &parent_tree, &tree, updated_repos)?;
210
211 git_repo.commit(
212 Some("HEAD"),
213 &signature,
214 &signature,
215 &commit_message,
216 &tree,
217 &[&parent_commit],
218 )?;
219
220 Ok(())
221}
222
223fn build_update_commit_message(
225 git_repo: &git2::Repository,
226 parent_tree: &git2::Tree,
227 index_tree: &git2::Tree,
228 updated_repos: &[(String, String, String)], ) -> Result<String> {
230 let diff = git_repo.diff_tree_to_tree(Some(parent_tree), Some(index_tree), None)?;
232
233 let mut changed_submodules = Vec::new();
234
235 let updated_map: std::collections::HashMap<_, _> = updated_repos
237 .iter()
238 .map(|(name, branch, hash)| (name.clone(), (branch.clone(), hash.clone())))
239 .collect();
240
241 for delta in diff.deltas() {
243 if let Some(file_path) = delta.new_file().path()
244 && let Some(file_path_str) = file_path.to_str()
245 {
246 match git_repo.find_submodule(file_path_str) {
247 std::result::Result::Ok(submodule) => {
248 let submodule_name = submodule.path().to_string_lossy().to_string();
249
250 if let Some((branch, hash)) = updated_map.get(&submodule_name) {
252 let short_hash = &hash[..std::cmp::min(8, hash.len())];
253 changed_submodules.push((
254 submodule_name,
255 format!("{} to {}", branch, short_hash),
256 ));
257 }
258 },
259 std::result::Result::Err(_) => continue,
260 }
261 }
262 }
263
264 let mut message = String::from("Update submodules to latest");
266
267 if !changed_submodules.is_empty() {
268 message.push_str("\n\nUpdated submodules:");
269 for (name, info) in &changed_submodules {
270 message.push_str(&format!("\n- {}: {}", name, info));
271 }
272 }
273
274 std::result::Result::Ok(message)
275}