Refactor `pijul-config` to support layered configuration

finchie
Jul 27, 2025, 10:19 AM
Z4PPQZUGHT5F5VFFBQBIW2J3OLHP4SF33QMT6POFCX6JS7L6G7GQC

Dependencies

  • [2] SXEYMYF7 Fixing the bad changes in history (unfortunately, by rebooting).
  • [3] L4JXJHWX pijul/*: reorganize imports and remove extern crate
  • [4] SEWGHUHQ .pijul/config: simplify remotes and hooks
  • [5] BZSC7VMY address clippy lints
  • [6] SLJ3OHD4 unrecord: show list of changes if none were given as arguments
  • [7] CCLLB7OI Upgrading to Sanakirja 0.15 + version bump
  • [8] KWAGWB73 Adding extra dependencies from the config file
  • [9] VL7ZYKHB Running hooks through shell on Windows and Unix
  • [10] 5BB266P6 Optional colours in the global config file
  • [11] I24UEJQL Various post-fire fixes
  • [12] TFPETWTV Add config options for patch message templates
  • [13] EEBKW7VT Keys and identities
  • [14] ZSFJT4SF Allow remotes to have a different push and pull address
  • [15] CB7UPUQF Customizable ignore_kinds (and a fix of .write())
  • [16] FVU3Y2U3 Adding a local "unrecord_changes" option in addition to the global one
  • [17] FVQYZQFL Create dialoguer themes based on global config
  • [18] 4OJWMSOW Fully replace crate::Identity
  • [19] 6FRPUHWK Fix identity tests
  • [20] H4AU6QRP New config for HTTP remotes
  • [21] DOEG3V7U Only re-write identity data when changed
  • [22] QCPIBC6M Make the default remote configurable through the cli
  • [23] EJ7TFFOW Re-adding Cargo.lock
  • [24] LZOGKBJX new command `pijul client` for authenticating to a HTTP server
  • [25] RVAH6PXA Getting libpijul to compile to WASM32
  • [26] X642QQQT Run record hooks from the repository root
  • [27] 7UU3TV5W Refactor `pijul::config` into new crate
  • [28] HRNMY2PG Correctly read empty config file
  • [29] 2TWREKSR Treat missing config file as empty
  • [30] 2MKP7CB7 Move dependencies into workspace `Cargo.toml`
  • [31] HJVWPKWV Migrate crates to edition 2024
  • [32] LTI3LT2G Bump all dependencies to latest compatible minor versions
  • [33] 67GIAQEU Handle named remotes in pijul remote and remote delete
  • [34] OUEZV7EL 🩹 Resolve conflicts, bump Cargo.lock
  • [35] A3RM526Y Integrating identity malleability
  • [36] ICEK2JVG Add reset_overwrites_changes config option. When set to never --force is required for reset
  • [37] YWL2K3P7 Removing the `Direction` argument in pijul::remote::Repository::remote
  • [38] IUGP6ZGB Add support for ~/.config/pijul even on macos
  • [39] 5OGOE4VW Store the current channel in the pristine
  • [40] VQPAUKBQ channel switch as an alias to reset

Change contents

  • edit in Cargo.lock at line 115
    [32.1768]
    [25.6058]
    [[package]]
    name = "atomic"
    version = "0.6.1"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
    dependencies = [
    "bytemuck",
    ]
  • edit in Cargo.lock at line 283
    [34.1220]
    [23.6949]
    [[package]]
    name = "bytemuck"
    version = "1.23.1"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
  • edit in Cargo.lock at line 862
    [23.20939]
    [23.20939]
    name = "figment"
    version = "0.10.19"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
    dependencies = [
    "atomic",
    "serde",
    "toml",
    "uncased",
    "version_check",
    ]
    [[package]]
  • edit in Cargo.lock at line 2229
    [27.980]
    [27.989]
    "figment",
    "libpijul",
  • edit in Cargo.lock at line 3374
    [23.71926]
    [23.71926]
    name = "uncased"
    version = "0.9.10"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
    dependencies = [
    "version_check",
    ]
    [[package]]
  • edit in Cargo.toml at line 57
    [30.6593]
    [32.244]
    figment = { version = "0.10", features = ["toml"] }
  • file addition: template.rs (----------)
    [27.41]
    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>,
    }
  • file addition: remote.rs (----------)
    [27.41]
    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,
    }
  • file addition: local.rs (----------)
    [27.41]
    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(())
    }
    }
  • edit in pijul-config/src/lib.rs at line 1
    [2.89866]
    [2.89867]
    pub mod author;
    pub mod global;
    pub mod hook;
    pub mod local;
    pub mod remote;
    pub mod template;
    use author::Author;
    use global::Global;
    use hook::Hooks;
    use local::Local;
    use remote::RemoteConfig;
    use template::Template;
  • replacement in pijul-config/src/lib.rs at line 16
    [2.89898][29.0:92]()
    use std::fs::File;
    use std::io;
    use std::io::{Read, Write};
    use std::path::{Path, PathBuf};
    [2.89898]
    [2.89898]
    use std::path::Path;
  • edit in pijul-config/src/lib.rs at line 18
    [2.89899][29.93:113]()
    use anyhow::anyhow;
  • replacement in pijul-config/src/lib.rs at line 19
    [17.22][3.960:976](),[7.11466][3.960:976](),[2.89899][3.960:976]()
    use log::debug;
    [17.22]
    [3.976]
    use figment::Figment;
    use figment::providers::{Format, Toml};
  • replacement in pijul-config/src/lib.rs at line 23
    [3.1021][29.114:164](),[29.164][2.89940:89960](),[2.89940][2.89940:89960](),[2.89960][28.0:22](),[28.22][13.89:113](),[2.89960][13.89:113]()
    #[derive(Debug, Default, Serialize, Deserialize)]
    pub struct Global {
    #[serde(default)]
    pub author: Author,
    [3.1021]
    [6.781]
    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 {
  • replacement in pijul-config/src/lib.rs at line 34
    [10.63][12.25:62](),[12.62][15.603:663](),[12.62][10.63:66](),[15.663][10.63:66](),[10.63][10.63:66](),[10.66][21.2124:2187](),[21.2187][13.162:182](),[13.162][13.162:182](),[13.182][19.4662:4739](),[19.4739][18.14349:14429](),[18.14349][18.14349:14429](),[18.14429][19.4740:4766](),[19.4766][18.14456:14987](),[18.14456][18.14456:14987](),[18.14987][19.4767:4804](),[19.4804][18.15025:15184](),[18.15025][18.15025:15184](),[18.15184][13.302:305](),[13.302][13.302:305](),[13.305][10.66:251](),[10.66][10.66:251](),[10.251][12.63:66](),[12.66][17.139:220](),[17.220][12.66:202](),[12.66][12.66:202](),[12.202][2.90002:90005](),[10.251][2.90002:90005](),[6.822][2.90002:90005](),[2.90002][2.90002:90005](),[2.90005][13.306:358](),[13.358][5.295:329](),[2.90005][5.295:329](),[5.329][2.90047:90048](),[2.90047][2.90047:90048](),[2.90048][13.359:407](),[13.407][17.221:407](),[17.407][13.460:543](),[13.460][13.460:543](),[13.543][29.165:479]()
    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())),
    }
    [10.63]
    [29.479]
    pub template: Option<Template>,
    #[serde(default)]
    pub hooks: Hooks,
  • edit in pijul-config/src/lib.rs at line 38
    [29.481][29.481:482](),[29.482][2.90048:90062](),[13.543][2.90048:90062](),[2.90048][2.90048:90062](),[2.90062][29.483:1533]()
    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)
    })?;
  • replacement in pijul-config/src/lib.rs at line 39
    [29.1534][29.1534:2119]()
    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();
    [29.1534]
    [29.2119]
    #[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>,
  • replacement in pijul-config/src/lib.rs at line 47
    [29.2120][29.2120:2157](),[29.2157][2.90964:90972](),[2.90964][2.90964:90972]()
    Ok((global, Some(file_age)))
    }
    }
    [29.2120]
    [2.90972]
    // Global
    pub author: Author,
    #[serde(default)]
    pub ignore_kinds: HashMap<String, Vec<String>>,
  • replacement in pijul-config/src/lib.rs at line 52
    [2.90973][17.408:458](),[14.307][2.91023:91043](),[17.458][2.91023:91043](),[2.91023][2.91023:91043]()
    #[derive(Debug, Serialize, Deserialize, Default)]
    pub struct Config {
    [2.90973]
    [2.91084]
    // Local
  • replacement in pijul-config/src/lib.rs at line 54
    [2.91124][22.218:279]()
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    [2.91124]
    [8.22]
    #[serde(default)]
  • replacement in pijul-config/src/lib.rs at line 56
    [8.63][22.280:341]()
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    [8.63]
    [20.3591]
    #[serde(default)]
  • replacement in pijul-config/src/lib.rs at line 58
    [14.354][4.23:67](),[20.3627][4.23:67](),[2.91166][4.23:67]()
    #[serde(default)]
    pub hooks: Hooks,
    [20.3627]
    [16.0]
    // Shared
  • edit in pijul-config/src/lib.rs at line 64
    [10.315]
    [14.355]
    pub template: Option<Template>,
    #[serde(default)]
    pub hooks: hook::Hooks,
  • replacement in pijul-config/src/lib.rs at line 69
    [14.358][20.3628:3912](),[20.3912][14.439:441](),[14.439][14.439:441]()
    #[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>,
    },
    }
    [14.358]
    [14.441]
    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);
  • replacement in pijul-config/src/lib.rs at line 74
    [14.442][20.3913:4092](),[20.4092][33.0:16]()
    impl RemoteConfig {
    pub fn name(&self) -> &str {
    match self {
    RemoteConfig::Ssh { name, .. } => name,
    RemoteConfig::Http { name, .. } => name,
    }
    }
    [14.442]
    [33.16]
    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)?;
  • replacement in pijul-config/src/lib.rs at line 81
    [33.17][33.17:173](),[33.173][14.794:804](),[20.4092][14.794:804](),[14.794][14.794:804]()
    pub fn url(&self) -> &str {
    match self {
    RemoteConfig::Ssh { ssh, .. } => ssh,
    RemoteConfig::Http { http, .. } => http,
    }
    [33.17]
    [14.804]
    // 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)
  • replacement in pijul-config/src/lib.rs at line 110
    [33.175][33.175:341]()
    pub fn db_uses_name(&self) -> bool {
    match self {
    RemoteConfig::Ssh { .. } => false,
    RemoteConfig::Http { .. } => true,
    }
    [33.175]
    [33.341]
    pub fn global(&self) -> Option<Global> {
    self.global_config.clone()
  • edit in pijul-config/src/lib.rs at line 113
    [33.347][14.810:812](),[14.810][14.810:812]()
    }
  • replacement in pijul-config/src/lib.rs at line 114
    [14.1609][20.4093:4219](),[4.67][2.91196:91198](),[10.315][2.91196:91198](),[14.2296][2.91196:91198](),[20.4219][2.91196:91198](),[2.91196][2.91196:91198]()
    #[derive(Debug, Serialize, Deserialize)]
    #[serde(untagged)]
    pub enum RemoteHttpHeader {
    String(String),
    Shell(Shell),
    }
    [14.1609]
    [2.91198]
    pub fn local(&self) -> Option<Local> {
    self.local_config.clone()
    }
  • replacement in pijul-config/src/lib.rs at line 118
    [2.91199][17.488:529](),[17.529][20.4220:4262](),[20.4336][14.2396:2398](),[14.2396][14.2396:2398]()
    #[derive(Debug, Serialize, Deserialize)]
    pub struct Shell {
    pub shell: String,
    }
    [2.91199]
    [14.2398]
    /// 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();
  • replacement in pijul-config/src/lib.rs at line 122
    [14.2399][2.91199:91268](),[2.91199][2.91199:91268](),[2.91268][4.68:90](),[4.90][9.0:32]()
    #[derive(Debug, Serialize, Deserialize, Default)]
    pub struct Hooks {
    #[serde(default)]
    pub record: Vec<HookEntry>,
    [14.2399]
    [9.32]
    match color_choice {
    Choice::Auto | Choice::Always => Box::new(theme::ColorfulTheme::default()),
    Choice::Never => Box::new(theme::SimpleTheme),
    }
    }
  • replacement in pijul-config/src/lib.rs at line 129
    [9.35][9.35:214]()
    #[derive(Debug, Serialize, Deserialize)]
    pub struct HookEntry(toml::Value);
    #[derive(Debug, Serialize, Deserialize)]
    struct RawHook {
    command: String,
    args: Vec<String>,
    [9.35]
    [9.214]
    #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
    pub enum Choice {
    #[default]
    #[serde(rename = "auto")]
    Auto,
    #[serde(rename = "always")]
    Always,
    #[serde(rename = "never")]
    Never,
  • edit in pijul-config/src/lib.rs at line 154
    [24.4341][20.4848:4851](),[20.4848][20.4848:4851](),[20.4851][9.217:234](),[9.217][9.217:234](),[9.234][26.27:95](),[26.95][9.287:327](),[9.287][9.287:327](),[9.327][31.1734:1774](),[31.1774][9.371:587](),[9.371][9.371:587](),[9.587][26.96:143](),[26.143][9.587:923](),[9.587][9.587:923](),[9.923][26.144:187](),[26.187][9.923:1331](),[9.923][9.923:1331](),[9.1331][26.188:231](),[26.231][9.1331:1582](),[9.1331][9.1331:1582](),[9.1614][9.1614:1818](),[11.1906][11.1906:1921](),[2.91619][2.91619:93366](),[2.93366][17.530:536]()
    }
    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)
    }
  • edit in pijul-config/src/lib.rs at line 155
    [17.538][17.538:1039](),[17.1039][2.93366:93374](),[2.93366][2.93366:93374]()
    /// 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()))
    }
    }
  • file addition: hook.rs (----------)
    [27.41]
    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(())
    }
    }
  • file addition: global.rs (----------)
    [27.41]
    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)))
    // }
    // }
  • file addition: author.rs (----------)
    [27.41]
    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,
    }
    }
    }
  • edit in pijul-config/Cargo.toml at line 11
    [27.404]
    [30.1646]
    libpijul.workspace = true
  • edit in pijul-config/Cargo.toml at line 16
    [30.1753]
    [30.1775]
    figment.workspace = true