elvsc

19 commits
Updated 2026-04-30 16:50:05
src/commands/update
src/commands/update/hub.rs
use crate::{ElvscArgs, RemoteOrLocal, check_output};

use std::path::Path;

use askama::Template;

use jj_cli::{
    cli_util::CommandHelper,
    command_error::{CommandError, internal_error_with_message, user_error_with_message},
    ui::Ui,
};

#[rustfmt::skip]
const ASSETS: &[(&str, &[u8])] = &[
    ("style.css",               include_bytes!("../../../assets/style.css")),
    ("ZedMono.woff2",           include_bytes!("../../../assets/ZedMono.woff2")),
    ("ZedSansBold.woff2",       include_bytes!("../../../assets/ZedSansBold.woff2")),
    ("ZedSansItalic.woff2",     include_bytes!("../../../assets/ZedSansItalic.woff2")),
    ("ZedSansRegular.woff2",    include_bytes!("../../../assets/ZedSansRegular.woff2")),
];

pub(super) fn write_assets(output: &Path) -> Result<(), CommandError> {
    for (name, bytes) in ASSETS {
        std::fs::write(output.join(name), bytes)?;
    }
    Ok(())
}

#[derive(Debug, Clone, serde::Deserialize)]
pub struct RepositorySummary {
    pub name: String,
    pub commits: usize,
    pub latest: String,
    pub description: String,
}

const NUSHELL_GET_SUMMARIES: &str = r#"
| where { |it| $it.type == dir }
| insert commits { |it| (try { git --git-dir $it.name rev-list --count trunk } catch { try { jj -R $it.name log -r 'ancestors(remote_bookmarks(trunk))' --count } catch { 0 } } | into int) + 1 }
| insert description { |it| try { git --git-dir $it.name show trunk:DESCRIPTION } catch { try { jj -R $it.name file show -r 'remote_bookmarks(trunk)' DESCRIPTION } catch { '<no description>' } } }
| insert latest { |it| try { git --git-dir $it.name log -1 --format=%cd trunk | format date '%Y-%m-%d %H:%M' } catch { '<no date>' } }
| update name { |it| $it.name | path basename }
| select name latest commits description
| to json
"#;

pub fn get_hub_summaries(hub: &RemoteOrLocal<'_>) -> Result<Vec<RepositorySummary>, CommandError> {
    let path = hub.path().display().to_string();
    let script = format!("ls {path} {NUSHELL_GET_SUMMARIES}");

    let output = check_output(hub.shell_command(&script).output()?)?;

    serde_json::from_slice(&output.stdout)
        .map_err(|e| user_error_with_message("Couldn't parse hub summaries json", e))
}

pub async fn run(ui: &mut Ui, ch: &CommandHelper) -> Result<(), CommandError> {
    let args = ElvscArgs::from_config(ch)?;
    let tmp = tempfile::tempdir()?;
    let repositories = get_hub_summaries(&(&args.hub).into())?;

    write_frontend(tmp.path(), &args.public, &repositories)?;
    write_assets(tmp.path())?;

    super::rsync(ui, tmp.path(), &args.output, None)
}

#[derive(askama::Template)]
#[template(path = "hub.html")]
struct HubTemplate<'a> {
    public: &'a str,
    repositories: &'a [RepositorySummary],
}

pub fn write_frontend(
    output: &Path,
    public: &str,
    repositories: &[RepositorySummary],
) -> Result<(), CommandError> {
    let html = HubTemplate {
        public,
        repositories,
    }
    .render()
    .map_err(|e| internal_error_with_message("Couldn't render html", e))?;

    std::fs::write(output.join("index.html"), html)?;
    Ok(())
}