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