grit_lib/
submodule_config.rs1use std::path::Path;
6
7use crate::config::{ConfigFile, ConfigScope, ConfigSet};
8use crate::index::Index;
9use crate::odb::Odb;
10use crate::pathspec::pathspec_matches;
11
12#[derive(Debug, Clone)]
14pub struct SubmoduleRegistration {
15 pub name: String,
17 pub path: String,
19}
20
21pub fn load_submodule_registrations(
26 work_tree: &Path,
27 index: Option<&Index>,
28 odb: Option<&Odb>,
29) -> Vec<SubmoduleRegistration> {
30 let gitmodules_path = work_tree.join(".gitmodules");
31 let content = if gitmodules_path.exists() {
32 let Some(s) = std::fs::read_to_string(&gitmodules_path).ok() else {
33 return Vec::new();
34 };
35 s
36 } else if let (Some(ix), Some(db)) = (index, odb) {
37 let Some(ie) = ix.get(b".gitmodules", 0) else {
38 return Vec::new();
39 };
40 let Ok(obj) = db.read(&ie.oid) else {
41 return Vec::new();
42 };
43 if obj.kind != crate::objects::ObjectKind::Blob {
44 return Vec::new();
45 }
46 let Some(s) = String::from_utf8(obj.data).ok() else {
47 return Vec::new();
48 };
49 s
50 } else {
51 return Vec::new();
52 };
53
54 let Ok(config) = ConfigFile::parse(&gitmodules_path, &content, ConfigScope::Local) else {
55 return Vec::new();
56 };
57 #[derive(Default)]
58 struct Fields {
59 path: Option<String>,
60 url: Option<String>,
61 }
62 let mut by_name: std::collections::BTreeMap<String, Fields> = std::collections::BTreeMap::new();
63 for entry in &config.entries {
64 let key = &entry.key;
65 if !key.starts_with("submodule.") {
66 continue;
67 }
68 let rest = &key["submodule.".len()..];
69 let Some(dot) = rest.rfind('.') else {
70 continue;
71 };
72 let name = &rest[..dot];
73 let var = &rest[dot + 1..];
74 let slot = by_name.entry(name.to_string()).or_default();
75 match var {
76 "path" => slot.path = entry.value.clone(),
77 "url" => slot.url = entry.value.clone(),
78 _ => {}
79 }
80 }
81
82 let mut out = Vec::new();
83 for (name, f) in by_name {
84 if let (Some(path), Some(_url)) = (f.path, f.url) {
85 let path = path.replace('\\', "/");
86 let path = path.trim_end_matches('/').to_string();
87 if !path.is_empty() {
88 out.push(SubmoduleRegistration { name, path });
89 }
90 }
91 }
92 out
93}
94
95pub fn submodule_name_for_path<'a>(
97 registrations: &'a [SubmoduleRegistration],
98 path: &str,
99) -> Option<&'a str> {
100 let path = path.replace('\\', "/");
101 registrations
102 .iter()
103 .find(|r| r.path == path)
104 .map(|r| r.name.as_str())
105}
106
107#[must_use]
111pub fn is_submodule_active(
112 config: &ConfigSet,
113 module_name: Option<&str>,
114 submodule_path: &str,
115) -> bool {
116 let Some(name) = module_name else {
117 return false;
118 };
119
120 let active_key = format!("submodule.{name}.active");
121 if let Some(res) = config.get_bool(&active_key) {
122 return res.unwrap_or(false);
123 }
124
125 let patterns = config.get_all("submodule.active");
126 if !patterns.is_empty() {
127 return patterns.iter().any(|p| pathspec_matches(p, submodule_path));
128 }
129
130 let url_key = format!("submodule.{name}.url");
131 config.get(&url_key).is_some()
132}