1use std::collections::HashMap;
2use std::io::Write;
3use std::path::PathBuf;
4
5use anyhow::bail;
6use dialoguer::theme;
7use log::debug;
8use serde_derive::{Deserialize, Serialize};
9
10#[derive(Debug, Serialize, Deserialize)]
11pub struct Global {
12 pub author: Author,
13 pub unrecord_changes: Option<usize>,
14 pub reset_overwrites_changes: Option<Choice>,
15 pub colors: Option<Choice>,
16 pub pager: Option<Choice>,
17 pub template: Option<Templates>,
18 pub ignore_kinds: Option<HashMap<String, Vec<String>>>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct Author {
23 #[serde(alias = "name", default, skip_serializing_if = "String::is_empty")]
25 pub username: String,
26 #[serde(alias = "full_name", default, skip_serializing_if = "String::is_empty")]
27 pub display_name: String,
28 #[serde(default, skip_serializing_if = "String::is_empty")]
29 pub email: String,
30 #[serde(default, skip_serializing_if = "String::is_empty")]
31 pub origin: String,
32 #[serde(default, skip_serializing)]
34 pub key_path: Option<PathBuf>,
35}
36
37impl Default for Author {
38 fn default() -> Self {
39 Self {
40 username: String::new(),
41 email: String::new(),
42 display_name: whoami::realname(),
43 origin: String::new(),
44 key_path: None,
45 }
46 }
47}
48
49#[derive(Debug, Serialize, Deserialize)]
50pub enum Choice {
51 #[serde(rename = "auto")]
52 Auto,
53 #[serde(rename = "always")]
54 Always,
55 #[serde(rename = "never")]
56 Never,
57}
58
59impl Default for Choice {
60 fn default() -> Self {
61 Self::Auto
62 }
63}
64
65#[derive(Debug, Serialize, Deserialize)]
66pub struct Templates {
67 pub message: Option<PathBuf>,
68 pub description: Option<PathBuf>,
69}
70
71pub const GLOBAL_CONFIG_DIR: &str = ".pijulconfig";
72const CONFIG_DIR: &str = "pijul";
73
74pub fn global_config_dir() -> Option<PathBuf> {
75 if let Ok(path) = std::env::var("PIJUL_CONFIG_DIR") {
76 let dir = std::path::PathBuf::from(path);
77 Some(dir)
78 } else if let Some(mut dir) = dirs_next::config_dir() {
79 dir.push(CONFIG_DIR);
80 Some(dir)
81 } else {
82 None
83 }
84}
85
86impl Global {
87 pub fn load() -> Result<(Global, u64), anyhow::Error> {
88 if let Some(mut dir) = global_config_dir() {
89 dir.push("config.toml");
90 let (s, meta) = std::fs::read(&dir)
91 .and_then(|x| Ok((x, std::fs::metadata(&dir)?)))
92 .or_else(|e| {
93 if let Some(mut dir) = dirs_next::home_dir() {
95 dir.push(".config");
96 dir.push(CONFIG_DIR);
97 dir.push("config.toml");
98 std::fs::read(&dir).and_then(|x| Ok((x, std::fs::metadata(&dir)?)))
99 } else {
100 Err(e.into())
101 }
102 })
103 .or_else(|e| {
104 if let Some(mut dir) = dirs_next::home_dir() {
106 dir.push(GLOBAL_CONFIG_DIR);
107 std::fs::read(&dir).and_then(|x| Ok((x, std::fs::metadata(&dir)?)))
108 } else {
109 Err(e.into())
110 }
111 })?;
112 debug!("s = {:?}", s);
113 if let Ok(t) = toml::from_slice(&s) {
114 let ts = meta
115 .modified()?
116 .duration_since(std::time::SystemTime::UNIX_EPOCH)
117 .unwrap()
118 .as_secs();
119 Ok((t, ts))
120 } else {
121 bail!("Could not read configuration file at {:?}", dir)
122 }
123 } else {
124 bail!("Global configuration file missing")
125 }
126 }
127}
128
129#[derive(Debug, Serialize, Deserialize, Default)]
130pub struct Config {
131 pub default_remote: Option<String>,
132 #[serde(default, skip_serializing_if = "Vec::is_empty")]
133 pub extra_dependencies: Vec<String>,
134 #[serde(default, skip_serializing_if = "Vec::is_empty")]
135 pub remotes: Vec<RemoteConfig>,
136 #[serde(default)]
137 pub hooks: Hooks,
138 pub unrecord_changes: Option<usize>,
139 pub reset_overwrites_changes: Option<Choice>,
140 pub colors: Option<Choice>,
141 pub pager: Option<Choice>,
142}
143
144#[derive(Debug, Serialize, Deserialize)]
145#[serde(untagged)]
146pub enum RemoteConfig {
147 Ssh {
148 name: String,
149 ssh: String,
150 },
151 Http {
152 name: String,
153 http: String,
154 #[serde(default)]
155 headers: HashMap<String, RemoteHttpHeader>,
156 },
157}
158
159impl RemoteConfig {
160 pub fn name(&self) -> &str {
161 match self {
162 RemoteConfig::Ssh { name, .. } => name,
163 RemoteConfig::Http { name, .. } => name,
164 }
165 }
166}
167
168#[derive(Debug, Serialize, Deserialize)]
169#[serde(untagged)]
170pub enum RemoteHttpHeader {
171 String(String),
172 Shell(Shell),
173}
174
175#[derive(Debug, Serialize, Deserialize)]
176pub struct Shell {
177 pub shell: String,
178}
179
180#[derive(Debug, Serialize, Deserialize, Default)]
181pub struct Hooks {
182 #[serde(default)]
183 pub record: Vec<HookEntry>,
184}
185
186#[derive(Debug, Serialize, Deserialize)]
187pub struct HookEntry(toml::Value);
188
189#[derive(Debug, Serialize, Deserialize)]
190struct RawHook {
191 command: String,
192 args: Vec<String>,
193}
194
195pub fn shell_cmd(s: &str) -> Result<String, anyhow::Error> {
196 let out = if cfg!(target_os = "windows") {
197 std::process::Command::new("cmd")
198 .args(&["/C", s])
199 .output()
200 .expect("failed to execute process")
201 } else {
202 std::process::Command::new(std::env::var("SHELL").unwrap_or("sh".to_string()))
203 .arg("-c")
204 .arg(s)
205 .output()
206 .expect("failed to execute process")
207 };
208 Ok(String::from_utf8(out.stdout)?.trim().to_string())
209}
210
211impl HookEntry {
212 pub fn run(&self, path: PathBuf) -> Result<(), anyhow::Error> {
213 let (proc, s) = match &self.0 {
214 toml::Value::String(ref s) => {
215 if s.is_empty() {
216 return Ok(());
217 }
218 (
219 if cfg!(target_os = "windows") {
220 std::process::Command::new("cmd")
221 .current_dir(path)
222 .args(&["/C", s])
223 .output()
224 .expect("failed to execute process")
225 } else {
226 std::process::Command::new(
227 std::env::var("SHELL").unwrap_or("sh".to_string()),
228 )
229 .current_dir(path)
230 .arg("-c")
231 .arg(s)
232 .output()
233 .expect("failed to execute process")
234 },
235 s.clone(),
236 )
237 }
238 v => {
239 let hook = v.clone().try_into::<RawHook>()?;
240 (
241 std::process::Command::new(&hook.command)
242 .current_dir(path)
243 .args(&hook.args)
244 .output()
245 .expect("failed to execute process"),
246 hook.command,
247 )
248 }
249 };
250 if !proc.status.success() {
251 let mut stderr = std::io::stderr();
252 writeln!(stderr, "Hook {:?} exited with code {:?}", s, proc.status)?;
253 std::process::exit(proc.status.code().unwrap_or(1))
254 }
255 Ok(())
256 }
257}
258
259#[derive(Debug, Serialize, Deserialize)]
260struct Remote_ {
261 ssh: Option<SshRemote>,
262 local: Option<String>,
263 url: Option<String>,
264}
265
266#[derive(Debug)]
267pub enum Remote {
268 Ssh(SshRemote),
269 Local { local: String },
270 Http { url: String },
271 None,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct SshRemote {
276 pub addr: String,
277}
278
279impl<'de> serde::Deserialize<'de> for Remote {
280 fn deserialize<D>(deserializer: D) -> Result<Remote, D::Error>
281 where
282 D: serde::de::Deserializer<'de>,
283 {
284 let r = Remote_::deserialize(deserializer)?;
285 if let Some(ssh) = r.ssh {
286 Ok(Remote::Ssh(ssh))
287 } else if let Some(local) = r.local {
288 Ok(Remote::Local { local })
289 } else if let Some(url) = r.url {
290 Ok(Remote::Http { url })
291 } else {
292 Ok(Remote::None)
293 }
294 }
295}
296
297impl serde::Serialize for Remote {
298 fn serialize<D>(&self, serializer: D) -> Result<D::Ok, D::Error>
299 where
300 D: serde::ser::Serializer,
301 {
302 let r = match *self {
303 Remote::Ssh(ref ssh) => Remote_ {
304 ssh: Some(ssh.clone()),
305 local: None,
306 url: None,
307 },
308 Remote::Local { ref local } => Remote_ {
309 local: Some(local.to_string()),
310 ssh: None,
311 url: None,
312 },
313 Remote::Http { ref url } => Remote_ {
314 local: None,
315 ssh: None,
316 url: Some(url.to_string()),
317 },
318 Remote::None => Remote_ {
319 local: None,
320 ssh: None,
321 url: None,
322 },
323 };
324 r.serialize(serializer)
325 }
326}
327
328pub fn load_theme() -> Result<Box<dyn theme::Theme>, anyhow::Error> {
330 if let Ok((config, _)) = Global::load() {
331 let color_choice = config.colors.unwrap_or_default();
332
333 match color_choice {
334 Choice::Auto | Choice::Always => Ok(Box::new(theme::ColorfulTheme::default())),
335 Choice::Never => Ok(Box::new(theme::SimpleTheme)),
336 }
337 } else {
338 Ok(Box::new(theme::ColorfulTheme::default()))
339 }
340}