rusty_commit/config/
format.rs1use anyhow::{Context, Result};
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use super::Config;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub enum ConfigFormat {
10 Toml,
11 Json,
12}
13
14impl ConfigFormat {
15 pub fn from_path(path: &Path) -> Self {
17 match path.extension().and_then(|s| s.to_str()) {
18 Some("toml") => ConfigFormat::Toml,
19 Some("json") => ConfigFormat::Json,
20 _ => ConfigFormat::Toml, }
22 }
23
24 pub fn parse(&self, contents: &str) -> Result<Config> {
26 match self {
27 ConfigFormat::Toml => toml::from_str(contents).context("Failed to parse TOML config"),
28 ConfigFormat::Json => {
29 serde_json::from_str(contents).context("Failed to parse JSON config")
30 }
31 }
32 }
33
34 pub fn serialize(&self, config: &Config) -> Result<String> {
36 match self {
37 ConfigFormat::Toml => {
38 toml::to_string_pretty(config).context("Failed to serialize to TOML")
39 }
40 ConfigFormat::Json => {
41 serde_json::to_string_pretty(config).context("Failed to serialize to JSON")
42 }
43 }
44 }
45}
46
47#[derive(Debug)]
49pub struct ConfigLocations {
50 pub repo: Option<PathBuf>,
52 pub global: PathBuf,
54}
55
56impl ConfigLocations {
57 pub fn get() -> Result<Self> {
59 let global = if let Ok(config_home) = std::env::var("RCO_CONFIG_HOME") {
61 PathBuf::from(config_home).join("config.toml")
62 } else {
63 let home = dirs::home_dir().context("Could not find home directory")?;
64 home.join(".config").join("rustycommit").join("config.toml")
65 };
66
67 let ignore_repo_config = std::env::var("RCO_IGNORE_REPO_CONFIG")
69 .ok()
70 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
71 .unwrap_or(false);
72
73 let repo = if ignore_repo_config {
75 None
76 } else if let Ok(repo) = git2::Repository::open_from_env() {
77 let workdir = repo
78 .workdir()
79 .context("Could not get repository working directory")?;
80
81 let possible_configs = [
83 workdir.join(".rustycommit.toml"),
84 workdir.join(".rustycommit.json"),
85 workdir.join(".rco.toml"),
86 workdir.join(".rco.json"),
87 ];
88
89 possible_configs.into_iter().find(|p| p.exists())
90 } else {
91 None
92 };
93
94 Ok(ConfigLocations { repo, global })
95 }
96
97 pub fn load_merged() -> Result<Config> {
99 let locations = Self::get()?;
100
101 let mut config = Config::default();
103
104 if locations.global.exists() {
106 if let Ok(contents) = fs::read_to_string(&locations.global) {
107 let format = ConfigFormat::from_path(&locations.global);
108 match format.parse(&contents) {
109 Ok(global_config) => config.merge(global_config),
110 Err(e) => tracing::warn!(
111 "Failed to parse global config at {}: {}",
112 locations.global.display(),
113 e
114 ),
115 }
116 }
117 }
118
119 if let Some(repo_path) = &locations.repo {
121 if let Ok(contents) = fs::read_to_string(repo_path) {
122 let format = ConfigFormat::from_path(repo_path);
123 match format.parse(&contents) {
124 Ok(repo_config) => config.merge(repo_config),
125 Err(e) => tracing::warn!(
126 "Failed to parse repo config at {}: {}",
127 repo_path.display(),
128 e
129 ),
130 }
131 }
132 }
133
134 config.load_from_environment();
136
137 if config.api_key.is_none() {
139 if let Ok(Some(key)) = crate::config::secure_storage::get_secret("RCO_API_KEY") {
140 config.api_key = Some(key);
141 }
142 }
143
144 if config.api_key.is_none() {
146 if let Some(_token) = crate::auth::token_storage::get_access_token()
147 .ok()
148 .flatten()
149 {
150 }
153 }
154
155 Ok(config)
156 }
157
158 pub fn save(config: &Config, location: ConfigLocation) -> Result<()> {
160 let locations = Self::get()?;
161
162 let (path, format) = match location {
163 ConfigLocation::Global => {
164 if let Some(parent) = locations.global.parent() {
166 fs::create_dir_all(parent)?;
167 }
168 (locations.global, ConfigFormat::Toml)
169 }
170 ConfigLocation::Repo => {
171 let path = locations.repo.unwrap_or_else(|| {
173 if let Ok(repo) = git2::Repository::open_from_env() {
174 if let Some(workdir) = repo.workdir() {
175 return workdir.join(".rustycommit.toml");
176 }
177 }
178 PathBuf::from(".rustycommit.toml")
179 });
180 let format = ConfigFormat::from_path(&path);
181 (path, format)
182 }
183 };
184
185 let contents = format.serialize(config)?;
186 fs::write(&path, contents).context("Failed to write config file")?;
187
188 Ok(())
189 }
190}
191
192#[derive(Debug, Clone, Copy)]
194pub enum ConfigLocation {
195 Global,
196 #[allow(dead_code)]
197 Repo,
198}