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