Skip to main content

grit_lib/
submodule_config.rs

1//! Submodule registration and activation (Git `submodule.c` parity for tooling).
2//!
3//! Used by `ls-files --recurse-submodules` to decide when to enter a gitlink.
4
5use 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/// Maps a submodule work tree path (as in the index / `.gitmodules`) to its submodule section name.
13#[derive(Debug, Clone)]
14pub struct SubmoduleRegistration {
15    /// `submodule.<name>` section name from `.gitmodules`.
16    pub name: String,
17    /// Normalized `submodule.<name>.path` (forward slashes, no trailing slash).
18    pub path: String,
19}
20
21/// Load `.gitmodules` mappings: path → submodule name.
22///
23/// Reads the work tree file when present; otherwise falls back to the `.gitmodules` blob at stage 0
24/// in `index` (sparse checkout / absent file), using `odb` to load the blob.
25pub 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
95/// Returns the `.gitmodules` submodule section name for a gitlink path, if registered.
96pub 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/// Git `is_submodule_active` / `is_tree_submodule_active` for `ls-files --recurse-submodules`.
108///
109/// Returns false when `module_name` is `None` (path not listed in `.gitmodules`).
110#[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}