grit_lib/
submodule_active.rs1use std::path::Path;
7
8use crate::config::{ConfigFile, ConfigScope, ConfigSet};
9use crate::index::{Index, MODE_GITLINK};
10use crate::objects::ObjectKind;
11use crate::pathspec::{matches_pathspec_with_context, PathspecMatchContext};
12use crate::repo::Repository;
13use crate::wildmatch::wildmatch;
14
15fn config_has_submodule_active_key(cfg: &ConfigSet) -> bool {
17 cfg.has_key("submodule.active")
18}
19
20fn submodule_active_pattern_values(
25 cfg: &ConfigSet,
26) -> std::result::Result<Option<Vec<String>>, String> {
27 if !config_has_submodule_active_key(cfg) {
28 return Ok(None);
29 }
30 let values = cfg.get_all("submodule.active");
31 if values.is_empty() {
32 return Ok(Some(Vec::new()));
33 }
34 for v in &values {
35 if v.is_empty() {
36 return Err("missing value for 'submodule.active'".to_string());
37 }
38 }
39 Ok(Some(values))
40}
41
42#[must_use]
48pub fn submodule_add_should_set_active(repo: &Repository, sm_path: &str) -> bool {
49 let Ok(cfg) = ConfigSet::load(Some(&repo.git_dir), true) else {
50 return true;
51 };
52 let path = sm_path.replace('\\', "/");
53 match submodule_active_pattern_values(&cfg) {
54 Ok(None) => true,
55 Err(_) => true,
56 Ok(Some(patterns)) => {
57 let matched = patterns
58 .iter()
59 .any(|p| wildmatch(p.trim().as_bytes(), path.as_bytes(), 0));
60 !matched
61 }
62 }
63}
64
65fn read_gitmodules_text(
66 repo: &Repository,
67 work_tree: &Path,
68) -> crate::error::Result<Option<String>> {
69 let gitmodules_path = work_tree.join(".gitmodules");
70 if gitmodules_path.exists() {
71 let content = std::fs::read_to_string(&gitmodules_path).map_err(crate::error::Error::Io)?;
72 return Ok(Some(content));
73 }
74 let index = repo.load_index()?;
75 let Some(ie) = index.get(b".gitmodules", 0) else {
76 return Ok(None);
77 };
78 let obj = repo.odb.read(&ie.oid)?;
79 if obj.kind != ObjectKind::Blob {
80 return Ok(None);
81 }
82 String::from_utf8(obj.data)
83 .map(Some)
84 .map_err(|e| crate::error::Error::ConfigError(format!("invalid .gitmodules utf-8: {e}")))
85}
86
87pub fn submodule_name_for_path(
92 repo: &Repository,
93 path: &str,
94) -> crate::error::Result<Option<String>> {
95 let Some(wt) = repo.work_tree.as_ref() else {
96 return Ok(None);
97 };
98 let Some(content) = read_gitmodules_text(repo, wt)? else {
99 return Ok(None);
100 };
101 let config = ConfigFile::parse(&wt.join(".gitmodules"), &content, ConfigScope::Local)?;
102 let path_norm = path.replace('\\', "/");
103
104 #[derive(Default)]
105 struct ModuleFields {
106 path: Option<String>,
107 }
108 let mut modules: std::collections::BTreeMap<String, ModuleFields> =
109 std::collections::BTreeMap::new();
110
111 for entry in &config.entries {
112 let key = &entry.key;
113 if !key.starts_with("submodule.") {
114 continue;
115 }
116 let rest = &key["submodule.".len()..];
117 let Some(last_dot) = rest.rfind('.') else {
118 continue;
119 };
120 let name = &rest[..last_dot];
121 let var = &rest[last_dot + 1..];
122 if var == "path" {
123 modules.entry(name.to_string()).or_default().path = entry.value.clone();
124 }
125 }
126
127 for (name, f) in modules {
128 if let Some(p) = f.path {
129 let p_norm = p.replace('\\', "/");
130 if p_norm == path_norm {
131 return Ok(Some(name));
132 }
133 }
134 }
135 Ok(None)
136}
137
138fn parse_pathspec_exclude(spec: &str) -> (bool, &str) {
139 let s = spec.trim();
140 if let Some(rest) = s.strip_prefix(":!") {
141 return (true, rest);
142 }
143 if let Some(rest) = s.strip_prefix(":^") {
144 return (true, rest);
145 }
146 if let Some(inner) = s.strip_prefix(":(exclude)") {
147 return (true, inner);
148 }
149 if let Some(inner) = s.strip_prefix(":(exclude,") {
150 if let Some(close) = inner.find(')') {
151 return (true, &inner[close + 1..]);
152 }
153 }
154 (false, s)
155}
156
157fn index_gitlink_match_for_path(index: &Index, path: &str) -> Option<PathspecMatchContext> {
158 for e in &index.entries {
159 if e.stage() != 0 {
160 continue;
161 }
162 if e.mode != MODE_GITLINK {
163 continue;
164 }
165 let name = String::from_utf8_lossy(&e.path);
166 let name = name.replace('\\', "/");
167 if name == path {
168 return Some(PathspecMatchContext {
169 is_directory: false,
170 is_git_submodule: true,
171 });
172 }
173 }
174 None
175}
176
177fn spec_matches_submodule_path(
178 _index: &Index,
179 spec: &str,
180 path: &str,
181 ctx: PathspecMatchContext,
182) -> bool {
183 let (is_exclude, pattern_src) = parse_pathspec_exclude(spec);
184 let pattern = pattern_src.trim();
185 if pattern.is_empty() && !is_exclude {
186 return false;
187 }
188 matches_pathspec_with_context(pattern, path, ctx)
189}
190
191pub fn submodule_active_pathspec_match(
195 index: &Index,
196 specs: &[String],
197 path: &str,
198) -> std::result::Result<bool, String> {
199 for v in specs {
200 if v.is_empty() {
201 return Err("error: missing value for 'submodule.active'".to_string());
202 }
203 }
204 let ctx = index_gitlink_match_for_path(index, path).unwrap_or(PathspecMatchContext {
205 is_directory: false,
206 is_git_submodule: true,
207 });
208
209 let positive = specs
210 .iter()
211 .any(|s| !parse_pathspec_exclude(s).0 && spec_matches_submodule_path(index, s, path, ctx));
212 if !positive {
213 return Ok(false);
214 }
215 let excluded = specs
216 .iter()
217 .any(|s| parse_pathspec_exclude(s).0 && spec_matches_submodule_path(index, s, path, ctx));
218 Ok(!excluded)
219}
220
221pub fn is_submodule_active(repo: &Repository, path: &str) -> std::result::Result<bool, String> {
226 let path_norm = path.replace('\\', "/");
227 let Some(name) = submodule_name_for_path(repo, &path_norm).map_err(|e| e.to_string())? else {
228 return Ok(false);
229 };
230
231 let cfg = ConfigSet::load(Some(&repo.git_dir), true).map_err(|e| e.to_string())?;
232 let per_key = format!("submodule.{name}.active");
233 if let Some(res) = cfg.get_bool(&per_key) {
234 let b = res.map_err(|_| format!("invalid boolean for '{per_key}'"))?;
235 return Ok(b);
236 }
237
238 if config_has_submodule_active_key(&cfg) {
239 let values = cfg.get_all("submodule.active");
240 let index = repo.load_index().map_err(|e| e.to_string())?;
241 return submodule_active_pathspec_match(&index, &values, &path_norm);
242 }
243
244 let url_key = format!("submodule.{name}.url");
245 Ok(cfg.get(&url_key).is_some())
246}