use std::path::PathBuf;
use super::Config;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum ConfigScope {
System,
Global,
Local,
Worktree,
Environment,
Command,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ConfigFile {
pub path: PathBuf,
pub scope: ConfigScope,
pub optional: bool,
pub skip_unreadable: bool,
}
impl ConfigFile {
pub fn new(path: PathBuf, scope: ConfigScope) -> Self {
Self {
path,
scope,
optional: false,
skip_unreadable: false,
}
}
#[must_use]
pub fn with_path(mut self, path: PathBuf) -> Self {
self.path = path;
self
}
#[must_use]
pub fn with_scope(mut self, scope: ConfigScope) -> Self {
self.scope = scope;
self
}
#[must_use]
pub fn with_optional(mut self, optional: bool) -> Self {
self.optional = optional;
self
}
#[must_use]
pub fn with_skip_unreadable(mut self, skip_unreadable: bool) -> Self {
self.skip_unreadable = skip_unreadable;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceLocation {
pub path: Option<PathBuf>,
pub line: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Origin {
pub scope: ConfigScope,
pub location: SourceLocation,
pub included_from: Vec<SourceLocation>,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct IncludeContext {
pub home: Option<PathBuf>,
pub user_homes: Vec<(Vec<u8>, PathBuf)>,
pub prefix: Option<PathBuf>,
pub git_dirs: Vec<PathBuf>,
pub branch: Option<Vec<u8>>,
}
impl IncludeContext {
#[must_use]
pub fn with_home(mut self, home: Option<PathBuf>) -> Self {
self.home = home;
self
}
#[must_use]
pub fn with_user_homes(mut self, user_homes: Vec<(Vec<u8>, PathBuf)>) -> Self {
self.user_homes = user_homes;
self
}
#[must_use]
pub fn with_prefix(mut self, prefix: Option<PathBuf>) -> Self {
self.prefix = prefix;
self
}
#[must_use]
pub fn with_git_dirs(mut self, git_dirs: Vec<PathBuf>) -> Self {
self.git_dirs = git_dirs;
self
}
#[must_use]
pub fn with_branch(mut self, branch: Option<Vec<u8>>) -> Self {
self.branch = branch;
self
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct ResolveLimits {
pub depth: usize,
pub bytes: usize,
pub entries: usize,
pub match_cells: usize,
}
impl ResolveLimits {
#[must_use]
pub fn with_depth(mut self, depth: usize) -> Self {
self.depth = depth;
self
}
#[must_use]
pub fn with_bytes(mut self, bytes: usize) -> Self {
self.bytes = bytes;
self
}
#[must_use]
pub fn with_entries(mut self, entries: usize) -> Self {
self.entries = entries;
self
}
#[must_use]
pub fn with_match_cells(mut self, match_cells: usize) -> Self {
self.match_cells = match_cells;
self
}
}
impl Default for ResolveLimits {
fn default() -> Self {
Self {
depth: 10,
bytes: 16 * 1024 * 1024,
entries: 100_000,
match_cells: 1_000_000,
}
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ConfigInputs {
pub files: Vec<ConfigFile>,
pub context: IncludeContext,
pub environment: Option<Config>,
pub command: Option<Config>,
pub limits: ResolveLimits,
}
impl ConfigInputs {
#[must_use]
pub fn with_files(mut self, files: Vec<ConfigFile>) -> Self {
self.files = files;
self
}
#[must_use]
pub fn with_context(mut self, context: IncludeContext) -> Self {
self.context = context;
self
}
#[must_use]
pub fn with_environment(mut self, environment: Option<Config>) -> Self {
self.environment = environment;
self
}
#[must_use]
pub fn with_command(mut self, command: Option<Config>) -> Self {
self.command = command;
self
}
#[must_use]
pub fn with_limits(mut self, limits: ResolveLimits) -> Self {
self.limits = limits;
self
}
}
impl ConfigInputs {
pub fn from_environment(
system: Option<PathBuf>,
mut get: impl FnMut(&str) -> Option<std::ffi::OsString>,
max_pairs: usize,
) -> Result<Self, super::ConfigError> {
let mut inputs = Self::default();
let home = get("HOME").map(PathBuf::from);
inputs.context.home = home.clone();
let no_system = get("GIT_CONFIG_NOSYSTEM");
let no_system = match no_system {
None => false,
Some(value) => {
#[cfg(unix)]
let bytes = {
use std::os::unix::ffi::OsStrExt;
value.as_bytes()
};
#[cfg(not(unix))]
let bytes = value
.to_str()
.ok_or(super::ConfigError {
line: 1,
reason: "invalid GIT_CONFIG_NOSYSTEM",
})?
.as_bytes();
super::boolean(Some(bytes)).ok_or(super::ConfigError {
line: 1,
reason: "invalid GIT_CONFIG_NOSYSTEM",
})?
}
};
let system = get("GIT_CONFIG_SYSTEM").map(PathBuf::from).or(system);
if !no_system && let Some(path) = system {
inputs.files.push(ConfigFile {
path,
scope: ConfigScope::System,
optional: true,
skip_unreadable: false,
});
}
if let Some(path) = get("GIT_CONFIG_GLOBAL") {
inputs.files.push(ConfigFile {
path: path.into(),
scope: ConfigScope::Global,
optional: true,
skip_unreadable: false,
});
} else {
let xdg = get("XDG_CONFIG_HOME")
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.or_else(|| home.as_ref().map(|home| home.join(".config")));
if let Some(xdg) = xdg {
inputs.files.push(ConfigFile {
path: xdg.join("git/config"),
scope: ConfigScope::Global,
optional: true,
skip_unreadable: false,
});
}
if let Some(home) = home {
inputs.files.push(ConfigFile {
path: home.join(".gitconfig"),
scope: ConfigScope::Global,
optional: true,
skip_unreadable: false,
});
}
}
#[cfg(not(unix))]
let mut invalid_encoding = false;
inputs.environment = Some(Config::from_environment(
|key| {
get(key).map(|value| {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStringExt;
value.into_vec()
}
#[cfg(not(unix))]
{
match value.into_string() {
Ok(value) => value.into_bytes(),
Err(_) => {
invalid_encoding = true;
Vec::new()
}
}
}
})
},
max_pairs,
)?);
#[cfg(not(unix))]
if invalid_encoding {
return Err(super::ConfigError {
line: 1,
reason: "environment pair is not Unicode",
});
}
Ok(inputs)
}
}
impl ConfigInputs {
pub fn git_default(
environment: &crate::workflow::Environment,
) -> Result<Self, super::ConfigError> {
let limits = ResolveLimits::default();
let get = |name: &str| environment.get(name).map(std::ffi::OsStr::to_owned);
let mut inputs = Self::from_environment(default_system_file(), get, limits.entries)?;
inputs
.files
.retain(|file| !file.path.as_os_str().is_empty());
for file in &mut inputs.files {
file.skip_unreadable = file.scope == ConfigScope::Global;
if let Some(directory) = environment.current_dir()
&& file.path.is_relative()
{
file.path = directory.join(&file.path);
}
}
#[cfg(not(unix))]
let mut invalid_encoding = false;
let environment_config = Config::from_command_environment(
|name| {
let value = environment.get(name)?;
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
Some(value.as_bytes().to_vec())
}
#[cfg(not(unix))]
{
let value = value.to_str();
invalid_encoding |= value.is_none();
Some(value.unwrap_or_default().as_bytes().to_vec())
}
},
limits.entries,
limits.bytes,
)?;
#[cfg(not(unix))]
if invalid_encoding {
return Err(super::ConfigError {
line: 1,
reason: "environment pair is not Unicode",
});
}
inputs.environment = Some(environment_config);
Ok(inputs)
}
}
fn default_system_file() -> Option<PathBuf> {
if cfg!(target_os = "macos") {
[
"/opt/homebrew/etc/gitconfig",
"/usr/local/etc/gitconfig",
"/etc/gitconfig",
]
.into_iter()
.map(PathBuf::from)
.find(|path| path.exists())
} else if cfg!(unix) {
Some(PathBuf::from("/etc/gitconfig"))
} else {
None
}
}