fix(expiry preservation): maintain expiry when not provided for update

Chasesomero
Mar 14, 2026, 6:51 PM
ZNRERVSVQ7XP6DKKOVYVKFV5AAV6S27QYAX3MZ2KTIM7DPRWDTKQC

Dependencies

  • [2] 4KJ45IJL Implement new identity management
  • [3] FOCBVLOU Implement testing for identity management
  • [4] 4OJWMSOW Fully replace crate::Identity
  • [5] KKNMDXAI Tweak identity subcommand
  • [6] 6FRPUHWK Fix identity tests
  • [7] DOEG3V7U Only re-write identity data when changed
  • [8] 32G3GOK7 Migrate from `dialoguer` to `pijul-interaction`
  • [9] N26HD5PF Replace `chrono` with `jiff`
  • [10] MBIKZPCC Add flags to disable the use of the system keyring, asking to prompt the key password directly instead
  • [11] YW6NICQV Migrate codebase to refactored `pijul_config` crate
  • [12] VQCREHLR Resolve conflicts in identity creation
  • [13] TVV36VDZ Solving conflicts
  • [14] WTPFQPO5 Add global `no_prompt` flag
  • [15] 5Z2Y7VGV Migrate `pijul::identity::Complete::prove` to `pijul::remote::prove`
  • [16] 6JKPLWMV Resolve hunk order conflicts

Change contents

  • replacement in pijul-identity/src/lib.rs at line 41
    [2.16968][12.668:698]()
    pub use create::CreateParams;
    [2.16968]
    [4.1919]
    pub use create::{CreateParams, parse_expiry_date};
  • edit in pijul/tests/identity.rs at line 206
    [3.4726]
    [3.4726]
    fn edit_preserves_existing_expiry() -> Result<(), Error> {
    use std::{fs, process::Command};
    let config_dir = tempfile::tempdir()?;
    fs::write(
    config_dir.path().join("config.toml"),
    "colors = 'never'\n[author]\nlogin = ''",
    )?;
    let new_status = Command::new(env!("CARGO_BIN_EXE_pijul"))
    .env("PIJUL_CONFIG_DIR", config_dir.path())
    .args([
    "identity",
    "new",
    "--no-link",
    "--no-prompt",
    "my_identity",
    "--display-name",
    default::FULL_NAME,
    "--expiry",
    default::EXPIRY,
    ])
    .status()?;
    assert!(new_status.success());
    let edit_status = Command::new(env!("CARGO_BIN_EXE_pijul"))
    .env("PIJUL_CONFIG_DIR", config_dir.path())
    .args([
    "identity",
    "edit",
    "--no-link",
    "--no-prompt",
    "my_identity",
    "--email",
    default::EMAIL,
    ])
    .status()?;
    assert!(edit_status.success());
    let identity_path = config_dir
    .path()
    .join("identities")
    .join("my_identity")
    .join("identity.toml");
    let identity_data = fs::read_to_string(identity_path)?;
    let toml_data = identity_data.parse::<toml::Value>().unwrap();
    assert_eq!(
    toml_data.get("email").and_then(toml::Value::as_str),
    Some(default::EMAIL)
    );
    let public_expiry = toml_data
    .get("public_key")
    .and_then(|value| value.get("expires"))
    .and_then(toml::Value::as_str)
    .expect("expected public key expiry to be preserved")
    .parse::<jiff::Timestamp>()?;
    assert_eq!(
    public_expiry.strftime("%Y-%m-%d").to_string(),
    default::EXPIRY
    );
    let secret_key = fs::read_to_string(
    config_dir
    .path()
    .join("identities")
    .join("my_identity")
    .join("secret_key.json"),
    )?;
    let secret_key: pijul_core::key::SecretKey = serde_json::from_str(&secret_key)?;
    assert_eq!(
    secret_key
    .expires
    .expect("expected secret key expiry to be preserved")
    .strftime("%Y-%m-%d")
    .to_string(),
    default::EXPIRY
    );
    Ok(())
    }
    #[test]
  • replacement in pijul/src/commands/identity.rs at line 64
    [9.2212][9.2212:2350]()
    let parsed_date: Timestamp = input
    .parse()
    .map_err(|error| anyhow!("Error parsing date: {error:#?}"))?;
    [9.2212]
    [2.42433]
    let parsed_date =
    pijul_identity::parse_expiry_date(input).map_err(|_| anyhow!("Invalid date"))?;
  • replacement in pijul/src/commands/identity.rs at line 166
    [2.46167][10.2848:2881]()
    Repair { no_keyring: bool },
    [2.46167]
    [2.46295]
    Repair {
    #[clap(long = "no-keyring")]
    no_keyring: bool,
    },
  • edit in pijul/src/commands/identity.rs at line 178
    [10.2932]
    [10.2932]
    #[clap(long = "no-keyring")]
  • edit in pijul/src/commands/identity.rs at line 220
    [9.2585]
    [2.47472]
    no_prompt: bool,
    use_keyring: bool,
  • edit in pijul/src/commands/identity.rs at line 224
    [2.47531]
    [2.47531]
    let mut public_key = default.public_key.clone();
    if let Some(expiry) = expiry.as_ref() {
    public_key.expires = Some(expiry.clone());
    }
  • replacement in pijul/src/commands/identity.rs at line 230
    [2.47558][2.47558:47572](),[2.47572][11.13491:13545](),[8.2865][6.5338:5383](),[11.13545][6.5338:5383](),[2.47648][6.5338:5383](),[6.5383][2.47700:47816](),[2.47700][2.47700:47816]()
    Some(
    pijul_interaction::Password::new(config)?
    .with_prompt("New password")
    .with_confirmation("Confirm password", "Password mismatch")
    .interact()?,
    )
    [2.47558]
    [2.47816]
    Some(read_password(config, no_prompt)?)
  • replacement in pijul/src/commands/identity.rs at line 236
    [7.2256][7.2256:2286]()
    key.expires = expiry;
    [7.2256]
    [7.2286]
    if let Some(password) = pw.as_deref() {
    let (secret_key, _) = default.decrypt(config, use_keyring)?;
    key = secret_key.save(Some(password));
    }
    if let Some(expiry) = expiry.as_ref() {
    key.expires = Some(expiry.clone());
    }
  • replacement in pijul/src/commands/identity.rs at line 260
    [2.48446][7.2370:2398]()
    default.public_key,
    [2.48446]
    [7.2398]
    public_key,
  • edit in pijul/src/commands/identity.rs at line 263
    [4.17594]
    [2.48480]
    }
    fn read_password(config: &pijul_config::Config, no_prompt: bool) -> Result<String, anyhow::Error> {
    if no_prompt {
    use std::io::{BufRead, Write};
    fn read_line(prompt: &str) -> Result<String, anyhow::Error> {
    let mut stderr = std::io::stderr();
    writeln!(stderr, "{prompt}")?;
    stderr.flush()?;
    let mut input = String::new();
    let read = std::io::stdin().lock().read_line(&mut input)?;
    if read == 0 {
    anyhow::bail!("Unexpected end of standard input")
    }
    while matches!(input.chars().last(), Some('\n' | '\r')) {
    input.pop();
    }
    Ok(input)
    }
    loop {
    let password = read_line("New password")?;
    let confirmation = read_line("Confirm password")?;
    if password == confirmation {
    return Ok(password);
    }
    let mut stderr = std::io::stderr();
    writeln!(stderr, "Password mismatch")?;
    stderr.flush()?;
    }
    } else {
    Ok(pijul_interaction::Password::new(config)?
    .with_prompt("New password")
    .with_confirmation("Confirm password", "Password mismatch")
    .interact()?)
    }
  • edit in pijul/src/commands/identity.rs at line 323
    [5.413]
    [5.413]
    options.no_prompt,
    !options.no_keyring,
  • edit in pijul/src/commands/identity.rs at line 332
    [12.1351][12.1351:1410]()
    link_remote: !options.no_link,
  • edit in pijul/src/commands/identity.rs at line 333
    [12.1472]
    [12.1472]
    link_remote: !options.no_link,
  • edit in pijul/src/commands/identity.rs at line 503
    [5.783]
    [5.783]
    options.no_prompt,
    !options.no_keyring,