1use std::ffi::OsString;
7
8use thiserror::Error;
9
10#[cfg(unix)]
11use crate::commit_encoding::decode_bytes;
12use crate::config::ConfigSet;
13use crate::ident_config::ident_default_name;
14
15pub trait IdentityEnv {
17 fn var(&self, key: &str) -> Option<String>;
19
20 fn var_os(&self, key: &str) -> Option<OsString>;
22}
23
24#[derive(Clone, Copy, Debug, Default)]
26pub struct SystemIdentityEnv;
27
28impl IdentityEnv for SystemIdentityEnv {
29 fn var(&self, key: &str) -> Option<String> {
30 std::env::var(key).ok()
31 }
32
33 fn var_os(&self, key: &str) -> Option<OsString> {
34 std::env::var_os(key)
35 }
36}
37
38#[derive(Clone, Debug, PartialEq, Eq)]
43pub enum GitIdentityNameEnv {
44 Unset,
46 Set(String),
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum IdentRole {
53 Author,
55 Committer,
57}
58
59impl IdentRole {
60 fn env_name_key(self) -> &'static str {
61 match self {
62 IdentRole::Author => "GIT_AUTHOR_NAME",
63 IdentRole::Committer => "GIT_COMMITTER_NAME",
64 }
65 }
66
67 fn env_email_key(self) -> &'static str {
68 match self {
69 IdentRole::Author => "GIT_AUTHOR_EMAIL",
70 IdentRole::Committer => "GIT_COMMITTER_EMAIL",
71 }
72 }
73
74 fn config_name_key(self) -> &'static str {
75 match self {
76 IdentRole::Author => "author.name",
77 IdentRole::Committer => "committer.name",
78 }
79 }
80
81 fn config_email_key(self) -> &'static str {
82 match self {
83 IdentRole::Author => "author.email",
84 IdentRole::Committer => "committer.email",
85 }
86 }
87
88 #[must_use]
90 pub fn missing_email_hint(self) -> &'static str {
91 match self {
92 IdentRole::Author => "Author identity unknown",
93 IdentRole::Committer => "Committer identity unknown",
94 }
95 }
96}
97
98#[derive(Clone, Debug, Error, PartialEq, Eq)]
100pub enum IdentityError {
101 #[error("email auto-detection is disabled (user.useConfigOnly) and no configured email is available")]
107 AutoDetectionDisabled {
108 role: IdentRole,
110 },
111 #[error("empty ident name (for <{email}>) not allowed")]
113 EmptyName {
114 email: String,
116 role: IdentRole,
118 },
119 #[error("invalid ident name: '{name}'")]
121 InvalidName {
122 name: String,
124 },
125}
126
127#[must_use]
129pub fn read_git_identity_name_env_with<E: IdentityEnv>(env: &E, key: &str) -> GitIdentityNameEnv {
130 let Some(os) = env.var_os(key) else {
131 return GitIdentityNameEnv::Unset;
132 };
133 #[cfg(unix)]
134 {
135 use std::os::unix::ffi::OsStrExt;
136 let bytes = os.as_bytes();
137 let s = if std::str::from_utf8(bytes).is_ok() {
138 String::from_utf8_lossy(bytes).into_owned()
139 } else {
140 decode_bytes(Some("ISO8859-1"), bytes)
141 };
142 GitIdentityNameEnv::Set(s.trim().to_owned())
143 }
144 #[cfg(not(unix))]
145 {
146 let s = os.to_str().map(|t| t.trim().to_owned()).unwrap_or_default();
147 GitIdentityNameEnv::Set(s)
148 }
149}
150
151#[must_use]
157pub fn read_git_identity_name_from_env_with<E: IdentityEnv>(env: &E, key: &str) -> Option<String> {
158 match read_git_identity_name_env_with(env, key) {
159 GitIdentityNameEnv::Unset => None,
160 GitIdentityNameEnv::Set(s) if s.is_empty() => None,
161 GitIdentityNameEnv::Set(s) => Some(s),
162 }
163}
164
165fn use_config_only(config: &ConfigSet) -> bool {
166 match config.get_bool("user.useConfigOnly") {
167 Some(Ok(b)) => b,
168 Some(Err(_)) => false,
169 None => false,
170 }
171}
172
173fn config_mail_given(config: &ConfigSet) -> bool {
174 ["user.email", "author.email", "committer.email"]
175 .iter()
176 .any(|key| config.get(key).is_some_and(|v| !v.trim().is_empty()))
177}
178
179fn ident_name_has_non_crud(name: &str) -> bool {
180 name.chars().any(|c| {
181 let o = c as u32;
182 !(o <= 32
183 || c == ','
184 || c == ':'
185 || c == ';'
186 || c == '<'
187 || c == '>'
188 || c == '"'
189 || c == '\\'
190 || c == '\'')
191 })
192}
193
194fn synthetic_email_with<E: IdentityEnv>(env: &E) -> String {
195 let user = env
196 .var("USER")
197 .or_else(|| env.var("USERNAME"))
198 .unwrap_or_else(|| "unknown".to_owned());
199 let host = env.var("HOSTNAME").unwrap_or_else(|| "unknown".to_owned());
200 let domain = if host.contains('.') {
201 host
202 } else {
203 format!("{host}.(none)")
204 };
205 format!("{user}@{domain}")
206}
207
208fn resolve_email_inner_with<E: IdentityEnv>(
209 env: &E,
210 config: &ConfigSet,
211 role: IdentRole,
212 honor_use_config_only: bool,
213) -> Result<String, IdentityError> {
214 if let Some(v) = env.var(role.env_email_key()) {
215 let t = v.trim();
216 if !t.is_empty() {
217 return Ok(t.to_owned());
218 }
219 }
220
221 if let Some(v) = config.get(role.config_email_key()) {
222 let t = v.trim();
223 if !t.is_empty() {
224 return Ok(t.to_owned());
225 }
226 }
227
228 if let Some(v) = config.get("user.email") {
229 let t = v.trim();
230 if !t.is_empty() {
231 return Ok(t.to_owned());
232 }
233 }
234
235 if honor_use_config_only && use_config_only(config) && !config_mail_given(config) {
236 return Err(IdentityError::AutoDetectionDisabled { role });
237 }
238
239 if let Some(v) = env.var("EMAIL") {
240 let t = v.trim();
241 if !t.is_empty() {
242 return Ok(t.to_owned());
243 }
244 }
245
246 Ok(synthetic_email_with(env))
247}
248
249pub fn resolve_email_with<E: IdentityEnv>(
251 env: &E,
252 config: &ConfigSet,
253 role: IdentRole,
254) -> Result<String, IdentityError> {
255 resolve_email_inner_with(env, config, role, true)
256}
257
258#[must_use]
260pub fn resolve_email_lenient_with<E: IdentityEnv>(
261 env: &E,
262 config: &ConfigSet,
263 role: IdentRole,
264) -> String {
265 resolve_email_inner_with(env, config, role, false).unwrap_or_else(|_| synthetic_email_with(env))
266}
267
268#[must_use]
270pub fn peek_name_with<E: IdentityEnv>(
271 env: &E,
272 config: &ConfigSet,
273 role: IdentRole,
274) -> Option<String> {
275 match read_git_identity_name_env_with(env, role.env_name_key()) {
276 GitIdentityNameEnv::Set(s) => {
277 if s.is_empty() {
278 None
279 } else {
280 Some(s)
281 }
282 }
283 GitIdentityNameEnv::Unset => {
284 if let Some(v) = config.get(role.config_name_key()) {
285 let t = v.trim();
286 if !t.is_empty() {
287 return Some(t.to_owned());
288 }
289 }
290 let d = ident_default_name(config);
291 if d.is_empty() {
292 None
293 } else {
294 Some(d)
295 }
296 }
297 }
298}
299
300pub fn resolve_name_with<E: IdentityEnv>(
302 env: &E,
303 config: &ConfigSet,
304 role: IdentRole,
305) -> Result<String, IdentityError> {
306 let email = resolve_email_inner_with(env, config, role, true)?;
307
308 let name: String = match read_git_identity_name_env_with(env, role.env_name_key()) {
309 GitIdentityNameEnv::Set(s) => s,
310 GitIdentityNameEnv::Unset => {
311 if let Some(v) = config.get(role.config_name_key()) {
312 let t = v.trim();
313 if !t.is_empty() {
314 t.to_owned()
315 } else {
316 ident_default_name(config)
317 }
318 } else {
319 ident_default_name(config)
320 }
321 }
322 };
323
324 if name.is_empty() {
325 return Err(IdentityError::EmptyName { email, role });
326 }
327
328 if !ident_name_has_non_crud(&name) {
329 return Err(IdentityError::InvalidName { name });
330 }
331
332 Ok(name)
333}
334
335#[must_use]
337pub fn resolve_loose_committer_parts_with<E: IdentityEnv>(
338 env: &E,
339 config: &ConfigSet,
340) -> (String, String) {
341 let name = match read_git_identity_name_env_with(env, "GIT_COMMITTER_NAME") {
342 GitIdentityNameEnv::Set(s) => {
343 if s.is_empty() {
344 None
345 } else {
346 Some(s)
347 }
348 }
349 GitIdentityNameEnv::Unset => read_git_identity_name_from_env_with(env, "GIT_AUTHOR_NAME"),
350 }
351 .or_else(|| {
352 config
353 .get("committer.name")
354 .map(|s| s.trim().to_owned())
355 .filter(|s| !s.is_empty())
356 })
357 .or_else(|| {
358 config
359 .get("user.name")
360 .map(|s| s.trim().to_owned())
361 .filter(|s| !s.is_empty())
362 })
363 .or_else(|| {
364 let d = ident_default_name(config);
365 if d.is_empty() {
366 None
367 } else {
368 Some(d)
369 }
370 })
371 .unwrap_or_else(|| "Unknown".to_owned());
372
373 let email = env
374 .var("GIT_COMMITTER_EMAIL")
375 .map(|s| s.trim().to_owned())
376 .filter(|s| !s.is_empty())
377 .or_else(|| {
378 env.var("GIT_AUTHOR_EMAIL")
379 .map(|s| s.trim().to_owned())
380 .filter(|s| !s.is_empty())
381 })
382 .or_else(|| {
383 config
384 .get("committer.email")
385 .map(|s| s.trim().to_owned())
386 .filter(|s| !s.is_empty())
387 })
388 .or_else(|| {
389 config
390 .get("user.email")
391 .map(|s| s.trim().to_owned())
392 .filter(|s| !s.is_empty())
393 })
394 .or_else(|| {
395 env.var("EMAIL")
396 .map(|s| s.trim().to_owned())
397 .filter(|s| !s.is_empty())
398 })
399 .unwrap_or_else(|| synthetic_email_with(env));
400
401 (name, email)
402}