Adds a pijul_config::Config
struct that uses the figment
crate to merge global and local configuration files.
Notable changes:
pijul_config::Config
struct that merges user configuration from global/local config filespijul_config::Shared
struct containing all of the configuration options that can be used in both global and local config filespijul_config::{global::Global, local::Local}
structs that contain global/local-specific options along with the shared options (these are what get serialized/deserialized on disk)pijul_config::Remote
- it it seemed to be completely unused and a duplicate of pijul_config::RemoteConfig
(now renamed to pijul_config::Remote
to replace it)pijul_repository
to pijul_config
Future work:
pijul_config::shell_cmd()
should be moved to pijul_remote
--config
flag)pijul_config
crate (currently subcommands have various hacks that can be removed)Since this is already quite a large change, migration of the codebase to this refactored crate will happen in a different change.
Z4PPQZUGHT5F5VFFBQBIW2J3OLHP4SF33QMT6POFCX6JS7L6G7GQC
SXEYMYF7P4RZMZ46WPL4IZUTSQ2ATBWYZX7QNVMS3SGOYXYOHAGQC
L4JXJHWXYNCL4QGJXNKKTOKKTAXKKXBJUUY7HFZGEUZ5A2V5H34QC
SEWGHUHQEEBJR7UPG3PSU7DSM376R43QEYENAZK325W46DCFMXKAC
BZSC7VMYSFRXDHDDAMCDR6X67FN5VWIBOSE76BQLX7OCVOJFUA3AC
SLJ3OHD4F6GJGZ3SV2D7DMR3PXYHPSI64X77KZ3RJ24EGEX6ZNQAC
CCLLB7OIFNFYJZTG3UCI7536TOCWSCSXR67VELSB466R24WLJSDAC
KWAGWB73AMLJFK2Z7SBKHHKKHFRX7AQKXCWDN2MBX72RYCNMB36QC
VL7ZYKHBPKLNY5SA5QBW56SJ7LBBCKCGV5UAYLVF75KY6PPBOD4AC
5BB266P6HPUGYEVR7QNNOA62EFPYPUYJ3UMLE5J3LLYMSUWXANIQC
I24UEJQLCH2SOXA4UHIYWTRDCHSOPU7AFTRUOTX7HZIAV4AZKYEQC
TFPETWTVADLG2DL7WERHJPGMJVOY4WOKCRWB3NZ3YOOQ4CVAUHBAC
EEBKW7VTILH6AGGV57ZIJ3DJGYHDSYBWGU3C7Q4WWAKSVNUGIYMQC
ZSFJT4SFIAS7WBODRZOFKKG4SVYBC5PC6XY75WYN7CCQ3SMV7IUQC
CB7UPUQFOUH6M32KXQZL25EV4IH3XSK56RG3OYNRZBBFEABSNCXQC
FVU3Y2U3R7B6SBA5GJ227NR2JQMMFMDREMW63QODA2EUXU3754ZQC
FVQYZQFL7WHSC3UUPJ4IWTP7SKTDQ4K6K5HY4EDK3JKXG3CQNZEAC
4OJWMSOWWNT5N4W4FDMKBZB5UARCLGV3SRZVKGR4EFAYFUMUHM7AC
6FRPUHWKBAWIYN6B6YDFQG2SFWZ6MBBYOYXFUN6DRZ4HPDSKFANQC
H4AU6QRPRDRFW3V7NN5CJ6DHLEUBYGNLRZ5GYV6ULBGRMOPCJQXQC
DOEG3V7UAVYBKLIPHSNWWONYDORKNJEZL2LD4EWGGUDB2SK6BOFQC
QCPIBC6MDPFE42KWELKZQ3ORNEOPSBXR7C7H6Z3ZT62RNVBFN73QC
EJ7TFFOWLM5EXYX57NJZZX3NLPBLLMRX7CGJYC75DJZ5LYXOQPJAC
LZOGKBJXRQJKXHYNNENJFGNLP5SHIXGSV6HDB7UVOP7FSA5EUNCQC
RVAH6PXA7H7NUDTF7Q52I7EXGXVJVMGI2LTNN6L3MVEDEMAXVH4AC
X642QQQTS4X2DENIZT7PGJN2M2FYVFMGGANXSZHJ7LBP6442Z6IAC
7UU3TV5W23QA7LLRBSBXEYPRMIVXPW4FNENEEE7ZEJYXDLXHVX4AC
HRNMY2PGRTO7DCLJJ3R3HUOBFYQLGYE4SXPNZ4CP6IJKFHKMXXMAC
2TWREKSRQN3QBIIJQ6Q4T3RVBTKSYA5N6W4YBRMRWD22WWS3DGQAC
2MKP7CB7FKQUNEAV3YPEJ7FNFW75VGGQIYPQRI54BFXGCUOQESPAC
HJVWPKWVSL5ZXALZOT4BOQUWWNGH62OU6YLSZQQEIOB37QQGHK6AC
LTI3LT2GJHQMH2G2RYVSKR4IZJY24L6O2KIZTRNKLZPJMOKTD56AC
67GIAQEUQG3KUD7YTYNUWK33BKWPFVNT4YPQMZ3RCALOZ2STDLRQC
OUEZV7ELFLRUF5LPVVBPLUSYUD77UZ7CVZAEB6G35CNISGQV6YQAC
use std::path::PathBuf;
use serde_derive::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Template {
pub message: Option<PathBuf>,
pub description: Option<PathBuf>,
}
use std::collections::HashMap;
use serde_derive::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RemoteConfig {
Ssh {
name: String,
ssh: String,
},
Http {
name: String,
http: String,
#[serde(default)]
headers: HashMap<String, RemoteHttpHeader>,
},
}
impl RemoteConfig {
pub fn name(&self) -> &str {
match self {
RemoteConfig::Ssh { name, .. } => name,
RemoteConfig::Http { name, .. } => name,
}
}
pub fn url(&self) -> &str {
match self {
RemoteConfig::Ssh { ssh, .. } => ssh,
RemoteConfig::Http { http, .. } => http,
}
}
pub fn db_uses_name(&self) -> bool {
match self {
RemoteConfig::Ssh { .. } => false,
RemoteConfig::Http { .. } => true,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RemoteHttpHeader {
String(String),
Shell(Shell),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Shell {
pub shell: String,
}
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use crate::remote::RemoteConfig;
use crate::{REPOSITORY_CONFIG_FILE, Shared};
use serde_derive::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct Local {
#[serde(skip)]
source_file: Option<PathBuf>,
pub default_remote: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub extra_dependencies: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub remotes: Vec<RemoteConfig>,
#[serde(flatten)]
pub shared_config: Shared,
}
impl Local {
pub fn config_file(repository_path: &Path) -> PathBuf {
repository_path
.join(libpijul::DOT_DIR)
.join(REPOSITORY_CONFIG_FILE)
}
pub fn read_contents(config_path: &Path) -> Result<String, anyhow::Error> {
let mut config_file = File::open(config_path)?;
let mut file_contents = String::new();
config_file.read_to_string(&mut file_contents)?;
Ok(file_contents)
}
pub fn parse_contents(config_path: &Path, toml_data: &str) -> Result<Self, anyhow::Error> {
let mut config: Self = toml::from_str(&toml_data)?;
// Store the location of the original configuration file, so it can later be written to
// The `source_file` field is annotated with `#[serde(skip)]` and should be always be None unless set manually
assert!(config.source_file.is_none());
config.source_file = Some(config_path.to_path_buf());
Ok(config)
}
pub fn write(&self) -> Result<(), anyhow::Error> {
let mut config_file = File::create(self.source_file.clone().unwrap())?;
let file_contents = toml::to_string_pretty(self)?;
config_file.write_all(file_contents.as_bytes())?;
Ok(())
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Global {
#[serde(default)]
pub author: Author,
pub const REPOSITORY_CONFIG_FILE: &str = "config";
pub const GLOBAL_CONFIG_FILE: &str = ".pijulconfig";
pub const CONFIG_DIR: &str = "pijul";
pub const CONFIG_FILE: &str = "config.toml";
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Shared {
pub template: Option<Templates>,
pub ignore_kinds: Option<HashMap<String, Vec<String>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Author {
// Older versions called this 'name', but 'username' is more descriptive
#[serde(alias = "name", default, skip_serializing_if = "String::is_empty")]
pub username: String,
#[serde(alias = "full_name", default, skip_serializing_if = "String::is_empty")]
pub display_name: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub email: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub origin: String,
// This has been moved to identity::Config, but we should still be able to read the values
#[serde(default, skip_serializing)]
pub key_path: Option<PathBuf>,
}
impl Default for Author {
fn default() -> Self {
Self {
username: String::new(),
email: String::new(),
display_name: whoami::realname(),
origin: String::new(),
key_path: None,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum Choice {
#[serde(rename = "auto")]
Auto,
#[serde(rename = "always")]
Always,
#[serde(rename = "never")]
Never,
}
impl Default for Choice {
fn default() -> Self {
Self::Auto
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Templates {
pub message: Option<PathBuf>,
pub description: Option<PathBuf>,
}
pub const GLOBAL_CONFIG_DIR: &str = ".pijulconfig";
const CONFIG_DIR: &str = "pijul";
pub fn global_config_dir() -> Option<PathBuf> {
if let Ok(path) = std::env::var("PIJUL_CONFIG_DIR") {
let dir = std::path::PathBuf::from(path);
Some(dir)
} else if let Some(mut dir) = dirs_next::config_dir() {
dir.push(CONFIG_DIR);
Some(dir)
} else {
None
}
}
fn try_load_file(path: impl Into<PathBuf> + AsRef<Path>) -> Option<(io::Result<File>, PathBuf)> {
let pp = path.as_ref();
match File::open(pp) {
Ok(v) => Some((Ok(v), path.into())),
Err(e) if e.kind() == io::ErrorKind::NotFound => None,
Err(e) => Some((Err(e), path.into())),
}
pub template: Option<Template>,
#[serde(default)]
pub hooks: Hooks,
impl Global {
pub fn load() -> Result<(Global, Option<u64>), anyhow::Error> {
let res = None
.or_else(|| {
let mut path = global_config_dir()?;
path.push("config.toml");
try_load_file(path)
})
.or_else(|| {
// Read from `$HOME/.config/pijul` dir
let mut path = dirs_next::home_dir()?;
path.push(".config");
path.push(CONFIG_DIR);
path.push("config.toml");
try_load_file(path)
})
.or_else(|| {
// Read from `$HOME/.pijulconfig`
let mut path = dirs_next::home_dir()?;
path.push(GLOBAL_CONFIG_DIR);
try_load_file(path)
});
let Some((file, path)) = res else {
return Ok((Global::default(), None));
};
let mut file = file.map_err(|e| {
anyhow!("Could not open configuration file at {}", path.display()).context(e)
})?;
let mut buf = String::new();
file.read_to_string(&mut buf).map_err(|e| {
anyhow!("Could not read configuration file at {}", path.display()).context(e)
})?;
debug!("buf = {:?}", buf);
let global: Global = toml::from_str(&buf).map_err(|e| {
anyhow!("Could not parse configuration file at {}", path.display()).context(e)
})?;
let metadata = file.metadata()?;
let file_age = metadata
.modified()?
.duration_since(std::time::SystemTime::UNIX_EPOCH)?
.as_secs();
#[derive(Debug, Default, Deserialize)]
pub struct Config {
// Store a copy of the original files, so that they can be modified independently
#[serde(skip)]
global_config: Option<Global>,
#[serde(skip)]
local_config: Option<Local>,
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RemoteConfig {
Ssh {
name: String,
ssh: String,
},
Http {
name: String,
http: String,
#[serde(default)]
headers: HashMap<String, RemoteHttpHeader>,
},
}
impl Config {
pub fn load(repository_path: &Path) -> Result<Self, anyhow::Error> {
let global_config_path = Global::config_file().unwrap();
let local_config_path = Local::config_file(&repository_path);
impl RemoteConfig {
pub fn name(&self) -> &str {
match self {
RemoteConfig::Ssh { name, .. } => name,
RemoteConfig::Http { name, .. } => name,
}
}
let global_config_contents = Global::read_contents(&global_config_path)?;
let local_config_contents = Local::read_contents(&local_config_path)?;
// Validate that the configuration sources are correct
let global_config = Global::parse_contents(&global_config_path, &global_config_contents)?;
let local_config = Local::parse_contents(&local_config_path, &local_config_contents)?;
pub fn url(&self) -> &str {
match self {
RemoteConfig::Ssh { ssh, .. } => ssh,
RemoteConfig::Http { http, .. } => http,
}
// Merge the two configuration values, using the raw TOML string instead of the deserialized structs.
// Figment uses a dictionary to store which fields are set, and using an already-deserialized
// struct will guarantee that each layer will override the previous one.
//
// For example, if the optional `unrecord_changes` field is set as 1 globally but not set locally:
// - Using deserialized structs (incorrect behaviour):
// - Global config is set to Some(1)
// - Local config is set to None - no value was found, so serde inserted the default
// - The local config technically has a value set, so the final (incorrect) value is None
// - Using strings (correct behaviour):
// - Global config is set to Some(1)
// - Local config is unset
// - The final (correct) value is Some(1)
let mut config: Self = Figment::new()
.merge(Toml::string(&global_config_contents))
.merge(Toml::string(&local_config_contents))
.extract()?;
// These fields are annotated with #[serde(skip)] and therefore should be None
assert!(config.global_config.is_none());
assert!(config.local_config.is_none());
// Store the original configuration sources so they can be modified later
config.global_config = Some(global_config);
config.local_config = Some(local_config);
Ok(config)
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RemoteHttpHeader {
String(String),
Shell(Shell),
}
pub fn local(&self) -> Option<Local> {
self.local_config.clone()
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Shell {
pub shell: String,
}
/// Choose the right dialoguer theme based on user's config
pub fn theme(&self) -> Box<dyn theme::Theme + Send + Sync> {
let color_choice = self.colors.unwrap_or_default();
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Hooks {
#[serde(default)]
pub record: Vec<HookEntry>,
match color_choice {
Choice::Auto | Choice::Always => Box::new(theme::ColorfulTheme::default()),
Choice::Never => Box::new(theme::SimpleTheme),
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct HookEntry(toml::Value);
#[derive(Debug, Serialize, Deserialize)]
struct RawHook {
command: String,
args: Vec<String>,
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub enum Choice {
#[default]
#[serde(rename = "auto")]
Auto,
#[serde(rename = "always")]
Always,
#[serde(rename = "never")]
Never,
}
impl HookEntry {
pub fn run(&self, path: PathBuf) -> Result<(), anyhow::Error> {
let (proc, s) = match &self.0 {
toml::Value::String(s) => {
if s.is_empty() {
return Ok(());
}
(
if cfg!(target_os = "windows") {
std::process::Command::new("cmd")
.current_dir(path)
.args(&["/C", s])
.output()
.expect("failed to execute process")
} else {
std::process::Command::new(
std::env::var("SHELL").unwrap_or("sh".to_string()),
)
.current_dir(path)
.arg("-c")
.arg(s)
.output()
.expect("failed to execute process")
},
s.clone(),
)
}
v => {
let hook = v.clone().try_into::<RawHook>()?;
(
std::process::Command::new(&hook.command)
.current_dir(path)
.args(&hook.args)
.output()
.expect("failed to execute process"),
hook.command,
)
}
};
if !proc.status.success() {
let mut stderr = std::io::stderr();
writeln!(stderr, "Hook {:?} exited with code {:?}", s, proc.status)?;
std::process::exit(proc.status.code().unwrap_or(1))
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
struct Remote_ {
ssh: Option<SshRemote>,
local: Option<String>,
url: Option<String>,
}
#[derive(Debug)]
pub enum Remote {
Ssh(SshRemote),
Local { local: String },
Http { url: String },
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SshRemote {
pub addr: String,
}
impl<'de> serde::Deserialize<'de> for Remote {
fn deserialize<D>(deserializer: D) -> Result<Remote, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let r = Remote_::deserialize(deserializer)?;
if let Some(ssh) = r.ssh {
Ok(Remote::Ssh(ssh))
} else if let Some(local) = r.local {
Ok(Remote::Local { local })
} else if let Some(url) = r.url {
Ok(Remote::Http { url })
} else {
Ok(Remote::None)
}
}
}
impl serde::Serialize for Remote {
fn serialize<D>(&self, serializer: D) -> Result<D::Ok, D::Error>
where
D: serde::ser::Serializer,
{
let r = match *self {
Remote::Ssh(ref ssh) => Remote_ {
ssh: Some(ssh.clone()),
local: None,
url: None,
},
Remote::Local { ref local } => Remote_ {
local: Some(local.to_string()),
ssh: None,
url: None,
},
Remote::Http { ref url } => Remote_ {
local: None,
ssh: None,
url: Some(url.to_string()),
},
Remote::None => Remote_ {
local: None,
ssh: None,
url: None,
},
};
r.serialize(serializer)
}
/// Choose the right dialoguer theme based on user's config
pub fn load_theme() -> Result<Box<dyn theme::Theme>, anyhow::Error> {
if let Ok((config, _)) = Global::load() {
let color_choice = config.colors.unwrap_or_default();
match color_choice {
Choice::Auto | Choice::Always => Ok(Box::new(theme::ColorfulTheme::default())),
Choice::Never => Ok(Box::new(theme::SimpleTheme)),
}
} else {
Ok(Box::new(theme::ColorfulTheme::default()))
}
}
use std::io::Write;
use std::path::PathBuf;
use serde_derive::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct Hooks {
#[serde(default)]
pub record: Vec<HookEntry>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HookEntry(toml::Value);
#[derive(Debug, Serialize, Deserialize)]
struct RawHook {
command: String,
args: Vec<String>,
}
impl HookEntry {
pub fn run(&self, path: PathBuf) -> Result<(), anyhow::Error> {
let (proc, s) = match &self.0 {
toml::Value::String(s) => {
if s.is_empty() {
return Ok(());
}
(
if cfg!(target_os = "windows") {
std::process::Command::new("cmd")
.current_dir(path)
.args(&["/C", s])
.output()
.expect("failed to execute process")
} else {
std::process::Command::new(
std::env::var("SHELL").unwrap_or("sh".to_string()),
)
.current_dir(path)
.arg("-c")
.arg(s)
.output()
.expect("failed to execute process")
},
s.clone(),
)
}
v => {
let hook = v.clone().try_into::<RawHook>()?;
(
std::process::Command::new(&hook.command)
.current_dir(path)
.args(&hook.args)
.output()
.expect("failed to execute process"),
hook.command,
)
}
};
if !proc.status.success() {
let mut stderr = std::io::stderr();
writeln!(stderr, "Hook {:?} exited with code {:?}", s, proc.status)?;
std::process::exit(proc.status.code().unwrap_or(1))
}
Ok(())
}
}
use crate::author::Author;
use crate::{CONFIG_DIR, CONFIG_FILE, GLOBAL_CONFIG_FILE, Shared};
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use serde_derive::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Global {
#[serde(skip)]
source_file: Option<PathBuf>,
#[serde(default)]
pub author: Author,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub ignore_kinds: HashMap<String, Vec<String>>,
#[serde(flatten)]
pub shared_config: Shared,
}
impl Global {
/// Select which configuration file to use
pub fn config_file() -> Option<PathBuf> {
// 1. PIJUL_CONFIG_DIR environment variable
std::env::var("PIJUL_CONFIG_DIR")
.ok()
.map(PathBuf::from)
// 2. ~/.config/pijul/config.toml
.or_else(|| match dirs_next::config_dir() {
Some(config_dir) => {
let config_path = config_dir.join(CONFIG_DIR).join(CONFIG_FILE);
match config_path.exists() {
true => Some(config_path),
false => None,
}
}
None => None,
})
// 3. ~/.pijulconfig
.or_else(|| match dirs_next::home_dir() {
Some(home_dir) => {
let config_path = home_dir.join(GLOBAL_CONFIG_FILE);
match config_path.exists() {
true => Some(config_path),
false => None,
}
}
None => None,
})
}
pub fn read_contents(config_path: &Path) -> Result<String, anyhow::Error> {
let mut config_file = File::open(config_path)?;
let mut file_contents = String::new();
config_file.read_to_string(&mut file_contents)?;
Ok(file_contents)
}
pub fn parse_contents(config_path: &Path, toml_data: &str) -> Result<Self, anyhow::Error> {
let mut config: Self = toml::from_str(&toml_data)?;
// Store the location of the original configuration file, so it can later be written to
// The `source_file` field is annotated with `#[serde(skip)]` and should be always be None unless set manually
assert!(config.source_file.is_none());
config.source_file = Some(config_path.to_path_buf());
Ok(config)
}
pub fn write(&self) -> Result<(), anyhow::Error> {
let mut config_file = File::create(self.source_file.clone().unwrap())?;
let file_contents = toml::to_string_pretty(self)?;
config_file.write_all(file_contents.as_bytes())?;
Ok(())
}
}
// pub fn global_config_dir() -> Option<PathBuf> {
// if let Ok(path) = std::env::var("PIJUL_CONFIG_DIR") {
// let dir = std::path::PathBuf::from(path);
// Some(dir)
// } else if let Some(mut dir) = dirs_next::config_dir() {
// dir.push(CONFIG_DIR);
// Some(dir)
// } else {
// None
// }
// }
// impl Global {
// pub fn load() -> Result<(Global, Option<u64>), anyhow::Error> {
// let res = None
// .or_else(|| {
// let mut path = global_config_dir()?;
// path.push("config.toml");
// try_load_file(path)
// })
// .or_else(|| {
// // Read from `$HOME/.config/pijul` dir
// let mut path = dirs_next::home_dir()?;
// path.push(".config");
// path.push(CONFIG_DIR);
// path.push("config.toml");
// try_load_file(path)
// })
// .or_else(|| {
// // Read from `$HOME/.pijulconfig`
// let mut path = dirs_next::home_dir()?;
// path.push(GLOBAL_CONFIG_DIR);
// try_load_file(path)
// });
// let Some((file, path)) = res else {
// return Ok((Global::default(), None));
// };
// let mut file = file.map_err(|e| {
// anyhow!("Could not open configuration file at {}", path.display()).context(e)
// })?;
// let mut buf = String::new();
// file.read_to_string(&mut buf).map_err(|e| {
// anyhow!("Could not read configuration file at {}", path.display()).context(e)
// })?;
// debug!("buf = {:?}", buf);
// let global: Global = toml::from_str(&buf).map_err(|e| {
// anyhow!("Could not parse configuration file at {}", path.display()).context(e)
// })?;
// let metadata = file.metadata()?;
// let file_age = metadata
// .modified()?
// .duration_since(std::time::SystemTime::UNIX_EPOCH)?
// .as_secs();
// Ok((global, Some(file_age)))
// }
// }
use std::path::PathBuf;
use serde_derive::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Author {
// Older versions called this 'name', but 'username' is more descriptive
#[serde(alias = "name", default, skip_serializing_if = "String::is_empty")]
pub username: String,
#[serde(alias = "full_name", default, skip_serializing_if = "String::is_empty")]
pub display_name: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub email: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub origin: String,
// This has been moved to identity::Config, but we should still be able to read the values
#[serde(default, skip_serializing)]
pub key_path: Option<PathBuf>,
}
impl Default for Author {
fn default() -> Self {
Self {
username: String::new(),
email: String::new(),
display_name: whoami::realname(),
origin: String::new(),
key_path: None,
}
}
}