pijul 1.0.0-beta.20

A distributed version control system.
use super::*;
use crate::commands::common_opts::RepoAndChannel;
use anyhow::bail;
use clap::Parser;
use pijul_core::{Base32, ChannelTxnT, Merkle, TxnT, pristine::TxnErr};
use pijul_remote as remote;
use std::path::Path;

#[derive(Parser, Debug)]
pub struct Approve {
    #[clap(flatten)]
    base: RepoAndChannel,
    /// The state to approve, or an unambiguous prefix thereof. Defaults to the
    /// latest state of the channel.
    #[clap(value_name = "STATE")]
    state: Option<String>,
    /// The remote (Nest) to send the approval to. Defaults to the configured
    /// default remote.
    #[clap(long = "to")]
    to: Option<String>,
    /// Identity to sign with (defaults to the configured identity)
    #[clap(long = "identity")]
    identity: Option<String>,
}

impl Approve {
    pub fn repository_path(&mut self) -> Option<&Path> {
        self.base.repo_path()
    }

    pub async fn run(mut self, config: &pijul_config::Config) -> Result<(), anyhow::Error> {
        let repo = self.base.find_root()?;

        let identity_name = self
            .identity
            .take()
            .unwrap_or(pijul_identity::choose_identity_name(config).await?);
        let complete = pijul_identity::Complete::load(&identity_name)?;
        let secret = complete.skey();

        // Compute the state to approve and its channel, then release the txn
        // before touching the network.
        let (state, channel_name) = {
            let txn = repo.pristine.txn_begin()?;
            let channel_name = get_channel(self.base.channel(), &txn).0.to_string();
            let (channel_ref, _) = load_channel(self.base.channel(), &txn)?;
            let Some((state, _)) = get_state(&txn, &*channel_ref.read(), self.state.as_deref())?
            else {
                bail!("No such state");
            };
            (state, channel_name)
        };

        // Sign the state Merkle: this pins the exact applied set, so no change
        // can be added or removed without invalidating the approval.
        let sig = pijul_identity::sign_sshsig(&secret, &state.to_bytes()).await?;

        let remote_name = if let Some(ref to) = self.to {
            to.clone()
        } else if let Some(ref def) = config.default_remote {
            def.clone()
        } else {
            bail!("Missing remote: pass --to or set a default remote");
        };

        let mut remote = remote::repository(
            config,
            Some(&repo.path),
            Some(&complete.config.author.username),
            &remote_name,
            &channel_name,
            false,
            true,
        )
        .await?;

        remote.approve(&channel_name, state, &sig).await?;

        println!("Approved {}", state.to_base32());
        Ok(())
    }
}

fn get_state<T: ChannelTxnT + TxnT>(
    txn: &T,
    channel: &T::Channel,
    state: Option<&str>,
) -> Result<Option<(Merkle, u64)>, TxnErr<T::GraphError>> {
    if let Some(state) = state {
        if let Ok((st, n)) = txn.state_from_prefix(&txn.states(channel), &state) {
            return Ok(Some((st.into(), n.into())));
        }
    } else if let Some(x) = txn
        .rev_cursor_revchangeset(txn.rev_changes(&channel), None)?
        .next()
    {
        let (n, p) = x?;
        return Ok(Some((p.b.into(), (*n).into())));
    }

    Ok(None)
}