1use std::path::PathBuf;
2use std::{env::current_dir, path::Path};
3
4use anyhow::Context;
5use clap::ValueEnum;
6use config::Config;
7use serde::{Deserialize, Serialize};
8use url::Url;
9
10use crate::paths::{berg_config_dir, config_path};
11use crate::render::table::TableWrapper;
12use crate::types::git::Git;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
15pub enum ConfigLocation {
16 Global,
17 Local,
18 Stdout,
19}
20
21impl ConfigLocation {
22 pub fn path(self) -> anyhow::Result<PathBuf> {
23 match self {
24 ConfigLocation::Global => {
25 let global_path = config_path()?;
26 let global_path_dir = global_path
27 .parent()
28 .context("config path has parent directory")?;
29 Ok(global_path_dir.to_path_buf())
30 }
31 ConfigLocation::Local => current_dir().context("Getting current directory failed!"),
32 ConfigLocation::Stdout => unreachable!(
33 "Path of Stdout doesn't make sense. Impossible codepath. Please report this as an issue!"
34 ),
35 }
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct BergConfig {
42 pub base_url: String,
44 pub protocol: String,
46 pub fancy_tables: bool,
49 pub no_color: bool,
54 pub editor: String,
56 pub max_width: i32,
67}
68
69impl BergConfig {
70 fn iter_default_field_value() -> impl Iterator<Item = (&'static str, config::Value)> {
71 let BergConfig {
72 base_url,
73 protocol,
74 fancy_tables,
75 no_color,
76 editor,
77 max_width,
78 } = Default::default();
79 [
80 ("base_url", config::Value::from(base_url)),
81 ("protocol", config::Value::from(protocol)),
82 ("fancy_tables", config::Value::from(fancy_tables)),
83 ("no_color", config::Value::from(no_color)),
84 ("editor", config::Value::from(editor)),
85 ("max_width", config::Value::from(max_width)),
86 ]
87 .into_iter()
88 }
89}
90
91impl Default for BergConfig {
92 fn default() -> Self {
93 let base_url = Git::new()
94 .origin()
95 .and_then(|origin| origin.url().map(strip_base_url));
96 Self {
97 base_url: base_url.unwrap_or(String::from("codeberg.org/")),
98 protocol: String::from("https"),
99 fancy_tables: true,
100 no_color: false,
101 editor: default_editor(),
102 max_width: 80,
103 }
104 }
105}
106
107impl BergConfig {
108 pub fn new() -> anyhow::Result<Self> {
121 let config = Self::raw()?.try_deserialize::<BergConfig>()?;
122 Ok(config)
123 }
124
125 pub fn raw() -> anyhow::Result<Config> {
126 let local_config_path = current_dir().map(add_berg_config_file)?;
127 let global_config_path = berg_config_dir().map(add_berg_config_file)?;
128 let mut config_builder = Config::builder();
129
130 config_builder = config_builder.add_source(file_from_path(global_config_path.as_path()));
139 tracing::debug!("config search in: {global_config_path:?}");
140
141 let mut walk_up = local_config_path.clone();
142 let walking_up = std::iter::from_fn(move || {
143 walk_up
144 .parent()
145 .and_then(|parent| parent.parent())
146 .map(add_berg_config_file)
147 .inspect(|parent| {
148 walk_up = parent.clone();
149 })
150 });
151
152 let pwd = std::iter::once(local_config_path);
153 let local_paths = pwd.chain(walking_up).collect::<Vec<_>>();
154
155 for path in local_paths.iter().rev() {
156 tracing::debug!("config search in: {path:?}");
157 config_builder = config_builder.add_source(file_from_path(path));
158 }
159
160 config_builder = config_builder.add_source(config::Environment::with_prefix("BERG"));
161 for (field_name, default_value) in BergConfig::iter_default_field_value() {
164 config_builder = config_builder.set_default(field_name, default_value)?;
165 }
166
167 config_builder.build().map_err(anyhow::Error::from)
168 }
169}
170
171impl BergConfig {
172 pub fn url(&self) -> anyhow::Result<Url> {
173 let url = format!(
174 "{protoc}://{url}",
175 protoc = self.protocol,
176 url = self.base_url
177 );
178 Url::parse(url.as_str())
179 .context("The protocol + base url in the config don't add up to a valid url")
180 }
181
182 pub fn make_table(&self) -> TableWrapper {
183 let mut table = TableWrapper::default();
184
185 table.max_width = match self.max_width.cmp(&0) {
186 std::cmp::Ordering::Less => None,
187 std::cmp::Ordering::Equal => termsize::get().map(|size| size.cols - 2),
188 std::cmp::Ordering::Greater => Some(self.max_width as u16),
189 };
190
191 let preset = if self.fancy_tables {
192 comfy_table::presets::UTF8_FULL
193 } else {
194 comfy_table::presets::NOTHING
195 };
196
197 table.load_preset(preset);
198
199 table
200 }
201}
202
203fn file_from_path(
204 path: impl AsRef<Path>,
205) -> config::File<config::FileSourceFile, config::FileFormat> {
206 config::File::new(
207 path.as_ref().to_str().unwrap_or_default(),
208 config::FileFormat::Toml,
209 )
210 .required(false)
211}
212
213fn add_berg_config_file(dir: impl AsRef<Path>) -> PathBuf {
214 dir.as_ref().join("berg.toml")
215}
216
217fn default_editor() -> String {
218 std::env::var("EDITOR")
219 .or(std::env::var("VISUAL"))
220 .unwrap_or_else(|_| {
221 let os_native_editor = if cfg!(target_os = "windows") {
222 "notepad"
224 } else if cfg!(target_os = "macos") {
225 "textedit"
227 } else {
228 "vi"
230 };
231 String::from(os_native_editor)
232 })
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 fn make_config(path: PathBuf, config: BergConfig) -> anyhow::Result<()> {
240 let config_path = add_berg_config_file(path);
241 let toml = toml::to_string(&config).context("Failed to create config string!")?;
242 std::fs::write(config_path, toml).context("Failed to write config file!")?;
243 Ok(())
244 }
245
246 fn delete_config(path: PathBuf) -> anyhow::Result<()> {
247 let config_path = add_berg_config_file(path);
248 std::fs::remove_file(config_path).context("Failed to remove file")?;
249 Ok(())
250 }
251
252 #[test]
253 #[ignore = "doesn't work on nix in 'ci' because of no r/w permissions on the system"]
254 fn berg_config_integration_test() -> anyhow::Result<()> {
255 let local_dir = current_dir()?;
258 std::fs::create_dir_all(local_dir)?;
259 let global_dir = berg_config_dir()?;
260 std::fs::create_dir_all(global_dir)?;
261
262 let config = BergConfig {
264 base_url: String::from("local"),
265 ..Default::default()
266 };
267 make_config(current_dir()?, config)?;
268 let config = BergConfig::new();
269 assert!(config.is_ok(), "{config:?}");
270 let config = config.unwrap();
271 assert_eq!(config.base_url.as_str(), "local");
272 delete_config(current_dir()?)?;
273
274 let config = BergConfig {
276 base_url: String::from("global"),
277 ..Default::default()
278 };
279 make_config(berg_config_dir()?, config)?;
280 let config = BergConfig::new();
281 assert!(config.is_ok(), "{config:?}");
282 let config = config.unwrap();
283 assert_eq!(config.base_url.as_str(), "global", "{0:?}", config.base_url);
284 delete_config(berg_config_dir()?)?;
285
286 let config = BergConfig::new();
288 assert!(config.is_ok(), "{config:?}");
289 let config = config.unwrap();
290 assert_eq!(config.base_url.as_str(), "codeberg.org/");
291
292 {
294 let config = BergConfig {
296 base_url: String::from("local"),
297 ..Default::default()
298 };
299 make_config(current_dir()?, config)?;
300 }
301 {
302 let config = BergConfig {
304 base_url: String::from("global"),
305 ..Default::default()
306 };
307 make_config(berg_config_dir()?, config)?;
308 }
309 let config = BergConfig::new();
310 assert!(config.is_ok(), "{config:?}");
311 let config = config.unwrap();
312 assert_eq!(config.base_url.as_str(), "local");
313 delete_config(current_dir()?)?;
314 delete_config(berg_config_dir()?)?;
315
316 Ok(())
317 }
318}
319
320fn strip_base_url(url: impl AsRef<str>) -> String {
327 let url = url.as_ref();
328 let url = url.split_once("//").map(|(_proto, url)| url).unwrap_or(url);
329 let url = url.split_once("@").map(|(_user, url)| url).unwrap_or(url);
330 let url = url.split_once("/").map(|(base, _path)| base).unwrap_or(url);
331 let url = url.split_once(":").map(|(base, _user)| base).unwrap_or(url);
332 format!("{url}/")
333}
334
335#[cfg(test)]
336mod strip_base_url {
337 use super::strip_base_url;
338 #[test]
339 fn https_works() {
340 assert_eq!(
341 strip_base_url("https://codeberg.org/example.com"),
342 "codeberg.org/"
343 );
344 }
345
346 #[test]
347 fn ssh_works() {
348 assert_eq!(
349 strip_base_url("git@codeberg.org:Aviac/codeberg-cli.git"),
350 "codeberg.org/"
351 );
352 }
353
354 #[test]
355 fn ssh_detailed_works() {
356 assert_eq!(
357 strip_base_url("ssh://git@example.com:1312/foo/bar.git"),
358 "example.com/"
359 );
360 }
361
362 #[test]
363 fn domain_and_port_no_protoc() {
364 assert_eq!(strip_base_url("example.com:1312"), "example.com/");
365 }
366}