harn-vm 0.10.16

Async bytecode virtual machine for the Harn programming language
Documentation
use serde_json::{json, Value as JsonValue};

use crate::value::VmError;

use super::{bool_option, string_list_option, string_option};

pub(super) fn tag_list_argv(options: Option<&crate::value::DictMap>) -> Vec<String> {
    let mut argv = vec!["git".to_string(), "tag".to_string(), "--list".to_string()];
    if let Some(sort) = string_option(options, "sort") {
        argv.push(format!("--sort={sort}"));
    }
    if let Some(pattern) = string_option(options, "pattern") {
        argv.push("--".to_string());
        argv.push(pattern);
    }
    argv
}

pub(super) fn describe_argv(options: Option<&crate::value::DictMap>) -> Vec<String> {
    let mut argv = vec![
        "git".to_string(),
        "describe".to_string(),
        "--long".to_string(),
        "--always".to_string(),
        "--dirty".to_string(),
    ];
    if bool_option(options, "tags").unwrap_or(false) {
        argv.push("--tags".to_string());
    }
    if let Some(pattern) = string_option(options, "match") {
        argv.push("--match".to_string());
        argv.push(pattern);
    }
    if let Some(rev) = string_option(options, "rev") {
        argv.push("--".to_string());
        argv.push(rev);
    }
    argv
}

pub(super) fn ls_remote_argv(
    remote: &str,
    options: Option<&crate::value::DictMap>,
) -> Result<Vec<String>, VmError> {
    let mut argv = vec!["git".to_string(), "ls-remote".to_string()];
    if bool_option(options, "tags").unwrap_or(false) {
        argv.push("--tags".to_string());
    }
    if bool_option(options, "heads").unwrap_or(false) {
        argv.push("--heads".to_string());
    }
    argv.push("--".to_string());
    argv.push(remote.to_string());
    if let Some(refs) = string_list_option(options, "refs", "git.ls_remote")? {
        argv.extend(refs);
    }
    Ok(argv)
}

pub(super) fn parse_tag_list(stdout: &str) -> JsonValue {
    let tags = stdout
        .lines()
        .map(str::trim)
        .filter(|tag| !tag.is_empty())
        .collect::<Vec<_>>();
    json!({"tags": tags})
}

pub(super) fn parse_describe(stdout: &str) -> JsonValue {
    let describe = stdout.trim();
    let (value, dirty) = describe
        .strip_suffix("-dirty")
        .map_or((describe, false), |value| (value, true));
    let mut suffix = value.rsplitn(3, '-');
    let sha_part = suffix.next().unwrap_or("");
    let distance_part = suffix.next().unwrap_or("");
    let tag_part = suffix.next();
    let parsed = tag_part
        .zip(sha_part.strip_prefix('g'))
        .and_then(|(tag, sha)| {
            distance_part
                .parse::<u64>()
                .ok()
                .map(|distance| (tag, sha, distance))
        });
    if let Some((tag, sha, distance)) = parsed {
        return json!({
            "describe": describe,
            "tag": tag,
            "distance": distance,
            "sha": sha,
            "dirty": dirty,
        });
    }
    json!({
        "describe": describe,
        "tag": JsonValue::Null,
        "distance": JsonValue::Null,
        "sha": value,
        "dirty": dirty,
    })
}

pub(super) fn parse_ls_remote(stdout: &str, remote: &str) -> JsonValue {
    let refs = stdout
        .lines()
        .filter_map(|line| {
            let (oid, ref_name) = line.split_once('\t')?;
            let peeled = ref_name.ends_with("^{}");
            Some(json!({
                "oid": oid,
                "ref": ref_name,
                "name": ref_name.strip_suffix("^{}").unwrap_or(ref_name),
                "peeled": peeled,
            }))
        })
        .collect::<Vec<_>>();
    json!({"remote": remote, "refs": refs})
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parsers_preserve_hyphenated_tags_and_peeled_refs() {
        assert_eq!(
            parse_tag_list("v2.0.0\nv1.9.0\n\n"),
            json!({"tags": ["v2.0.0", "v1.9.0"]})
        );
        assert_eq!(
            parse_describe("release-candidate-2-7-gabc123-dirty\n"),
            json!({
                "describe": "release-candidate-2-7-gabc123-dirty",
                "tag": "release-candidate-2",
                "distance": 7,
                "sha": "abc123",
                "dirty": true,
            })
        );
        assert_eq!(
            parse_describe("abc123\n"),
            json!({
                "describe": "abc123",
                "tag": JsonValue::Null,
                "distance": JsonValue::Null,
                "sha": "abc123",
                "dirty": false,
            })
        );
        assert_eq!(
            parse_ls_remote(
                "aaaaaaaa\trefs/tags/v1.0.0\nbbbbbbbb\trefs/tags/v1.0.0^{}\n",
                "origin"
            ),
            json!({
                "remote": "origin",
                "refs": [
                    {"oid": "aaaaaaaa", "ref": "refs/tags/v1.0.0", "name": "refs/tags/v1.0.0", "peeled": false},
                    {"oid": "bbbbbbbb", "ref": "refs/tags/v1.0.0^{}", "name": "refs/tags/v1.0.0", "peeled": true},
                ],
            })
        );
    }

    #[test]
    fn argv_uses_option_boundaries() {
        let tag_options = crate::stdlib::json_to_vm_value(&json!({
            "pattern": "--contains=surprise",
            "sort": "-v:refname",
        }));
        assert_eq!(
            tag_list_argv(tag_options.as_dict()),
            [
                "git",
                "tag",
                "--list",
                "--sort=-v:refname",
                "--",
                "--contains=surprise"
            ]
        );

        let describe_options = crate::stdlib::json_to_vm_value(&json!({
            "tags": true,
            "match": "v*",
            "rev": "--all",
        }));
        assert_eq!(
            describe_argv(describe_options.as_dict()),
            [
                "git", "describe", "--long", "--always", "--dirty", "--tags", "--match", "v*",
                "--", "--all"
            ]
        );

        let remote_options = crate::stdlib::json_to_vm_value(&json!({
            "tags": true,
            "refs": ["--upload-pack=surprise"],
        }));
        assert_eq!(
            ls_remote_argv("--exec=surprise", remote_options.as_dict()).expect("valid refs"),
            [
                "git",
                "ls-remote",
                "--tags",
                "--",
                "--exec=surprise",
                "--upload-pack=surprise"
            ]
        );
    }
}