use std::collections::HashMap;
use std::path::Path;
use crate::config::{canonical_key, ConfigFile, ConfigScope};
use crate::merge_diff::blob_oid_at_path;
use crate::objects::{parse_commit, ObjectId, ObjectKind};
use crate::odb::Odb;
use crate::repo::Repository;
use crate::rev_parse::resolve_revision;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SubmoduleInfo {
pub name: String,
pub path: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FetchRecurse {
None,
On,
Off,
OnDemand,
Error,
}
#[derive(Clone, Debug)]
struct SubmoduleBuild {
name: String,
path: Option<String>,
url: Option<String>,
fetch_recurse: FetchRecurse,
}
impl SubmoduleBuild {
fn new(name: String) -> Self {
Self {
name,
path: None,
url: None,
fetch_recurse: FetchRecurse::None,
}
}
}
#[derive(Default)]
pub struct SubmoduleConfigCache {
by_blob: HashMap<ObjectId, Vec<SubmoduleBuild>>,
path_index: HashMap<(ObjectId, String), String>,
name_index: HashMap<(ObjectId, String), SubmoduleBuild>,
}
impl SubmoduleConfigCache {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn submodule_from_path(
&mut self,
repo: &Repository,
treeish: Option<(ObjectId, ObjectId)>,
path: &str,
) -> Result<Option<SubmoduleInfo>, String> {
let gm_oid = self.gitmodules_oid_for_treeish(repo, treeish)?;
let gm_oid = match gm_oid {
Some(o) => o,
None => return Ok(None),
};
self.ensure_blob_parsed(repo, treeish, gm_oid)?;
let key_path = norm_path_key(path);
let name = self
.path_index
.get(&(gm_oid, key_path.clone()))
.cloned()
.or_else(|| self.path_index.get(&(gm_oid, path.to_string())).cloned());
let Some(name) = name else {
return Ok(None);
};
let path_out = self
.name_index
.get(&(gm_oid, name.clone()))
.and_then(|b| b.path.clone())
.unwrap_or_else(|| path.to_string());
Ok(Some(SubmoduleInfo {
name,
path: path_out,
}))
}
pub fn submodule_from_name(
&mut self,
repo: &Repository,
treeish: Option<(ObjectId, ObjectId)>,
name: &str,
) -> Result<Option<SubmoduleInfo>, String> {
let gm_oid = self.gitmodules_oid_for_treeish(repo, treeish)?;
let gm_oid = match gm_oid {
Some(o) => o,
None => return Ok(None),
};
self.ensure_blob_parsed(repo, treeish, gm_oid)?;
let b = self.name_index.get(&(gm_oid, name.to_string())).cloned();
let Some(b) = b else {
return Ok(None);
};
let Some(path) = b.path.clone() else {
return Ok(None);
};
Ok(Some(SubmoduleInfo { name: b.name, path }))
}
fn gitmodules_oid_for_treeish(
&self,
repo: &Repository,
treeish: Option<(ObjectId, ObjectId)>,
) -> Result<Option<ObjectId>, String> {
let Some((_rev, tree_oid)) = treeish else {
return self.gitmodules_oid_worktree_index_head(repo);
};
Ok(blob_oid_at_path(&repo.odb, &tree_oid, ".gitmodules"))
}
fn gitmodules_oid_worktree_index_head(
&self,
repo: &Repository,
) -> Result<Option<ObjectId>, String> {
let Some(wt) = repo.work_tree.as_ref() else {
return Ok(None);
};
if wt.join(".gitmodules").exists() {
return Ok(Some(ObjectId::zero()));
}
let index = repo.load_index().map_err(|e| e.to_string())?;
if let Some(ie) = index.get(b".gitmodules", 0) {
return Ok(Some(ie.oid));
}
let head_oid = crate::state::resolve_head(&repo.git_dir)
.map_err(|e| e.to_string())?
.oid()
.copied();
let Some(commit_oid) = head_oid else {
return Ok(None);
};
let obj = repo.odb.read(&commit_oid).map_err(|e| e.to_string())?;
if obj.kind != ObjectKind::Commit {
return Ok(None);
}
let commit = parse_commit(&obj.data).map_err(|e| e.to_string())?;
Ok(blob_oid_at_path(&repo.odb, &commit.tree, ".gitmodules"))
}
fn ensure_blob_parsed(
&mut self,
repo: &Repository,
treeish: Option<(ObjectId, ObjectId)>,
gitmodules_blob: ObjectId,
) -> Result<(), String> {
if self.by_blob.contains_key(&gitmodules_blob) {
return Ok(());
}
if gitmodules_blob.is_zero() {
let Some(wt) = repo.work_tree.as_ref() else {
self.by_blob.insert(gitmodules_blob, Vec::new());
return Ok(());
};
let path = wt.join(".gitmodules");
let text = if path.exists() {
std::fs::read_to_string(&path).map_err(|e| e.to_string())?
} else {
let index = repo.load_index().map_err(|e| e.to_string())?;
if let Some(ie) = index.get(b".gitmodules", 0) {
let obj = repo.odb.read(&ie.oid).map_err(|e| e.to_string())?;
if obj.kind != ObjectKind::Blob {
self.by_blob.insert(gitmodules_blob, Vec::new());
return Ok(());
}
String::from_utf8(obj.data).map_err(|e| e.to_string())?
} else {
let head_oid = crate::state::resolve_head(&repo.git_dir)
.ok()
.and_then(|h| h.oid().copied());
let Some(commit_oid) = head_oid else {
self.by_blob.insert(gitmodules_blob, Vec::new());
return Ok(());
};
let obj = repo.odb.read(&commit_oid).map_err(|e| e.to_string())?;
if obj.kind != ObjectKind::Commit {
self.by_blob.insert(gitmodules_blob, Vec::new());
return Ok(());
}
let commit = parse_commit(&obj.data).map_err(|e| e.to_string())?;
let Some(blob_oid) = blob_oid_at_path(&repo.odb, &commit.tree, ".gitmodules")
else {
self.by_blob.insert(gitmodules_blob, Vec::new());
return Ok(());
};
let blob = repo.odb.read(&blob_oid).map_err(|e| e.to_string())?;
if blob.kind != ObjectKind::Blob {
self.by_blob.insert(gitmodules_blob, Vec::new());
return Ok(());
}
String::from_utf8(blob.data).map_err(|e| e.to_string())?
}
};
self.ingest_gitmodules_blob(repo, None, None, ObjectId::zero(), &text, true)?;
return Ok(());
}
let obj = repo
.odb
.read(&gitmodules_blob)
.map_err(|e| format!("failed to read .gitmodules blob: {e}"))?;
if obj.kind != ObjectKind::Blob {
self.by_blob.insert(gitmodules_blob, Vec::new());
return Ok(());
}
let text = String::from_utf8(obj.data).map_err(|e| e.to_string())?;
let commit_for_warn = treeish.map(|(rev, _)| rev).filter(|o| !o.is_zero());
self.ingest_gitmodules_blob(
repo,
commit_for_warn,
treeish.map(|(rev, _)| rev),
gitmodules_blob,
&text,
false,
)?;
Ok(())
}
fn ingest_gitmodules_blob(
&mut self,
repo: &Repository,
treeish_for_warning: Option<ObjectId>,
treeish_for_blob_spec: Option<ObjectId>,
gitmodules_blob: ObjectId,
content: &str,
die_on_bad_fetch_recurse: bool,
) -> Result<(), String> {
if self.by_blob.contains_key(&gitmodules_blob) {
return Ok(());
}
let (git_entries, bad_line) = ConfigFile::parse_gitmodules_best_effort(
Path::new(".gitmodules"),
content,
ConfigScope::Local,
);
if let Some(line) = bad_line {
eprintln!(
"{}",
gitmodules_config_error(
repo,
treeish_for_blob_spec,
gitmodules_blob,
line,
"bad config",
)
);
}
let mut by_name: HashMap<String, SubmoduleBuild> = HashMap::new();
for ent in &git_entries {
let Some((name, var)) = submodule_name_and_var(&ent.key) else {
continue;
};
if !check_submodule_name_ok(&name) {
eprintln!("warning: ignoring suspicious submodule name: {name}");
continue;
}
let entry = by_name
.entry(name.clone())
.or_insert_with(|| SubmoduleBuild::new(name.clone()));
match var.as_str() {
"path" => {
let Some(value) = ent.value.as_deref() else {
return Err(gitmodules_config_error(
repo,
treeish_for_blob_spec,
gitmodules_blob,
ent.line,
"bad config",
));
};
if crate::gitmodules::looks_like_command_line_option(value) {
eprintln!(
"warning: ignoring '{}' which may be interpreted as a command-line option: {value}",
ent.key
);
continue;
}
let overwrite = gitmodules_blob.is_zero();
if entry.path.is_some() && !overwrite {
warn_multiple_config(treeish_for_warning, &entry.name, "path");
} else {
if let Some(old) = &entry.path {
self.path_index_remove(gitmodules_blob, old);
}
entry.path = Some(value.to_string());
self.path_index_insert(gitmodules_blob, value, entry.name.clone());
}
}
"url" => {
let Some(value) = ent.value.as_deref() else {
return Err(gitmodules_config_error(
repo,
treeish_for_blob_spec,
gitmodules_blob,
ent.line,
"bad config",
));
};
if crate::gitmodules::looks_like_command_line_option(value) {
eprintln!(
"warning: ignoring '{}' which may be interpreted as a command-line option: {value}",
ent.key
);
continue;
}
let overwrite = gitmodules_blob.is_zero();
if entry.url.is_some() && !overwrite {
warn_multiple_config(treeish_for_warning, &entry.name, "url");
} else {
entry.url = Some(value.to_string());
}
}
"fetchrecursesubmodules" => {
let value = ent.value.as_deref().unwrap_or("");
let parsed = parse_fetch_recurse(value, die_on_bad_fetch_recurse);
let parsed = parsed?;
let overwrite = gitmodules_blob.is_zero();
if entry.fetch_recurse != FetchRecurse::None && !overwrite {
warn_multiple_config(
treeish_for_warning,
&entry.name,
"fetchrecursesubmodules",
);
} else {
entry.fetch_recurse = parsed;
}
}
"ignore" => {
let Some(value) = ent.value.as_deref() else {
return Err(gitmodules_config_error(
repo,
treeish_for_blob_spec,
gitmodules_blob,
ent.line,
"bad config",
));
};
let _ = value;
}
"branch" => {
let Some(_value) = ent.value.as_deref() else {
return Err(gitmodules_config_error(
repo,
treeish_for_blob_spec,
gitmodules_blob,
ent.line,
"bad config",
));
};
}
"update" | "shallow" => {}
_ => {}
}
}
let list: Vec<SubmoduleBuild> = by_name.into_values().collect();
for b in &list {
self.name_index
.insert((gitmodules_blob, b.name.clone()), b.clone());
}
self.by_blob.insert(gitmodules_blob, list);
Ok(())
}
fn path_index_insert(&mut self, blob: ObjectId, path: &str, name: String) {
let key = norm_path_key(path);
self.path_index.insert((blob, key), name);
}
fn path_index_remove(&mut self, blob: ObjectId, path: &str) {
let key = norm_path_key(path);
self.path_index.remove(&(blob, key));
}
pub fn print_config_from_nested_gitmodules(
_super_repo: &Repository,
super_work_tree: &Path,
submodule_path: &str,
key: &str,
) -> Result<(), String> {
let wanted = canonical_key(key).map_err(|e| e.to_string())?;
let sub_work = super_work_tree.join(submodule_path);
let sub_git = if sub_work.join(".git").is_file() {
let gf = std::fs::read_to_string(sub_work.join(".git"))
.map_err(|e| format!("read gitfile: {e}"))?;
let line = gf.lines().next().unwrap_or("").trim();
let Some(rest) = line.strip_prefix("gitdir:") else {
return Err("invalid gitfile".into());
};
let rest = rest.trim();
let p = Path::new(rest);
if p.is_absolute() {
p.to_path_buf()
} else {
sub_work.join(rest)
}
} else {
sub_work.join(".git")
};
let sub_repo = Repository::open(&sub_git, Some(&sub_work))
.map_err(|e| format!("open submodule repo: {e}"))?;
let gm_path = sub_work.join(".gitmodules");
let (content, _) = if gm_path.exists() {
let c = std::fs::read_to_string(&gm_path).map_err(|e| e.to_string())?;
(c, gm_path)
} else {
let index = sub_repo.load_index().map_err(|e| e.to_string())?;
if let Some(ie) = index.get(b".gitmodules", 0) {
let obj = sub_repo.odb.read(&ie.oid).map_err(|e| e.to_string())?;
if obj.kind != ObjectKind::Blob {
return Ok(());
}
let c = String::from_utf8(obj.data).map_err(|e| e.to_string())?;
(c, gm_path)
} else {
let head_oid = crate::state::resolve_head(&sub_repo.git_dir)
.ok()
.and_then(|h| h.oid().copied());
let Some(commit_oid) = head_oid else {
return Ok(());
};
let obj = sub_repo.odb.read(&commit_oid).map_err(|e| e.to_string())?;
if obj.kind != ObjectKind::Commit {
return Ok(());
}
let commit = parse_commit(&obj.data).map_err(|e| e.to_string())?;
let Some(blob_oid) = blob_oid_at_path(&sub_repo.odb, &commit.tree, ".gitmodules")
else {
return Ok(());
};
let blob = sub_repo.odb.read(&blob_oid).map_err(|e| e.to_string())?;
if blob.kind != ObjectKind::Blob {
return Ok(());
}
let c = String::from_utf8(blob.data).map_err(|e| e.to_string())?;
(c, gm_path)
}
};
let cfg = ConfigFile::parse(Path::new(".gitmodules"), &content, ConfigScope::Local)
.map_err(|e| e.to_string())?;
for e in &cfg.entries {
if e.key == wanted {
if let Some(v) = &e.value {
println!("{v}");
}
}
}
Ok(())
}
}
fn norm_path_key(path: &str) -> String {
path.replace('\\', "/")
}
fn warn_multiple_config(treeish: Option<ObjectId>, name: &str, option: &str) {
let commit_string = treeish
.map(|o| o.to_hex())
.unwrap_or_else(|| "WORKTREE".to_string());
eprintln!(
"warning: {commit_string}:.gitmodules, multiple configurations found for \
'submodule.{name}.{option}'. Skipping second one!"
);
}
fn gitmodules_config_error(
repo: &Repository,
treeish_for_blob: Option<ObjectId>,
gitmodules_blob: ObjectId,
line: usize,
msg: &str,
) -> String {
if gitmodules_blob.is_zero() {
format!("{msg} line {line} in file .gitmodules")
} else {
let spec = submodule_blob_spec(repo, treeish_for_blob, gitmodules_blob);
format!("{msg} line {line} in submodule-blob {spec}")
}
}
fn submodule_blob_spec(
repo: &Repository,
treeish_for_blob: Option<ObjectId>,
blob: ObjectId,
) -> String {
let fallback = format!("{}:.gitmodules", blob.to_hex());
let Some(treeish) = treeish_for_blob else {
return fallback;
};
let Ok(obj) = repo.odb.read(&treeish) else {
return fallback;
};
let commit_oid = match obj.kind {
ObjectKind::Commit => treeish,
ObjectKind::Tree => {
let Ok(c) = find_commit_containing_tree(repo, treeish) else {
return fallback;
};
c
}
_ => return fallback,
};
format!("{}:.gitmodules", commit_oid.to_hex())
}
fn find_commit_containing_tree(repo: &Repository, tree_oid: ObjectId) -> Result<ObjectId, ()> {
let mut stack = vec![format!("HEAD^{{commit}}")];
for name in ["HEAD", "refs/heads/master", "refs/heads/main"] {
stack.push(name.to_string());
}
for spec in stack {
let Ok(oid) = resolve_revision(repo, spec.as_str()) else {
continue;
};
let Ok(obj) = repo.odb.read(&oid) else {
continue;
};
if obj.kind != ObjectKind::Commit {
continue;
}
let Ok(c) = parse_commit(&obj.data) else {
continue;
};
if tree_contains_oid(&repo.odb, c.tree, tree_oid)? {
return Ok(oid);
}
}
Err(())
}
fn tree_contains_oid(odb: &Odb, tree: ObjectId, target: ObjectId) -> Result<bool, ()> {
let obj = odb.read(&tree).map_err(|_| ())?;
if obj.kind != ObjectKind::Tree {
return Ok(false);
}
let entries = crate::objects::parse_tree(&obj.data).map_err(|_| ())?;
for e in entries {
if e.oid == target {
return Ok(true);
}
if e.mode == 0o040000 && tree_contains_oid(odb, e.oid, target)? {
return Ok(true);
}
}
Ok(false)
}
fn submodule_name_and_var(key: &str) -> Option<(String, String)> {
let rest = key.strip_prefix("submodule.")?;
let dot = rest.rfind('.')?;
let name = rest[..dot].to_string();
let var = rest[dot + 1..].to_string();
if name.is_empty() {
return None;
}
Some((name, var))
}
fn check_submodule_name_ok(name: &str) -> bool {
if name.is_empty() {
return false;
}
let b = name.as_bytes();
if b.len() >= 2
&& b[0] == b'.'
&& b[1] == b'.'
&& (b.len() == 2 || b[2] == b'/' || b[2] == b'\\')
{
return false;
}
let mut i = 0usize;
while i < b.len() {
let c = b[i];
i += 1;
if c == b'/' || c == b'\\' {
let j = i;
if b.len() >= j + 2
&& b[j] == b'.'
&& b[j + 1] == b'.'
&& (j + 2 >= b.len() || b[j + 2] == b'/' || b[j + 2] == b'\\')
{
return false;
}
}
}
true
}
fn parse_fetch_recurse(value: &str, die_on_error: bool) -> Result<FetchRecurse, String> {
let v = value.trim();
match crate::config::parse_bool(v) {
Ok(true) => return Ok(FetchRecurse::On),
Ok(false) => return Ok(FetchRecurse::Off),
Err(_) => {}
}
if v.eq_ignore_ascii_case("on-demand") {
return Ok(FetchRecurse::OnDemand);
}
if die_on_error {
Err(format!(
"fatal: bad submodule.fetchRecurseSubmodules argument: '{v}'"
))
} else {
Ok(FetchRecurse::Error)
}
}