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