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