use std::path::PathBuf;
use std::{env::current_dir, path::Path};
use clap::ValueEnum;
use config::Config;
use miette::{Context, IntoDiagnostic};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::paths::{berg_config_dir, config_path};
use crate::render::table::TableWrapper;
use crate::types::git::Git;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum ConfigLocation {
Global,
Local,
Stdout,
}
impl ConfigLocation {
pub fn path(self) -> miette::Result<PathBuf> {
match self {
ConfigLocation::Global => {
let global_path = config_path()?;
let global_path_dir = global_path
.parent()
.context("config path has parent directory")?;
Ok(global_path_dir.to_path_buf())
}
ConfigLocation::Local => current_dir()
.into_diagnostic()
.context("Getting current directory failed!"),
ConfigLocation::Stdout => unreachable!(
"Path of Stdout doesn't make sense. Impossible codepath. Please report this as an issue!"
),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BergConfig {
pub base_url: String,
pub enable_https: bool,
pub fancy_tables: bool,
pub no_color: bool,
pub editor: String,
pub max_width: i32,
}
impl BergConfig {
fn iter_default_field_value() -> impl Iterator<Item = (&'static str, config::Value)> {
let BergConfig {
base_url,
enable_https,
fancy_tables,
no_color,
editor,
max_width,
} = Default::default();
[
("base_url", config::Value::from(base_url)),
("enable_https", config::Value::from(enable_https)),
("fancy_tables", config::Value::from(fancy_tables)),
("no_color", config::Value::from(no_color)),
("editor", config::Value::from(editor)),
("max_width", config::Value::from(max_width)),
]
.into_iter()
}
}
impl Default for BergConfig {
fn default() -> Self {
let base_url = Git::default()
.origin()
.and_then(|origin| origin.url().map(strip_base_url));
Self {
base_url: base_url.unwrap_or(String::from("codeberg.org/")),
enable_https: true,
fancy_tables: true,
no_color: false,
editor: default_editor(),
max_width: 80,
}
}
}
impl BergConfig {
pub fn new() -> miette::Result<Self> {
let config = Self::raw()?
.try_deserialize::<BergConfig>()
.into_diagnostic()?;
if !config.enable_https {
eprintln!(
"You are using 'http' as protocol. Please note that this can influence the functionality of some API calls! You should only use it for testing purposes or internal network traffic!"
);
}
Ok(config)
}
pub fn raw() -> miette::Result<Config> {
let local_config_path = current_dir().map(add_berg_config_file).into_diagnostic()?;
let global_config_path = berg_config_dir().map(add_berg_config_file)?;
let mut config_builder = Config::builder();
config_builder = config_builder.add_source(file_from_path(global_config_path.as_path()));
tracing::debug!("config search in: {global_config_path:?}");
let mut walk_up = local_config_path.clone();
let walking_up = std::iter::from_fn(move || {
walk_up
.parent()
.and_then(|parent| parent.parent())
.map(add_berg_config_file)
.inspect(|parent| {
walk_up = parent.clone();
})
});
let pwd = std::iter::once(local_config_path);
let local_paths = pwd.chain(walking_up).collect::<Vec<_>>();
for path in local_paths.iter().rev() {
tracing::debug!("config search in: {path:?}");
config_builder = config_builder.add_source(file_from_path(path));
}
config_builder = config_builder.add_source(config::Environment::with_prefix("BERG"));
for (field_name, default_value) in BergConfig::iter_default_field_value() {
config_builder = config_builder
.set_default(field_name, default_value)
.into_diagnostic()?;
}
config_builder.build().into_diagnostic()
}
}
impl BergConfig {
pub fn url(&self) -> miette::Result<Url> {
let url = format!(
"{protoc}://{url}",
protoc = if self.enable_https { "https" } else { "http" },
url = self.base_url
);
Url::parse(url.as_str())
.into_diagnostic()
.context("The protocol + base url in the config don't add up to a valid url")
}
pub fn make_table(&self) -> TableWrapper {
let mut table = TableWrapper::default();
table.max_width = match self.max_width.cmp(&0) {
std::cmp::Ordering::Less => None,
std::cmp::Ordering::Equal => termsize::get().map(|size| size.cols - 2),
std::cmp::Ordering::Greater => Some(self.max_width as u16),
};
let preset = if self.fancy_tables {
comfy_table::presets::UTF8_FULL
} else {
comfy_table::presets::NOTHING
};
table.load_preset(preset)
}
}
fn file_from_path(
path: impl AsRef<Path>,
) -> config::File<config::FileSourceFile, config::FileFormat> {
config::File::new(
path.as_ref().to_str().unwrap_or_default(),
config::FileFormat::Toml,
)
.required(false)
}
fn add_berg_config_file(dir: impl AsRef<Path>) -> PathBuf {
dir.as_ref().join("berg.toml")
}
fn default_editor() -> String {
std::env::var("EDITOR")
.or(std::env::var("VISUAL"))
.unwrap_or_else(|_| {
let os_native_editor = if cfg!(target_os = "windows") {
"notepad"
} else if cfg!(target_os = "macos") {
"textedit"
} else {
"vi"
};
String::from(os_native_editor)
})
}
#[cfg(test)]
mod tests {
use super::*;
fn make_config(path: PathBuf, config: BergConfig) -> miette::Result<()> {
let config_path = add_berg_config_file(path);
let toml = toml::to_string(&config)
.into_diagnostic()
.context("Failed to create config string!")?;
std::fs::write(config_path, toml)
.into_diagnostic()
.context("Failed to write config file!")?;
Ok(())
}
fn delete_config(path: PathBuf) -> miette::Result<()> {
let config_path = add_berg_config_file(path);
std::fs::remove_file(config_path)
.into_diagnostic()
.context("Failed to remove file")?;
Ok(())
}
#[test]
#[ignore = "doesn't work on nix in 'ci' because of no r/w permissions on the system"]
fn berg_config_integration_test() -> miette::Result<()> {
let local_dir = current_dir().into_diagnostic()?;
std::fs::create_dir_all(local_dir).into_diagnostic()?;
let global_dir = berg_config_dir()?;
std::fs::create_dir_all(global_dir).into_diagnostic()?;
let config = BergConfig {
base_url: String::from("local"),
..Default::default()
};
make_config(current_dir().into_diagnostic()?, config)?;
let config = BergConfig::new();
assert!(config.is_ok(), "{config:?}");
let config = config.unwrap();
assert_eq!(config.base_url.as_str(), "local");
delete_config(current_dir().into_diagnostic()?)?;
let config = BergConfig {
base_url: String::from("global"),
..Default::default()
};
make_config(berg_config_dir()?, config)?;
let config = BergConfig::new();
assert!(config.is_ok(), "{config:?}");
let config = config.unwrap();
assert_eq!(config.base_url.as_str(), "global", "{0:?}", config.base_url);
delete_config(berg_config_dir()?)?;
let config = BergConfig::new();
assert!(config.is_ok(), "{config:?}");
let config = config.unwrap();
assert_eq!(config.base_url.as_str(), "codeberg.org/");
{
let config = BergConfig {
base_url: String::from("local"),
..Default::default()
};
make_config(current_dir().into_diagnostic()?, config)?;
}
{
let config = BergConfig {
base_url: String::from("global"),
..Default::default()
};
make_config(berg_config_dir()?, config)?;
}
let config = BergConfig::new();
assert!(config.is_ok(), "{config:?}");
let config = config.unwrap();
assert_eq!(config.base_url.as_str(), "local");
delete_config(current_dir().into_diagnostic()?)?;
delete_config(berg_config_dir()?)?;
Ok(())
}
}
fn strip_base_url(url: impl AsRef<str>) -> String {
let url = url.as_ref();
let url = url.split_once("//").map(|(_proto, url)| url).unwrap_or(url);
let url = url.split_once("@").map(|(_user, url)| url).unwrap_or(url);
let url = url.split_once("/").map(|(base, _path)| base).unwrap_or(url);
let url = url.split_once(":").map(|(base, _user)| base).unwrap_or(url);
format!("{url}/")
}
#[cfg(test)]
mod strip_base_url {
use super::strip_base_url;
#[test]
fn https_works() {
assert_eq!(
strip_base_url("https://codeberg.org/example.com"),
"codeberg.org/"
);
}
#[test]
fn ssh_works() {
assert_eq!(
strip_base_url("git@codeberg.org:Aviac/codeberg-cli.git"),
"codeberg.org/"
);
}
#[test]
fn ssh_detailed_works() {
assert_eq!(
strip_base_url("ssh://git@example.com:1312/foo/bar.git"),
"example.com/"
);
}
#[test]
fn domain_and_port_no_protoc() {
assert_eq!(strip_base_url("example.com:1312"), "example.com/");
}
}