diff --git a/lib/src/git.rs b/lib/src/git.rs index cf8ce0a44..0907c27fc 100644 --- a/lib/src/git.rs +++ b/lib/src/git.rs @@ -14,10 +14,12 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::default::Default; +use std::io::Read; use std::path::PathBuf; use git2::Oid; use itertools::Itertools; +use tempfile::NamedTempFile; use thiserror::Error; use crate::backend::{CommitId, ObjectId}; @@ -777,3 +779,83 @@ pub struct Progress { pub bytes_downloaded: Option, pub overall: f32, } + +#[derive(Default)] +struct PartialSubmoduleConfig { + path: Option, + url: Option, +} + +/// Represents configuration from a submodule, e.g. in .gitmodules +/// This doesn't include all possible fields, only the ones we care about +#[derive(Debug, PartialEq)] +pub struct SubmoduleConfig { + pub name: String, + pub path: String, + pub url: String, +} + +#[derive(Error, Debug)] +pub enum GitConfigParseError { + #[error("Unexpected io error when parsing config: {0}")] + IoError(#[from] std::io::Error), + #[error("Unexpected git error when parsing config: {0}")] + InternalGitError(#[from] git2::Error), +} + +pub fn parse_gitmodules( + config: &mut dyn Read, +) -> Result, GitConfigParseError> { + // git2 can only read from a path, so set one up + let mut temp_file = NamedTempFile::new()?; + std::io::copy(config, &mut temp_file)?; + let path = temp_file.into_temp_path(); + let git_config = git2::Config::open(&path)?; + // Partial config value for each submodule name + let mut partial_configs: BTreeMap = BTreeMap::new(); + + let entries = git_config.entries(Some(r#"submodule\..+\."#))?; + entries.for_each(|entry| { + let (config_name, config_value) = match (entry.name(), entry.value()) { + // Reject non-utf8 entries + (Some(name), Some(value)) => (name, value), + _ => return, + }; + + // config_name is of the form submodule.. + let (submod_name, submod_var) = config_name + .strip_prefix("submodule.") + .unwrap() + .split_once('.') + .unwrap(); + + let map_entry = partial_configs.entry(submod_name.to_string()).or_default(); + + match (submod_var.to_ascii_lowercase().as_str(), &map_entry) { + // TODO Git warns when a duplicate config entry is found, we should + // consider doing the same. + ("path", PartialSubmoduleConfig { path: None, .. }) => { + map_entry.path = Some(config_value.to_string()) + } + ("url", PartialSubmoduleConfig { url: None, .. }) => { + map_entry.url = Some(config_value.to_string()) + } + _ => (), + }; + })?; + + let ret = partial_configs + .into_iter() + .filter_map(|(name, val)| { + Some(( + name.clone(), + SubmoduleConfig { + name, + path: val.path?, + url: val.url?, + }, + )) + }) + .collect(); + Ok(ret) +} diff --git a/lib/tests/test_git.rs b/lib/tests/test_git.rs index 11cec346e..a1452a9f5 100644 --- a/lib/tests/test_git.rs +++ b/lib/tests/test_git.rs @@ -25,7 +25,7 @@ use jujutsu_lib::backend::{ use jujutsu_lib::commit::Commit; use jujutsu_lib::commit_builder::CommitBuilder; use jujutsu_lib::git; -use jujutsu_lib::git::{GitFetchError, GitPushError, GitRefUpdate}; +use jujutsu_lib::git::{GitFetchError, GitPushError, GitRefUpdate, SubmoduleConfig}; use jujutsu_lib::git_backend::GitBackend; use jujutsu_lib::op_store::{BranchTarget, RefTarget}; use jujutsu_lib::repo::{MutableRepo, ReadonlyRepo, Repo}; @@ -2184,3 +2184,56 @@ fn create_rooted_commit<'repo>( .set_author(signature.clone()) .set_committer(signature) } + +#[test] +fn test_parse_gitmodules() { + let result = git::parse_gitmodules( + &mut r#" +[submodule "wellformed"] +url = https://github.com/martinvonz/jj +path = mod +update = checkout # Extraneous config + +[submodule "uppercase"] +URL = https://github.com/martinvonz/jj +PATH = mod2 + +[submodule "repeated_keys"] +url = https://github.com/martinvonz/jj +path = mod3 +url = https://github.com/chooglen/jj +path = mod4 + +# The following entries aren't expected in a well-formed .gitmodules +[submodule "missing_url"] +path = mod + +[submodule] +ignoreThisSection = foo + +[randomConfig] +ignoreThisSection = foo +"# + .as_bytes(), + ) + .unwrap(); + let expected = btreemap! { + "wellformed".to_string() => SubmoduleConfig { + name: "wellformed".to_string(), + url: "https://github.com/martinvonz/jj".to_string(), + path: "mod".to_string(), + }, + "uppercase".to_string() => SubmoduleConfig { + name: "uppercase".to_string(), + url: "https://github.com/martinvonz/jj".to_string(), + path: "mod2".to_string(), + }, + "repeated_keys".to_string() => SubmoduleConfig { + name: "repeated_keys".to_string(), + url: "https://github.com/martinvonz/jj".to_string(), + path: "mod3".to_string(), + }, + }; + + assert_eq!(result, expected); +}