harn-vm 0.9.8

Async bytecode virtual machine for the Harn programming language
Documentation
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use crate::value::VmValue;

use super::*;

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(super) struct GitRemoteSignal {
    pub(super) name: String,
    pub(super) host: String,
    pub(super) slug: Option<String>,
    pub(super) redacted_url: String,
}

impl GitRemoteSignal {
    pub(super) fn into_vm_value(self) -> VmValue {
        let mut out = BTreeMap::new();
        out.insert("name".to_string(), VmValue::string(self.name));
        out.insert("host".to_string(), VmValue::string(self.host));
        out.insert(
            "slug".to_string(),
            self.slug.map(VmValue::string).unwrap_or(VmValue::Nil),
        );
        out.insert("url".to_string(), VmValue::string(self.redacted_url));
        VmValue::dict(out)
    }
}

pub(super) fn remote_signal_from_value(value: &VmValue) -> Option<GitRemoteSignal> {
    match value {
        VmValue::String(url) => remote_signal_from_url("origin", url),
        VmValue::Dict(dict) => {
            let name = optional_string_field(dict, "name").unwrap_or_else(|| "origin".to_string());
            let url = optional_string_field(dict, "url").unwrap_or_default();
            let host = optional_string_field(dict, "host")
                .or_else(|| remote_host(&url))
                .map(normalize_remote_host)
                .unwrap_or_default();
            let slug = optional_string_field(dict, "slug")
                .and_then(|slug| normalize_github_slug(&slug))
                .or_else(|| github_slug_from_remote(&url));
            if host.is_empty() && slug.is_none() && url.is_empty() {
                return None;
            }
            Some(GitRemoteSignal {
                name,
                host,
                slug,
                redacted_url: redact_remote_url(&url),
            })
        }
        _ => None,
    }
}

pub(super) fn detect_git_remote(dir: &Path) -> Option<GitRemoteSignal> {
    let git_path = find_git_path(dir)?;
    let mut remotes = Vec::new();
    for config in git_config_paths(&git_path) {
        let Some(text) = read_text_if_exists(config) else {
            continue;
        };
        remotes.extend(parse_git_config_remotes(&text));
    }
    remotes
        .iter()
        .find(|remote| remote.name == "origin")
        .cloned()
        .or_else(|| remotes.into_iter().next())
}

fn find_git_path(dir: &Path) -> Option<PathBuf> {
    let mut cursor = Some(dir);
    while let Some(path) = cursor {
        let git_path = path.join(".git");
        if git_path.exists() {
            return Some(git_path);
        }
        cursor = path.parent();
    }
    None
}

fn git_config_paths(git_path: &Path) -> Vec<PathBuf> {
    if git_path.is_dir() {
        return vec![git_path.join("config")];
    }
    let Some(git_dir) = read_gitdir_file(git_path) else {
        return Vec::new();
    };
    let mut out = vec![git_dir.join("config")];
    if let Some(common_dir) = read_commondir(&git_dir) {
        out.push(common_dir.join("config"));
    }
    out
}

fn read_gitdir_file(git_path: &Path) -> Option<PathBuf> {
    let text = read_text_if_exists(git_path.to_path_buf())?;
    let raw = text.trim().strip_prefix("gitdir:")?.trim();
    let candidate = PathBuf::from(raw);
    if candidate.is_absolute() {
        Some(candidate)
    } else {
        Some(git_path.parent()?.join(candidate))
    }
}

fn read_commondir(git_dir: &Path) -> Option<PathBuf> {
    let raw = read_text_if_exists(git_dir.join("commondir"))?;
    let candidate = PathBuf::from(raw.trim());
    if candidate.is_absolute() {
        Some(candidate)
    } else {
        Some(git_dir.join(candidate))
    }
}

fn parse_git_config_remotes(config: &str) -> Vec<GitRemoteSignal> {
    let mut current_remote: Option<String> = None;
    let mut remotes = Vec::new();
    for line in config.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') && trimmed.ends_with(']') {
            current_remote = parse_remote_section(trimmed);
            continue;
        }
        let Some(name) = current_remote.as_deref() else {
            continue;
        };
        let Some((key, value)) = trimmed.split_once('=') else {
            continue;
        };
        if key.trim() == "url" {
            if let Some(signal) = remote_signal_from_url(name, value.trim()) {
                remotes.push(signal);
            }
        }
    }
    remotes
}

fn parse_remote_section(section: &str) -> Option<String> {
    let inner = section.strip_prefix('[')?.strip_suffix(']')?.trim();
    let rest = inner.strip_prefix("remote")?.trim();
    let quoted = rest.strip_prefix('"')?;
    let (name, _) = quoted.split_once('"')?;
    Some(name.to_string())
}

fn remote_signal_from_url(name: &str, url: &str) -> Option<GitRemoteSignal> {
    let host = remote_host(url).map(normalize_remote_host)?;
    Some(GitRemoteSignal {
        name: name.to_string(),
        slug: github_slug_from_remote(url),
        host,
        redacted_url: redact_remote_url(url),
    })
}

fn remote_host(url: &str) -> Option<String> {
    let trimmed = url.trim();
    if trimmed.is_empty() {
        return None;
    }
    if let Some(rest) = trimmed.split_once("://").map(|(_, rest)| rest) {
        let authority = rest.split('/').next().unwrap_or_default();
        let host_port = authority.rsplit('@').next().unwrap_or(authority);
        let host = host_port.split(':').next().unwrap_or_default();
        return (!host.is_empty()).then(|| host.to_string());
    }
    if let Some((left, _path)) = trimmed.split_once(':') {
        let host = left.rsplit('@').next().unwrap_or(left);
        return (!host.is_empty()).then(|| host.to_string());
    }
    None
}

fn normalize_remote_host(host: String) -> String {
    let normalized = host.trim().trim_end_matches('.').to_ascii_lowercase();
    if normalized == "github" {
        "github.com".to_string()
    } else {
        normalized
    }
}

pub(super) fn github_slug_from_remote(url: &str) -> Option<String> {
    let host = remote_host(url).map(normalize_remote_host)?;
    if host != "github.com" {
        return None;
    }
    let path = if let Some(rest) = url.split_once("://").map(|(_, rest)| rest) {
        rest.split_once('/')
            .map(|(_, path)| path)
            .unwrap_or_default()
            .to_string()
    } else {
        url.split_once(':')
            .map(|(_, path)| path)
            .unwrap_or_default()
            .to_string()
    };
    normalize_github_slug(&path)
}

fn normalize_github_slug(value: &str) -> Option<String> {
    let mut path = strip_url_suffix(value.trim())
        .trim_start_matches('/')
        .trim_end_matches('/')
        .to_string();
    if let Some(stripped) = path.strip_suffix(".git") {
        path = stripped.to_string();
    }
    let mut parts = path.split('/').filter(|part| !part.is_empty());
    let owner = parts.next()?;
    let repo = parts.next()?;
    Some(format!("{owner}/{repo}"))
}

pub(super) fn redact_remote_url(url: &str) -> String {
    let sanitized = strip_url_suffix(url.trim()).to_string();
    let Some((scheme, rest)) = sanitized.split_once("://") else {
        return sanitized;
    };
    let Some((userinfo, tail)) = rest.split_once('@') else {
        return sanitized;
    };
    if userinfo.is_empty() || tail.is_empty() {
        return sanitized;
    }
    format!("{scheme}://<redacted>@{tail}")
}

fn strip_url_suffix(value: &str) -> &str {
    let mut sanitized = value;
    for marker in ['?', '#'] {
        if let Some((head, _)) = sanitized.split_once(marker) {
            sanitized = head;
        }
    }
    sanitized
}