pray-cli 1.10.0

Package manager for the language placed before inference — Prayfile CLI (binary: pray)
use crate::revision_backend::{
    record_git_revision, record_hg_revision, record_other_revision, CommandSpec,
    OtherRevisionConfig, RepositoryRevisionConfig,
};
use pray_core::{PrayError, PrayResult};
use serde::Deserialize;
use std::fs;
use std::path::Path;

const REVISION_CONFIG_PATH: &str = "v1/revision.json";

#[derive(Clone, Copy, Debug)]
pub enum RevisionAction {
    Publish,
    Sync,
}

impl RevisionAction {
    fn default_commit_message(self) -> &'static str {
        match self {
            Self::Publish => "pray publish: update distribution state",
            Self::Sync => "pray sync: update distribution state",
        }
    }
}

#[derive(Debug)]
enum RevisionContext {
    Git(RepositoryRevisionConfig),
    Hg(RepositoryRevisionConfig),
    Other(OtherRevisionConfig),
}

#[derive(Debug, Deserialize)]
struct RevisionConfigFile {
    #[serde(default)]
    backend: Option<RevisionBackend>,

    #[serde(default)]
    remote: Option<String>,

    #[serde(default)]
    push: Option<bool>,

    #[serde(default)]
    commit_message: Option<String>,

    #[serde(default)]
    commit_command: Option<CommandSpec>,

    #[serde(default)]
    push_command: Option<CommandSpec>,
}

#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
enum RevisionBackend {
    Git,
    Hg,
    Other,
}

pub fn record_root_revision(root: &Path, action: RevisionAction) -> PrayResult<()> {
    match load_revision_context(root, action)? {
        Some(context) => context.record(root, action),
        None => Ok(()),
    }
}

fn load_revision_context(
    root: &Path,
    action: RevisionAction,
) -> PrayResult<Option<RevisionContext>> {
    let config_path = root.join(REVISION_CONFIG_PATH);
    if config_path.exists() {
        let text = fs::read_to_string(&config_path)?;
        let config: RevisionConfigFile =
            serde_json::from_str(&text).map_err(|error| PrayError::Parse {
                kind: "revision config",
                message: error.to_string(),
            })?;
        return Ok(Some(revision_context_from_config(root, config, action)?));
    }

    Ok(detect_revision_context(root, action))
}

fn revision_context_from_config(
    root: &Path,
    config: RevisionConfigFile,
    action: RevisionAction,
) -> PrayResult<RevisionContext> {
    let backend = config.backend.unwrap_or_else(|| {
        if config.commit_command.is_some() || config.push_command.is_some() {
            RevisionBackend::Other
        } else if root.join(".git").exists() {
            RevisionBackend::Git
        } else if root.join(".hg").exists() {
            RevisionBackend::Hg
        } else {
            RevisionBackend::Git
        }
    });

    match backend {
        RevisionBackend::Git => {
            let remote = normalize_optional_string(config.remote);
            let push = config.push.unwrap_or(remote.is_some());
            Ok(RevisionContext::Git(RepositoryRevisionConfig {
                remote,
                push,
                commit_message: normalize_optional_string(config.commit_message)
                    .or_else(|| Some(action.default_commit_message().to_string())),
            }))
        }
        RevisionBackend::Hg => {
            let remote = normalize_optional_string(config.remote);
            let push = config.push.unwrap_or(remote.is_some());
            Ok(RevisionContext::Hg(RepositoryRevisionConfig {
                remote,
                push,
                commit_message: normalize_optional_string(config.commit_message)
                    .or_else(|| Some(action.default_commit_message().to_string())),
            }))
        }
        RevisionBackend::Other => {
            let commit_command = config
                .commit_command
                .ok_or_else(|| {
                    PrayError::Unsupported(
                        "revision backend 'other' requires commit_command".to_string(),
                    )
                })?
                .validate()?;
            let push_command_present = config.push_command.is_some();
            let push_command = config.push_command.map(CommandSpec::validate).transpose()?;
            Ok(RevisionContext::Other(OtherRevisionConfig {
                commit_command,
                push_command,
                push: config.push.unwrap_or(push_command_present),
            }))
        }
    }
}

fn detect_revision_context(root: &Path, action: RevisionAction) -> Option<RevisionContext> {
    if root.join(".git").exists() {
        return Some(RevisionContext::Git(RepositoryRevisionConfig {
            remote: None,
            push: false,
            commit_message: Some(action.default_commit_message().to_string()),
        }));
    }

    if root.join(".hg").exists() {
        return Some(RevisionContext::Hg(RepositoryRevisionConfig {
            remote: None,
            push: false,
            commit_message: Some(action.default_commit_message().to_string()),
        }));
    }

    None
}

fn normalize_optional_string(value: Option<String>) -> Option<String> {
    value.and_then(|entry| {
        let trimmed = entry.trim();
        if trimmed.is_empty() {
            None
        } else {
            Some(trimmed.to_string())
        }
    })
}

impl RevisionContext {
    fn record(self, root: &Path, action: RevisionAction) -> PrayResult<()> {
        match self {
            Self::Git(config) => {
                let commit_message = config
                    .commit_message
                    .clone()
                    .unwrap_or_else(|| action.default_commit_message().to_string());
                record_git_revision(root, config, commit_message)
            }
            Self::Hg(config) => {
                let commit_message = config
                    .commit_message
                    .clone()
                    .unwrap_or_else(|| action.default_commit_message().to_string());
                record_hg_revision(root, config, commit_message)
            }
            Self::Other(config) => record_other_revision(root, config),
        }
    }
}