grit_lib/
precompose_config.rs1use std::fs;
6use std::path::{Path, PathBuf};
7
8use crate::config::{parse_config_parameters, ConfigSet};
9use crate::unicode_normalization::probe_filesystem_normalizes_nfd_to_nfc;
10
11fn parse_ceiling_directories_paths() -> Vec<PathBuf> {
12 let raw = match std::env::var("GIT_CEILING_DIRECTORIES") {
13 Ok(val) => val,
14 Err(_) => return Vec::new(),
15 };
16 if raw.is_empty() {
17 return Vec::new();
18 }
19 raw.split(':')
20 .filter(|s| !s.is_empty())
21 .filter_map(|s| {
22 let p = PathBuf::from(s);
23 if !p.is_absolute() {
24 return None;
25 }
26 Some(
27 p.canonicalize()
28 .unwrap_or_else(|_| PathBuf::from(s.trim_end_matches('/'))),
29 )
30 })
31 .collect()
32}
33
34fn path_for_ceiling_compare(path: &Path) -> String {
35 path.to_string_lossy().replace('\\', "/")
36}
37
38fn offset_1st_component(path: &str) -> usize {
39 if path.starts_with('/') {
40 1
41 } else {
42 0
43 }
44}
45
46fn longest_ancestor_length(path: &str, ceilings: &[String]) -> Option<usize> {
47 if path == "/" {
48 return None;
49 }
50 let mut max_len: Option<usize> = None;
51 for ceil in ceilings {
52 let mut len = ceil.len();
53 while len > 0 && ceil.as_bytes().get(len - 1) == Some(&b'/') {
54 len -= 1;
55 }
56 if len == 0 {
57 continue;
58 }
59 if path.len() <= len + 1 {
60 continue;
61 }
62 if !path.starts_with(&ceil[..len]) {
63 continue;
64 }
65 if path.as_bytes().get(len) != Some(&b'/') {
66 continue;
67 }
68 if path.as_bytes().get(len + 1).is_none() {
69 continue;
70 }
71 max_len = Some(max_len.map_or(len, |m| m.max(len)));
72 }
73 max_len
74}
75
76fn probe_git_dir_at(dir: &Path) -> Option<PathBuf> {
77 let dot_git = dir.join(".git");
78 if dot_git.is_dir() {
79 return Some(dot_git.canonicalize().unwrap_or(dot_git));
80 }
81 if dot_git.is_file() {
82 let content = fs::read_to_string(&dot_git).ok()?;
83 for line in content.lines() {
84 let line = line.trim();
85 if let Some(rest) = line.strip_prefix("gitdir:") {
86 let p = Path::new(rest.trim());
87 let resolved = if p.is_absolute() {
88 p.to_path_buf()
89 } else {
90 dir.join(p)
91 };
92 return Some(resolved.canonicalize().unwrap_or(resolved));
93 }
94 }
95 }
96 None
97}
98
99pub fn locate_git_dir_from_cwd(cwd: PathBuf) -> Option<PathBuf> {
101 let start_canon = cwd.canonicalize().unwrap_or(cwd);
102 let ceilings: Vec<String> = parse_ceiling_directories_paths()
103 .into_iter()
104 .map(|p| path_for_ceiling_compare(&p))
105 .collect();
106 let mut dir_buf = path_for_ceiling_compare(&start_canon);
107 let min_offset = offset_1st_component(&dir_buf);
108 let mut ceil_offset: isize = longest_ancestor_length(&dir_buf, &ceilings)
109 .map(|n| n as isize)
110 .unwrap_or(-1);
111 if ceil_offset < 0 {
112 ceil_offset = min_offset as isize - 2;
113 }
114
115 loop {
116 if let Some(gd) = probe_git_dir_at(Path::new(&dir_buf)) {
117 return Some(gd);
118 }
119
120 let mut offset: isize = dir_buf.len() as isize;
121 if offset <= min_offset as isize {
122 break;
123 }
124 loop {
125 offset -= 1;
126 if offset <= ceil_offset {
127 break;
128 }
129 if dir_buf
130 .as_bytes()
131 .get(offset as usize)
132 .is_some_and(|b| *b == b'/')
133 {
134 break;
135 }
136 }
137 if offset <= ceil_offset {
138 break;
139 }
140 let off_u = offset as usize;
141 let new_len = if off_u > min_offset {
142 off_u
143 } else {
144 min_offset
145 };
146 dir_buf.truncate(new_len);
147 }
148 None
149}
150
151#[must_use]
153pub fn read_core_precomposeunicode(git_dir: &Path) -> Option<bool> {
154 let path = git_dir.join("config");
155 let Ok(text) = fs::read_to_string(&path) else {
156 return None;
157 };
158 let mut in_core = false;
159 let mut last: Option<bool> = None;
160 for line in text.lines() {
161 let t = line.trim();
162 if t.starts_with('[') {
163 in_core = t.eq_ignore_ascii_case("[core]");
164 continue;
165 }
166 if !in_core {
167 continue;
168 }
169 let Some((k, v)) = t.split_once('=') else {
170 continue;
171 };
172 if !k.trim().eq_ignore_ascii_case("precomposeunicode") {
173 continue;
174 }
175 let v = v.trim();
176 last = Some(matches!(
177 v.to_ascii_lowercase().as_str(),
178 "true" | "yes" | "on" | "1"
179 ));
180 }
181 last
182}
183
184fn precompose_from_git_config_parameters() -> Option<bool> {
186 let Ok(raw) = std::env::var("GIT_CONFIG_PARAMETERS") else {
187 return None;
188 };
189 let mut last: Option<bool> = None;
190 for entry in parse_config_parameters(&raw) {
191 let Some((k, v)) = entry.split_once('=') else {
192 continue;
193 };
194 if !k.trim().eq_ignore_ascii_case("core.precomposeunicode") {
195 continue;
196 }
197 let v = v.trim();
198 last = Some(matches!(
199 v.to_ascii_lowercase().as_str(),
200 "true" | "yes" | "on" | "1"
201 ));
202 }
203 last
204}
205
206#[must_use]
212pub fn effective_core_precomposeunicode(git_dir: Option<&Path>) -> bool {
213 if let Some(v) = precompose_from_git_config_parameters() {
214 return v;
215 }
216 let Some(gd) = git_dir else {
217 return false;
218 };
219 ConfigSet::load(Some(gd), true)
220 .ok()
221 .and_then(|cfg| cfg.get_bool("core.precomposeunicode").and_then(|r| r.ok()))
222 .unwrap_or(false)
223}
224
225#[must_use]
231pub fn filesystem_nfd_nfc_aliases(git_dir: &Path) -> bool {
232 probe_filesystem_normalizes_nfd_to_nfc(git_dir).unwrap_or(false)
233}
234
235#[must_use]
237pub fn argv_precompose_enabled(git_dir: Option<&Path>) -> bool {
238 if !effective_core_precomposeunicode(git_dir) {
239 return false;
240 }
241 let Some(gd) = git_dir else {
242 return false;
243 };
244 filesystem_nfd_nfc_aliases(gd)
245}
246
247#[must_use]
253pub fn pathspec_precompose_enabled() -> bool {
254 static CACHE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
255 *CACHE.get_or_init(|| {
256 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
257 let gd = locate_git_dir_from_cwd(cwd);
258 effective_core_precomposeunicode(gd.as_deref())
259 })
260}