forkctl 0.0.8

Control audited StGit downstream patch stacks
use anyhow::{Context, Result};
use askama::Template;

pub struct RebaseReport {
    pub target: String,
    pub old_base: String,
    pub old_tip: String,
    pub new_base: String,
    pub new_tip: String,
    pub recovery_tag: String,
    pub exports: Vec<ExportEvidence>,
    pub range_diff: String,
}

pub struct ExportEvidence {
    pub path: String,
    pub hash: String,
}

#[derive(Template)]
#[template(path = "rebase-report.md", escape = "none")]
struct RebaseReportTemplate {
    target: String,
    old_base: String,
    old_tip: String,
    new_base: String,
    new_tip: String,
    recovery_tag: String,
    exports: Vec<ExportRow>,
    range_diff: String,
}

struct ExportRow {
    path: String,
    hash: String,
}

pub fn render(report: RebaseReport) -> Result<String> {
    let mut output = RebaseReportTemplate {
        target: escape_inline(&report.target),
        old_base: escape_inline(&report.old_base),
        old_tip: escape_inline(&report.old_tip),
        new_base: escape_inline(&report.new_base),
        new_tip: escape_inline(&report.new_tip),
        recovery_tag: escape_inline(&report.recovery_tag),
        exports: report
            .exports
            .into_iter()
            .map(|export| ExportRow {
                path: escape_inline(&export.path),
                hash: escape_inline(&export.hash),
            })
            .collect(),
        range_diff: report.range_diff,
    }
    .render()
    .context("render rebase report")?;
    if !output.ends_with('\n') {
        output.push('\n');
    }
    Ok(output)
}

fn escape_inline(value: &str) -> String {
    value.replace('\\', "\\\\").replace('`', "\\`")
}

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

    #[test]
    fn renders_no_exports_and_range_diff() {
        let output = render(RebaseReport {
            target: "refs/tags/v1".into(),
            old_base: "a".repeat(40),
            old_tip: "b".repeat(40),
            new_base: "c".repeat(40),
            new_tip: "d".repeat(40),
            recovery_tag: "vsh/pre-sync".into(),
            exports: Vec::new(),
            range_diff: "1: old\n2: new".into(),
        })
        .unwrap();
        assert!(output.contains("- None"));
        assert!(output.contains("1: old\n2: new\n"));
        assert!(output.ends_with('\n'));
    }
}