diff --git a/schemas/v1.0.0/submod_config_v1.0.0.json b/schemas/v1.0.0/submod_config_v1.0.0.json index ee5079c..6ce0094 100644 --- a/schemas/v1.0.0/submod_config_v1.0.0.json +++ b/schemas/v1.0.0/submod_config_v1.0.0.json @@ -58,7 +58,7 @@ }, "branch": { "type": "string", - "description": "Branch to track in the submodule. Defaults to the submodule's default branch (usually main or master).\nUse \".\" or the aliases \"current\", \"current-in-superproject\", \"superproject\", or \"super\" to track the superproject's current branch. **Do not use these strings as actual branch names in the submodule repository.** If you need to track a branch with one of these names, use the full branch name (e.g., \"refs/heads/current\")." + "description": "Branch to track in the submodule. Defaults to the submodule's default branch (usually main or master).\nUse \".\" or the aliases \"current\", \"current-in-super-project\", \"superproject\", or \"super\" to track the superproject's current branch. **Do not use these strings as actual branch names in the submodule repository.** If you need to track a branch with one of these names, use the full branch name (e.g., \"refs/heads/current\")." }, "sparse_paths": { "type": "array", diff --git a/src/config.rs b/src/config.rs index 8f3e373..1743117 100644 --- a/src/config.rs +++ b/src/config.rs @@ -526,7 +526,8 @@ impl SubmoduleEntry { .get("ignore") .and_then(|i| SerializableIgnore::from_gitmodules(i).ok()); let fetch_recurse = entries - .get("fetchRecurse") + .get("fetchRecurseSubmodules") + .or_else(|| entries.get("fetchRecurse")) .and_then(|fr| SerializableFetchRecurse::from_gitmodules(fr).ok()); let update = entries .get("update") @@ -1293,7 +1294,7 @@ mod tests { map.insert("branch".to_string(), "main".to_string()); map.insert("ignore".to_string(), "dirty".to_string()); map.insert("update".to_string(), "rebase".to_string()); - map.insert("fetchRecurse".to_string(), "true".to_string()); + map.insert("fetchRecurseSubmodules".to_string(), "true".to_string()); map.insert("active".to_string(), "true".to_string()); map.insert("shallow".to_string(), "true".to_string()); diff --git a/src/options.rs b/src/options.rs index 4e463c4..58f8fd5 100644 --- a/src/options.rs +++ b/src/options.rs @@ -402,20 +402,24 @@ impl Serialize for SerializableBranch { } impl<'de> Deserialize<'de> for SerializableBranch { - /// Deserialize from a plain string using the same logic as [`FromStr`]. - /// Accepts `"."`, `"current"`, `"current-in-super-project"`, `"current-in-superproject"`, - /// `"superproject"`, or `"super"` as - /// [`CurrentInSuperproject`](SerializableBranch::CurrentInSuperproject); all other strings - /// become [`Name`](SerializableBranch::Name). + /// Deserialize from a plain string, delegating to [`from_gitmodules`](GitmodulesConvert::from_gitmodules). + /// Accepts `"."`, `"current"`, `"current-in-super-project"`, `"current-in-superproject"`, `"superproject"`, or `"super"` + /// as [`CurrentInSuperproject`](SerializableBranch::CurrentInSuperproject); all other + /// non-empty, non-whitespace strings become [`Name`](SerializableBranch::Name). + /// Empty or whitespace-only strings are rejected with a deserialization error. fn deserialize>(deserializer: D) -> Result { let s = String::deserialize(deserializer)?; - let branch = match s.as_str() { - "." | "current" | "current-in-super-project" | "current-in-superproject" | "superproject" | "super" => { - SerializableBranch::CurrentInSuperproject - } - _ => SerializableBranch::Name(s), - }; - Ok(branch) + // Backward compatibility: accept the previously-documented alias + // "current-in-superproject" in addition to the spellings handled + // by `from_gitmodules`. + if s == "current-in-superproject" { + return Ok(SerializableBranch::CurrentInSuperproject); + } + SerializableBranch::from_gitmodules(&s).map_err(|_| { + serde::de::Error::custom(format!( + "invalid branch value: {s:?}; expected \".\", \"current\", \"current-in-super-project\", \"superproject\", \"super\", or a non-empty, non-whitespace branch name" + )) + }) } } @@ -452,7 +456,6 @@ impl GitmodulesConvert for SerializableBranch { || options == "current-in-super-project" || options == "superproject" || options == "super" - || options == SerializableBranch::current_in_superproject().unwrap_or_default() { return Ok(SerializableBranch::CurrentInSuperproject); } @@ -711,6 +714,34 @@ impl GixGit2Convert for SerializableUpdate { mod tests { use super::*; + #[test] + fn test_branch_deserialize_from_toml_rejects_empty_and_whitespace() { + // Use a wrapper struct because TOML top-level must be a table; + // SerializableBranch appears as a field value in real config files. + #[derive(Debug, serde::Deserialize)] + struct Helper { + branch: SerializableBranch, + } + + // Empty string should be rejected + let res_empty: Result = toml::from_str(r#"branch = """#); + assert!(res_empty.is_err(), "expected error for empty branch value"); + let err_empty = res_empty.unwrap_err().to_string(); + assert!( + err_empty.contains("invalid branch value"), + "error for empty branch value should contain context, got: {err_empty}" + ); + + // Whitespace-only string should be rejected + let res_ws: Result = toml::from_str(r#"branch = " ""#); + assert!(res_ws.is_err(), "expected error for whitespace-only branch value"); + let err_ws = res_ws.unwrap_err().to_string(); + assert!( + err_ws.contains("invalid branch value"), + "error for whitespace-only branch value should contain context, got: {err_ws}" + ); + } + #[test] fn test_serializable_ignore_gitmodules_key() { assert_eq!(SerializableIgnore::All.gitmodules_key(), "ignore");