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
18    if include_umbrella {
19        let (_, conflicts) = update_repo(umbrella, &umbrella.head, "umbrella", stdout)?;
20        saw_conflicts |= conflicts;
21    }
22
23    // Step 1: Update each repo with fetch and merge
24    for config_repo in &wok_config.repos {
25        if config_repo.is_skipped_for("update") {
26            continue;
27        }
28
29        if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
30            let label = config_repo.path.display().to_string();
31            let (updated, conflicts) =
32                update_repo(subrepo, &config_repo.head, &label, stdout)?;
33            saw_subrepo_updates |= updated;
34            saw_conflicts |= conflicts;
35        }
36    }
37
38    // Step 2: Stage all submodule changes in umbrella repo
39    let staged_changes = stage_submodule_changes(&umbrella.git_repo)?;
40
41    if saw_conflicts {
42        writeln!(
43            stdout,
44            "Skipped committing umbrella repo due to merge conflicts"
45        )?;
46        return Ok(());
47    }
48
49    if no_commit {
50        if staged_changes || saw_subrepo_updates {
51            writeln!(
52                stdout,
53                "Changes staged; commit skipped because --no-commit was provided"
54            )?;
55        } else {
56            writeln!(stdout, "No submodule updates detected; nothing to commit")?;
57        }
58        return Ok(());
59    }
60
61    // Step 3: Commit the updated submodule state
62    if !staged_changes {
63        writeln!(stdout, "No submodule updates detected; nothing to commit")?;
64        return Ok(());
65    }
66
67    commit_submodule_updates(&umbrella.git_repo)?;
68
69    writeln!(stdout, "Updated submodule state committed")?;
70    Ok(())
71}
72
73fn update_repo<W: Write>(
74    repo: &repo::Repo,
75    branch_name: &str,
76    label: &str,
77    stdout: &mut W,
78) -> Result<(bool, bool)> {
79    // Switch to the desired branch first
80    repo.switch(branch_name)?;
81
82    // Attempt to merge with remote changes
83    let merge_result = repo.merge(branch_name)?;
84
85    // Get the current commit hash for reporting
86    let current_commit = get_current_commit_hash(&repo.git_repo)?;
87    let short_commit = &current_commit[..std::cmp::min(8, current_commit.len())];
88
89    let mut updated = false;
90    let mut conflicts = false;
91
92    match merge_result {
93        repo::MergeResult::UpToDate => {
94            writeln!(
95                stdout,
96                "- '{}': already up to date on '{}' ({})",
97                label, branch_name, short_commit
98            )?;
99        },
100        repo::MergeResult::FastForward => {
101            updated = true;
102            writeln!(
103                stdout,
104                "- '{}': fast-forwarded '{}' to {}",
105                label, branch_name, short_commit
106            )?;
107        },
108        repo::MergeResult::Merged => {
109            updated = true;
110            writeln!(
111                stdout,
112                "- '{}': merged '{}' to {}",
113                label, branch_name, short_commit
114            )?;
115        },
116        repo::MergeResult::Rebased => {
117            updated = true;
118            writeln!(
119                stdout,
120                "- '{}': rebased '{}' to {}",
121                label, branch_name, short_commit
122            )?;
123        },
124        repo::MergeResult::Conflicts => {
125            conflicts = true;
126            writeln!(
127                stdout,
128                "- '{}': merge conflicts in '{}' ({}), manual resolution required",
129                label, branch_name, short_commit
130            )?;
131        },
132    }
133
134    Ok((updated, conflicts))
135}
136
137fn get_current_commit_hash(git_repo: &git2::Repository) -> Result<String> {
138    let head = git_repo.head()?;
139    let commit = head.peel_to_commit()?;
140    Ok(commit.id().to_string())
141}
142
143fn stage_submodule_changes(git_repo: &git2::Repository) -> Result<bool> {
144    let head_tree = git_repo
145        .head()
146        .ok()
147        .and_then(|head| head.peel_to_tree().ok());
148    let mut index = git_repo.index()?;
149
150    for submodule in git_repo.submodules()? {
151        let submodule_path = submodule.path();
152
153        // Only stage submodules that have a head (are initialized)
154        if let Some(_submodule_oid) = submodule.head_id() {
155            index.add_path(submodule_path)?;
156        }
157    }
158
159    index.write()?;
160
161    if let Some(tree) = head_tree.as_ref() {
162        let diff = git_repo.diff_tree_to_index(Some(tree), Some(&index), None)?;
163        Ok(diff.deltas().len() > 0)
164    } else {
165        Ok(!index.is_empty())
166    }
167}
168
169fn commit_submodule_updates(git_repo: &git2::Repository) -> Result<()> {
170    let commit_message = "Update submodules to latest";
171    let signature = git_repo.signature()?;
172    let tree_id = git_repo.index()?.write_tree()?;
173    let tree = git_repo.find_tree(tree_id)?;
174
175    let head_ref = git_repo.head()?;
176    let parent_commit = head_ref.peel_to_commit()?;
177
178    git_repo.commit(
179        Some("HEAD"),
180        &signature,
181        &signature,
182        commit_message,
183        &tree,
184        &[&parent_commit],
185    )?;
186
187    Ok(())
188}