Skip to main content

sley_submodule/
update_strategy.rs

1//! Submodule update-strategy resolution — a Rust port of git's
2//! `builtin/submodule--helper.c::determine_submodule_update_strategy` plus the
3//! per-strategy execution dispatch (`run_update_command`).
4//!
5//! `submodule update` must, for each selected submodule, pick exactly ONE update
6//! mode and run it against the gitlink oid recorded in the superproject index.
7//! git resolves the mode from a fixed precedence:
8//!
9//! 1. the command-line `--checkout`/`--merge`/`--rebase` flag (the `update`
10//!    default passed into `update_submodule`), if any;
11//! 2. else `.git/config submodule.<name>.update`;
12//! 3. else the `.gitmodules submodule.<name>.update` value parsed into the typed
13//!    [`UpdateStrategy`] (but NOT a `!command` — git BUG()s if it ever reads a
14//!    command out of `.gitmodules`, because `init` refuses to copy one);
15//! 4. else [`UpdateType::Checkout`].
16//!
17//! Then a `just_cloned` submodule downgrades `merge`/`rebase`/`none` to
18//! `checkout` (there is no local HEAD to merge/rebase onto yet).
19//!
20//! Centralizing this here means the CLI's `submodule update` routes every mode
21//! through one resolver + one dispatch, so the whole update CLASS (checkout,
22//! merge, rebase, command, none-skip) converges on a single code path instead of
23//! the old checkout-only special case.
24
25use crate::config::{UpdateStrategy, UpdateType, parse_update_strategy};
26
27/// Port of `determine_submodule_update_strategy`. Resolves the effective update
28/// strategy for one submodule from the precedence above.
29///
30/// - `cli_default` — the strategy forced by a `--checkout`/`--merge`/`--rebase`
31///   flag (`UpdateType::Unspecified` when none was given).
32/// - `config_update` — the raw `submodule.<name>.update` value from
33///   `.git/config` (None when unset).
34/// - `gitmodules_strategy` — the typed strategy parsed from `.gitmodules`.
35/// - `just_cloned` — true when the submodule worktree was populated by this very
36///   `update` run (no pre-existing HEAD to merge/rebase onto).
37///
38/// Returns `Err(invalid_value)` when the `.git/config` value is present but
39/// unparseable, mirroring git's `die("Invalid update mode '%s' …")`.
40pub fn determine_update_strategy(
41    cli_default: UpdateType,
42    config_update: Option<&str>,
43    gitmodules_strategy: &UpdateStrategy,
44    just_cloned: bool,
45) -> Result<UpdateStrategy, String> {
46    let mut out = if cli_default != UpdateType::Unspecified {
47        UpdateStrategy {
48            kind: cli_default,
49            command: None,
50        }
51    } else if let Some(val) = config_update {
52        // git: parse_submodule_update_strategy(val, out) < 0 -> die "Invalid …".
53        match parse_update_strategy(val) {
54            Some(strategy) => strategy,
55            None => return Err(val.to_string()),
56        }
57    } else if gitmodules_strategy.kind != UpdateType::Unspecified {
58        // git BUG()s if .gitmodules ever yields a Command; init refuses to copy
59        // one, so by construction we never see it here. Carry the typed value
60        // through (command stays None for the non-Command kinds).
61        gitmodules_strategy.clone()
62    } else {
63        UpdateStrategy {
64            kind: UpdateType::Checkout,
65            command: None,
66        }
67    };
68
69    if just_cloned
70        && matches!(
71            out.kind,
72            UpdateType::Merge | UpdateType::Rebase | UpdateType::None
73        )
74    {
75        out.kind = UpdateType::Checkout;
76        out.command = None;
77    }
78
79    Ok(out)
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    fn checkout() -> UpdateStrategy {
87        UpdateStrategy {
88            kind: UpdateType::Checkout,
89            command: None,
90        }
91    }
92
93    fn unspec() -> UpdateStrategy {
94        UpdateStrategy::default()
95    }
96
97    #[test]
98    fn cli_flag_wins() {
99        // --rebase given: beats .git/config merge and .gitmodules none.
100        let got = determine_update_strategy(
101            UpdateType::Rebase,
102            Some("merge"),
103            &UpdateStrategy {
104                kind: UpdateType::None,
105                command: None,
106            },
107            false,
108        )
109        .expect("CLI update strategy should be accepted");
110        assert_eq!(got.kind, UpdateType::Rebase);
111    }
112
113    #[test]
114    fn config_beats_gitmodules() {
115        let got = determine_update_strategy(
116            UpdateType::Unspecified,
117            Some("rebase"),
118            &UpdateStrategy {
119                kind: UpdateType::Merge,
120                command: None,
121            },
122            false,
123        )
124        .expect("configured update strategy should be accepted");
125        assert_eq!(got.kind, UpdateType::Rebase);
126    }
127
128    #[test]
129    fn gitmodules_used_when_no_config() {
130        let got = determine_update_strategy(
131            UpdateType::Unspecified,
132            None,
133            &UpdateStrategy {
134                kind: UpdateType::Merge,
135                command: None,
136            },
137            false,
138        )
139        .expect("gitmodules update strategy should be accepted");
140        assert_eq!(got.kind, UpdateType::Merge);
141    }
142
143    #[test]
144    fn default_is_checkout() {
145        let got = determine_update_strategy(UpdateType::Unspecified, None, &unspec(), false)
146            .expect("default update strategy should be accepted");
147        assert_eq!(got, checkout());
148    }
149
150    #[test]
151    fn command_from_config_carries_string() {
152        let got =
153            determine_update_strategy(UpdateType::Unspecified, Some("!false"), &unspec(), false)
154                .expect("custom update command should be accepted");
155        assert_eq!(got.kind, UpdateType::Command);
156        assert_eq!(got.command.as_deref(), Some("false"));
157    }
158
159    #[test]
160    fn just_cloned_downgrades_to_checkout() {
161        for kind in [UpdateType::Merge, UpdateType::Rebase, UpdateType::None] {
162            let got = determine_update_strategy(
163                UpdateType::Unspecified,
164                None,
165                &UpdateStrategy {
166                    kind,
167                    command: None,
168                },
169                true,
170            )
171            .expect("just-cloned strategy should be accepted");
172            assert_eq!(got.kind, UpdateType::Checkout, "downgrade {kind:?}");
173        }
174        // checkout / command are NOT downgraded.
175        let got =
176            determine_update_strategy(UpdateType::Unspecified, Some("!true"), &unspec(), true)
177                .expect("custom update command should not be downgraded");
178        assert_eq!(got.kind, UpdateType::Command);
179    }
180
181    #[test]
182    fn invalid_config_value_errors() {
183        let err =
184            determine_update_strategy(UpdateType::Unspecified, Some("bogus"), &unspec(), false)
185                .expect_err("invalid update strategy should error");
186        assert_eq!(err, "bogus");
187    }
188}