ownpg 0.1.0

OwnPG serves PostgreSQL DBA tools to AI clients over the Model Context Protocol, one database and one schema per run
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

fn main() {
    println!("cargo::rerun-if-changed=build.rs");
    println!("cargo::rerun-if-env-changed=SOURCE_DATE_EPOCH");
    println!("cargo::rerun-if-env-changed=OWNPG_BUILD_COMMIT");
    for path in git_state_files() {
        println!("cargo::rerun-if-changed={}", path.display());
    }
    println!("cargo::rustc-env=OWNPG_BUILD_COMMIT={}", commit_hash());
    println!("cargo::rustc-env=OWNPG_BUILD_DATE={}", build_date());
}

fn workspace_root() -> Option<PathBuf> {
    env::var_os("CARGO_MANIFEST_DIR")
        .map(PathBuf::from)
        .and_then(|dir| dir.parent()?.parent().map(Path::to_path_buf))
        .filter(|root| root.join(".git").is_dir())
}

fn git_state_files() -> Vec<PathBuf> {
    let Some(git_dir) = workspace_root().map(|root| root.join(".git")) else {
        return Vec::new();
    };
    let head = git_dir.join("HEAD");
    let mut files = vec![head.clone()];
    if let Ok(content) = fs::read_to_string(&head)
        && let Some(reference) = content.trim().strip_prefix("ref: ")
    {
        let target = git_dir.join(reference);
        if target.is_file() {
            files.push(target);
        }
    }
    files
}

fn git_output(root: &Path, arguments: &[&str]) -> Option<String> {
    Command::new("git")
        .arg("-C")
        .arg(root)
        .args(arguments)
        .output()
        .ok()
        .filter(|output| output.status.success())
        .and_then(|output| String::from_utf8(output.stdout).ok())
        .map(|text| text.trim().to_owned())
        .filter(|text| !text.is_empty())
}

fn packaged_commit() -> Option<String> {
    let manifest_dir = env::var_os("CARGO_MANIFEST_DIR").map(PathBuf::from)?;
    let text = fs::read_to_string(manifest_dir.join(".cargo_vcs_info.json")).ok()?;
    let marker = "\"sha1\"";
    let start = text.find(marker)? + marker.len();
    let rest = text.get(start..)?;
    let value_start = rest.find('"')? + 1;
    let hash: String = rest
        .get(value_start..)?
        .chars()
        .take_while(char::is_ascii_hexdigit)
        .take(12)
        .collect();
    (hash.len() == 12).then_some(hash)
}

fn commit_hash() -> String {
    if let Some(env_commit) = env::var("OWNPG_BUILD_COMMIT")
        .ok()
        .map(|value| value.trim().chars().take(12).collect::<String>())
        .filter(|value| !value.is_empty())
    {
        return env_commit;
    }
    workspace_root()
        .and_then(|root| git_output(&root, &["rev-parse", "--short=12", "HEAD"]))
        .or_else(packaged_commit)
        .unwrap_or_else(|| "unknown".to_owned())
}

fn build_date() -> String {
    env::var("SOURCE_DATE_EPOCH")
        .ok()
        .and_then(|raw| raw.trim().parse::<i64>().ok())
        .or_else(|| {
            workspace_root()
                .and_then(|root| git_output(&root, &["log", "-1", "--format=%ct"]))
                .and_then(|text| text.parse::<i64>().ok())
        })
        .map(|epoch| {
            let (year, month, day) = civil_from_days(epoch.div_euclid(86_400));
            format!("{year:04}-{month:02}-{day:02}")
        })
        .unwrap_or_else(|| "unknown".to_owned())
}

#[expect(
    clippy::integer_division,
    reason = "the civil date formula works in whole days"
)]
fn civil_from_days(days: i64) -> (i64, i64, i64) {
    let shifted = days + 719_468;
    let era = shifted.div_euclid(146_097);
    let day_of_era = shifted.rem_euclid(146_097);
    let year_of_era =
        (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
    let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
    let month_index = (5 * day_of_year + 2) / 153;
    let day = day_of_year - (153 * month_index + 2) / 5 + 1;
    let month = if month_index < 10 {
        month_index + 3
    } else {
        month_index - 9
    };
    let year = year_of_era + era * 400 + i64::from(month <= 2);
    (year, month, day)
}