envstash 0.1.12

Manage .env files across git branches with versioning, diffing, and optional encryption
use std::path::Path;

use chrono::{SecondsFormat, Utc};

use colored::Colorize;

use crate::cli;
use crate::error::{Error, Result};
use crate::parser;
use crate::store::queries::{self, SaveInput};
use crate::util::fs as util_fs;

/// Run the `save` command: read .env from disk, parse, and store.
pub fn run(
    cwd: &Path,
    file: Option<&str>,
    key_file: Option<&str>,
    message: Option<&str>,
) -> Result<()> {
    let mut conn = cli::require_store()?;
    let aes_key = cli::load_encryption_key(&conn, key_file)?;
    let (project_path, git_ctx) = cli::resolve_project(cwd)?;

    let file_name = file.unwrap_or(".env");
    let disk_path = cwd.join(file_name);

    if !disk_path.exists() {
        return Err(Error::FileNotFound(disk_path));
    }

    // Refuse to read through a symlink, which can bypass validate_target_path
    // and exfiltrate arbitrary files.
    util_fs::refuse_symlink(&disk_path, "save")?;

    let content = std::fs::read_to_string(&disk_path)?;
    let entries = parser::parse(&content)?;
    let hash = parser::content_hash(&entries);
    let file_path = cli::resolve_file_path(file_name, cwd, &git_ctx)?;

    let (branch, commit) = match &git_ctx {
        Some(ctx) => (ctx.branch.as_str(), ctx.commit.as_str()),
        None => ("", ""),
    };

    let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);

    queries::insert_save_input(
        &mut conn,
        &SaveInput {
            project_path: &project_path,
            file_path: &file_path,
            branch,
            commit_hash: commit,
            timestamp: &timestamp,
            content_hash: &hash,
            entries: &entries,
            aes_key: aes_key.as_deref(),
            message,
        },
    )?;

    let msg_suffix = match message {
        Some(m) => format!(" {}", format!("-- {m}").dimmed()),
        None => String::new(),
    };

    println!(
        "{} {} ({} variables){}",
        "Saved".green().bold(),
        file_path,
        entries.len(),
        msg_suffix
    );
    Ok(())
}