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(
102 "no email was given and auto-detection is disabled\n\n\
103*** Please tell me who you are.\n\n\
104Run\n\n\
105 git config --global user.email \"you@example.com\"\n\
106 git config --global user.name \"Your Name\"\n\n\
107to set your account's default identity.\n\
108Omit --global to set the identity only in this repository.\n"
109 )]
110 AutoDetectionDisabled {
111 role: IdentRole,
113 },
114 #[error("empty ident name (for <{email}>) not allowed")]
116 EmptyName {
117 email: String,
119 role: IdentRole,
121 },
122 #[error("invalid ident name: '{name}'")]
124 InvalidName {
125 name: String,
127 },
128}
129
130#[must_use]
132pub fn read_git_identity_name_env_with<E: IdentityEnv>(env: &E, key: &str) -> GitIdentityNameEnv {
133 let Some(os) = env.var_os(key) else {
134 return GitIdentityNameEnv::Unset;
135 };
136 #[cfg(unix)]
137 {
138 use std::os::unix::ffi::OsStrExt;
139 let bytes = os.as_bytes();
140 let s = if std::str::from_utf8(bytes).is_ok() {
141 String::from_utf8_lossy(bytes).into_owned()
142 } else {
143 decode_bytes(Some("ISO8859-1"), bytes)
144 };
145 GitIdentityNameEnv::Set(s.trim().to_owned())
146 }
147 #[cfg(not(unix))]
148 {
149 let s = os.to_str().map(|t| t.trim().to_owned()).unwrap_or_default();
150 GitIdentityNameEnv::Set(s)
151 }
152}
153
154#[must_use]
160pub fn read_git_identity_name_from_env_with<E: IdentityEnv>(env: &E, key: &str) -> Option<String> {
161 match read_git_identity_name_env_with(env, key) {
162 GitIdentityNameEnv::Unset => None,
163 GitIdentityNameEnv::Set(s) if s.is_empty() => None,
164 GitIdentityNameEnv::Set(s) => Some(s),
165 }
166}
167
168fn use_config_only(config: &ConfigSet) -> bool {
169 match config.get_bool("user.useConfigOnly") {
170 Some(Ok(b)) => b,
171 Some(Err(_)) => false,
172 None => false,
173 }
174}
175
176fn config_mail_given(config: &ConfigSet) -> bool {
177 ["user.email", "author.email", "committer.email"]
178 .iter()
179 .any(|key| config.get(key).is_some_and(|v| !v.trim().is_empty()))
180}
181
182fn ident_name_has_non_crud(name: &str) -> bool {
183 name.chars().any(|c| {
184 let o = c as u32;
185 !(o <= 32
186 || c == ','
187 || c == ':'
188 || c == ';'
189 || c == '<'
190 || c == '>'
191 || c == '"'
192 || c == '\\'
193 || c == '\'')
194 })
195}
196
197fn synthetic_email_with<E: IdentityEnv>(env: &E) -> String {
198 let user = env
199 .var("USER")
200 .or_else(|| env.var("USERNAME"))
201 .unwrap_or_else(|| "unknown".to_owned());
202 let host = env.var("HOSTNAME").unwrap_or_else(|| "unknown".to_owned());
203 let domain = if host.contains('.') {
204 host
205 } else {
206 format!("{host}.(none)")
207 };
208 format!("{user}@{domain}")
209}
210
211fn resolve_email_inner_with<E: IdentityEnv>(
212 env: &E,
213 config: &ConfigSet,
214 role: IdentRole,
215 honor_use_config_only: bool,
216) -> Result<String, IdentityError> {
217 if let Some(v) = env.var(role.env_email_key()) {
218 let t = v.trim();
219 if !t.is_empty() {
220 return Ok(t.to_owned());
221 }
222 }
223
224 if let Some(v) = config.get(role.config_email_key()) {
225 let t = v.trim();
226 if !t.is_empty() {
227 return Ok(t.to_owned());
228 }
229 }
230
231 if let Some(v) = config.get("user.email") {
232 let t = v.trim();
233 if !t.is_empty() {
234 return Ok(t.to_owned());
235 }
236 }
237
238 if honor_use_config_only && use_config_only(config) && !config_mail_given(config) {
239 return Err(IdentityError::AutoDetectionDisabled { role });
240 }
241
242 if let Some(v) = env.var("EMAIL") {
243 let t = v.trim();
244 if !t.is_empty() {
245 return Ok(t.to_owned());
246 }
247 }
248
249 Ok(synthetic_email_with(env))
250}
251
252pub fn resolve_email_with<E: IdentityEnv>(
254 env: &E,
255 config: &ConfigSet,
256 role: IdentRole,
257) -> Result<String, IdentityError> {
258 resolve_email_inner_with(env, config, role, true)
259}
260
261#[must_use]
263pub fn resolve_email_lenient_with<E: IdentityEnv>(
264 env: &E,
265 config: &ConfigSet,
266 role: IdentRole,
267) -> String {
268 resolve_email_inner_with(env, config, role, false).unwrap_or_else(|_| synthetic_email_with(env))
269}
270
271#[must_use]
273pub fn peek_name_with<E: IdentityEnv>(
274 env: &E,
275 config: &ConfigSet,
276 role: IdentRole,
277) -> Option<String> {
278 match read_git_identity_name_env_with(env, role.env_name_key()) {
279 GitIdentityNameEnv::Set(s) => {
280 if s.is_empty() {
281 None
282 } else {
283 Some(s)
284 }
285 }
286 GitIdentityNameEnv::Unset => {
287 if let Some(v) = config.get(role.config_name_key()) {
288 let t = v.trim();
289 if !t.is_empty() {
290 return Some(t.to_owned());
291 }
292 }
293 let d = ident_default_name(config);
294 if d.is_empty() {
295 None
296 } else {
297 Some(d)
298 }
299 }
300 }
301}
302
303pub fn resolve_name_with<E: IdentityEnv>(
305 env: &E,
306 config: &ConfigSet,
307 role: IdentRole,
308) -> Result<String, IdentityError> {
309 let email = resolve_email_inner_with(env, config, role, true)?;
310
311 let name: String = match read_git_identity_name_env_with(env, role.env_name_key()) {
312 GitIdentityNameEnv::Set(s) => s,
313 GitIdentityNameEnv::Unset => {
314 if let Some(v) = config.get(role.config_name_key()) {
315 let t = v.trim();
316 if !t.is_empty() {
317 t.to_owned()
318 } else {
319 ident_default_name(config)
320 }
321 } else {
322 ident_default_name(config)
323 }
324 }
325 };
326
327 if name.is_empty() {
328 return Err(IdentityError::EmptyName { email, role });
329 }
330
331 if !ident_name_has_non_crud(&name) {
332 return Err(IdentityError::InvalidName { name });
333 }
334
335 Ok(name)
336}
337
338#[must_use]
340pub fn resolve_loose_committer_parts_with<E: IdentityEnv>(
341 env: &E,
342 config: &ConfigSet,
343) -> (String, String) {
344 let name = match read_git_identity_name_env_with(env, "GIT_COMMITTER_NAME") {
345 GitIdentityNameEnv::Set(s) => {
346 if s.is_empty() {
347 None
348 } else {
349 Some(s)
350 }
351 }
352 GitIdentityNameEnv::Unset => read_git_identity_name_from_env_with(env, "GIT_AUTHOR_NAME"),
353 }
354 .or_else(|| {
355 config
356 .get("committer.name")
357 .map(|s| s.trim().to_owned())
358 .filter(|s| !s.is_empty())
359 })
360 .or_else(|| {
361 config
362 .get("user.name")
363 .map(|s| s.trim().to_owned())
364 .filter(|s| !s.is_empty())
365 })
366 .or_else(|| {
367 let d = ident_default_name(config);
368 if d.is_empty() {
369 None
370 } else {
371 Some(d)
372 }
373 })
374 .unwrap_or_else(|| "Unknown".to_owned());
375
376 let email = env
377 .var("GIT_COMMITTER_EMAIL")
378 .map(|s| s.trim().to_owned())
379 .filter(|s| !s.is_empty())
380 .or_else(|| {
381 env.var("GIT_AUTHOR_EMAIL")
382 .map(|s| s.trim().to_owned())
383 .filter(|s| !s.is_empty())
384 })
385 .or_else(|| {
386 config
387 .get("committer.email")
388 .map(|s| s.trim().to_owned())
389 .filter(|s| !s.is_empty())
390 })
391 .or_else(|| {
392 config
393 .get("user.email")
394 .map(|s| s.trim().to_owned())
395 .filter(|s| !s.is_empty())
396 })
397 .or_else(|| {
398 env.var("EMAIL")
399 .map(|s| s.trim().to_owned())
400 .filter(|s| !s.is_empty())
401 })
402 .unwrap_or_else(|| synthetic_email_with(env));
403
404 (name, email)
405}