use std::ffi::OsString;
use thiserror::Error;
use crate::commit_encoding::decode_bytes;
use crate::config::ConfigSet;
use crate::ident_config::ident_default_name;
pub trait IdentityEnv {
fn var(&self, key: &str) -> Option<String>;
fn var_os(&self, key: &str) -> Option<OsString>;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct SystemIdentityEnv;
impl IdentityEnv for SystemIdentityEnv {
fn var(&self, key: &str) -> Option<String> {
std::env::var(key).ok()
}
fn var_os(&self, key: &str) -> Option<OsString> {
std::env::var_os(key)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum GitIdentityNameEnv {
Unset,
Set(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IdentRole {
Author,
Committer,
}
impl IdentRole {
fn env_name_key(self) -> &'static str {
match self {
IdentRole::Author => "GIT_AUTHOR_NAME",
IdentRole::Committer => "GIT_COMMITTER_NAME",
}
}
fn env_email_key(self) -> &'static str {
match self {
IdentRole::Author => "GIT_AUTHOR_EMAIL",
IdentRole::Committer => "GIT_COMMITTER_EMAIL",
}
}
fn config_name_key(self) -> &'static str {
match self {
IdentRole::Author => "author.name",
IdentRole::Committer => "committer.name",
}
}
fn config_email_key(self) -> &'static str {
match self {
IdentRole::Author => "author.email",
IdentRole::Committer => "committer.email",
}
}
#[must_use]
pub fn missing_email_hint(self) -> &'static str {
match self {
IdentRole::Author => "Author identity unknown",
IdentRole::Committer => "Committer identity unknown",
}
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum IdentityError {
#[error(
"no email was given and auto-detection is disabled\n\n\
*** Please tell me who you are.\n\n\
Run\n\n\
git config --global user.email \"you@example.com\"\n\
git config --global user.name \"Your Name\"\n\n\
to set your account's default identity.\n\
Omit --global to set the identity only in this repository.\n"
)]
AutoDetectionDisabled {
role: IdentRole,
},
#[error("empty ident name (for <{email}>) not allowed")]
EmptyName {
email: String,
role: IdentRole,
},
#[error("invalid ident name: '{name}'")]
InvalidName {
name: String,
},
}
#[must_use]
pub fn read_git_identity_name_env_with<E: IdentityEnv>(env: &E, key: &str) -> GitIdentityNameEnv {
let Some(os) = env.var_os(key) else {
return GitIdentityNameEnv::Unset;
};
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
let bytes = os.as_bytes();
let s = if std::str::from_utf8(bytes).is_ok() {
String::from_utf8_lossy(bytes).into_owned()
} else {
decode_bytes(Some("ISO8859-1"), bytes)
};
GitIdentityNameEnv::Set(s.trim().to_owned())
}
#[cfg(not(unix))]
{
let s = os.to_str().map(|t| t.trim().to_owned()).unwrap_or_default();
GitIdentityNameEnv::Set(s)
}
}
#[must_use]
pub fn read_git_identity_name_from_env_with<E: IdentityEnv>(env: &E, key: &str) -> Option<String> {
match read_git_identity_name_env_with(env, key) {
GitIdentityNameEnv::Unset => None,
GitIdentityNameEnv::Set(s) if s.is_empty() => None,
GitIdentityNameEnv::Set(s) => Some(s),
}
}
fn use_config_only(config: &ConfigSet) -> bool {
match config.get_bool("user.useConfigOnly") {
Some(Ok(b)) => b,
Some(Err(_)) => false,
None => false,
}
}
fn config_mail_given(config: &ConfigSet) -> bool {
["user.email", "author.email", "committer.email"]
.iter()
.any(|key| config.get(key).is_some_and(|v| !v.trim().is_empty()))
}
fn ident_name_has_non_crud(name: &str) -> bool {
name.chars().any(|c| {
let o = c as u32;
!(o <= 32
|| c == ','
|| c == ':'
|| c == ';'
|| c == '<'
|| c == '>'
|| c == '"'
|| c == '\\'
|| c == '\'')
})
}
fn synthetic_email_with<E: IdentityEnv>(env: &E) -> String {
let user = env
.var("USER")
.or_else(|| env.var("USERNAME"))
.unwrap_or_else(|| "unknown".to_owned());
let host = env.var("HOSTNAME").unwrap_or_else(|| "unknown".to_owned());
let domain = if host.contains('.') {
host
} else {
format!("{host}.(none)")
};
format!("{user}@{domain}")
}
fn resolve_email_inner_with<E: IdentityEnv>(
env: &E,
config: &ConfigSet,
role: IdentRole,
honor_use_config_only: bool,
) -> Result<String, IdentityError> {
if let Some(v) = env.var(role.env_email_key()) {
let t = v.trim();
if !t.is_empty() {
return Ok(t.to_owned());
}
}
if let Some(v) = config.get(role.config_email_key()) {
let t = v.trim();
if !t.is_empty() {
return Ok(t.to_owned());
}
}
if let Some(v) = config.get("user.email") {
let t = v.trim();
if !t.is_empty() {
return Ok(t.to_owned());
}
}
if honor_use_config_only && use_config_only(config) && !config_mail_given(config) {
return Err(IdentityError::AutoDetectionDisabled { role });
}
if let Some(v) = env.var("EMAIL") {
let t = v.trim();
if !t.is_empty() {
return Ok(t.to_owned());
}
}
Ok(synthetic_email_with(env))
}
pub fn resolve_email_with<E: IdentityEnv>(
env: &E,
config: &ConfigSet,
role: IdentRole,
) -> Result<String, IdentityError> {
resolve_email_inner_with(env, config, role, true)
}
#[must_use]
pub fn resolve_email_lenient_with<E: IdentityEnv>(
env: &E,
config: &ConfigSet,
role: IdentRole,
) -> String {
resolve_email_inner_with(env, config, role, false).unwrap_or_else(|_| synthetic_email_with(env))
}
#[must_use]
pub fn peek_name_with<E: IdentityEnv>(
env: &E,
config: &ConfigSet,
role: IdentRole,
) -> Option<String> {
match read_git_identity_name_env_with(env, role.env_name_key()) {
GitIdentityNameEnv::Set(s) => {
if s.is_empty() {
None
} else {
Some(s)
}
}
GitIdentityNameEnv::Unset => {
if let Some(v) = config.get(role.config_name_key()) {
let t = v.trim();
if !t.is_empty() {
return Some(t.to_owned());
}
}
let d = ident_default_name(config);
if d.is_empty() {
None
} else {
Some(d)
}
}
}
}
pub fn resolve_name_with<E: IdentityEnv>(
env: &E,
config: &ConfigSet,
role: IdentRole,
) -> Result<String, IdentityError> {
let email = resolve_email_inner_with(env, config, role, true)?;
let name: String = match read_git_identity_name_env_with(env, role.env_name_key()) {
GitIdentityNameEnv::Set(s) => s,
GitIdentityNameEnv::Unset => {
if let Some(v) = config.get(role.config_name_key()) {
let t = v.trim();
if !t.is_empty() {
t.to_owned()
} else {
ident_default_name(config)
}
} else {
ident_default_name(config)
}
}
};
if name.is_empty() {
return Err(IdentityError::EmptyName { email, role });
}
if !ident_name_has_non_crud(&name) {
return Err(IdentityError::InvalidName { name });
}
Ok(name)
}
#[must_use]
pub fn resolve_loose_committer_parts_with<E: IdentityEnv>(
env: &E,
config: &ConfigSet,
) -> (String, String) {
let name = match read_git_identity_name_env_with(env, "GIT_COMMITTER_NAME") {
GitIdentityNameEnv::Set(s) => {
if s.is_empty() {
None
} else {
Some(s)
}
}
GitIdentityNameEnv::Unset => read_git_identity_name_from_env_with(env, "GIT_AUTHOR_NAME"),
}
.or_else(|| {
config
.get("committer.name")
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
})
.or_else(|| {
config
.get("user.name")
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
})
.or_else(|| {
let d = ident_default_name(config);
if d.is_empty() {
None
} else {
Some(d)
}
})
.unwrap_or_else(|| "Unknown".to_owned());
let email = env
.var("GIT_COMMITTER_EMAIL")
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
.or_else(|| {
env.var("GIT_AUTHOR_EMAIL")
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
})
.or_else(|| {
config
.get("committer.email")
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
})
.or_else(|| {
config
.get("user.email")
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
})
.or_else(|| {
env.var("EMAIL")
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
})
.unwrap_or_else(|| synthetic_email_with(env));
(name, email)
}