Skip to content
Merged
2 changes: 1 addition & 1 deletion schemas/v1.0.0/submod_config_v1.0.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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());

Expand Down
57 changes: 44 additions & 13 deletions src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
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"
))
})
}
Comment on lines 404 to 423
}

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<Helper, toml::de::Error> = 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<Helper, toml::de::Error> = 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");
Expand Down
Loading