git_wok/cmd/
update.rs

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(); // Track updated repos
18
19    if include_umbrella {
20        let (_, conflicts) = update_repo(umbrella, &umbrella.head, "umbrella", stdout)?;
21        saw_conflicts |= conflicts;
22    }
23
24    // Step 1: Update each repo with fetch and merge
25    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            // Track updated repos
38            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    // Step 2: Stage all submodule changes in umbrella repo
50    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    // Step 3: Commit the updated submodule state
73    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    // Switch to the desired branch first
91    repo.switch(branch_name)?;
92
93    // Attempt to merge with remote changes
94    let merge_result = repo.merge(branch_name)?;
95
96    // Get the current commit hash for reporting
97    let current_commit = get_current_commit_hash(&repo.git_repo)?;
98    let short_commit = &current_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        // Only stage submodules that have a head (are initialized)
165        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)], // (name, branch, commit_hash)
183) -> 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    // Build commit message with update details
193    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
208/// Build a commit message for update operation showing which repos were updated.
209fn 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)], // (name, branch, commit_hash)
214) -> Result<String> {
215    // Get diff between parent tree and staged index
216    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    // Build a map of updated repos for quick lookup
221    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    // Iterate through changed files in the diff
227    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                    // Check if this was one of the updated repos
236                    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    // Build the commit message
250    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}