OXYGENHUB
# 19c71c828f86 oxygenhub / crates / hub / src / web / history.rs
4.7 KiB · Rust · last changed Milestone 0.1: browse 1 month ago source blame history raw
1
//! History, change and compare pages.
2
3
use super::{filters, lookup, Error, Page, RepoShell, Shell};
4
use crate::render::{self, diff, graph};
5
use crate::repos::view;
6
use crate::Shared;
7
use askama::Template;
8
use axum::extract::{Path, Query, State};
9
use oxy_core::repo::FileDiff;
10
use serde::Deserialize;
11
use std::collections::BTreeMap;
12
13
const PAGE: usize = 50;
14
15
#[derive(Deserialize)]
16
pub struct HistoryQuery {
17
    path: Option<String>,
18
    offset: Option<usize>,
19
}
20
21
#[derive(Template)]
22
#[template(path = "history.html")]
23
pub struct HistoryPage {
24
    shell: Shell,
25
    repo: RepoShell,
26
    h: view::History,
27
    rows: Vec<graph::Row>,
28
    lanes: usize,
29
}
30
31
pub async fn history(
32
    State(app): State<Shared>,
33
    Path((owner, name, r)): Path<(String, String, String)>,
34
    Query(q): Query<HistoryQuery>,
35
) -> Result<Page<HistoryPage>, Error> {
36
    let shell = Shell::new(&app, &format!("/{owner}/{name}/changes/{r}"));
37
    let (repo, row) = lookup(&app, &shell, &owner, &name).await?;
38
    let spec = r.clone();
39
    let path = q.path.clone().filter(|p| !p.is_empty());
40
    let offset = q.offset.unwrap_or(0);
41
    let h = app
42
        .repos
43
        .read(&repo, move |repo| {
44
            let target = view::resolve(repo, &spec)?;
45
            view::history(repo, target, path.as_deref(), offset, PAGE)
46
        })
47
        .await
48
        .map_err(|e| Error::from_repo(shell.clone(), e))?;
49
    let graph_input: Vec<(String, Vec<String>)> = h.entries.iter().map(|c| (c.hash.clone(), c.deps.clone())).collect();
50
    let rows = graph::layout(&graph_input);
51
    let lanes = graph::width(&rows);
52
    Ok(Page(HistoryPage { shell, repo: RepoShell::new(&app, &row, "changes"), h, rows, lanes }))
53
}
54
55
/// A file's diff, rendered.
56
pub struct FileRows {
57
    pub path: String,
58
    pub from: Option<String>,
59
    pub kind: String,
60
    pub lines: Vec<diff::Line>,
61
    pub added: usize,
62
    pub removed: usize,
63
    pub anchor: String,
64
}
65
66
pub fn render_files(files: &[FileDiff], bases: &BTreeMap<String, Vec<u8>>) -> Vec<FileRows> {
67
    files
68
        .iter()
69
        .enumerate()
70
        .map(|(i, f)| {
71
            let base = bases.get(&f.path).map(Vec::as_slice).unwrap_or(b"");
72
            let lines = diff::unified(base, &f.hunks, 3);
73
            FileRows {
74
                path: f.path.clone(),
75
                from: f.from.clone(),
76
                kind: f.kind.clone(),
77
                added: lines.iter().filter(|l| l.kind == diff::Kind::Add).count(),
78
                removed: lines.iter().filter(|l| l.kind == diff::Kind::Del).count(),
79
                lines,
80
                anchor: format!("diff-{i}"),
81
            }
82
        })
83
        .collect()
84
}
85
86
#[derive(Template)]
87
#[template(path = "change.html")]
88
pub struct ChangePage {
89
    shell: Shell,
90
    repo: RepoShell,
91
    c: view::ChangeDetail,
92
    files: Vec<FileRows>,
93
    body_html: String,
94
}
95
96
pub async fn change(
97
    State(app): State<Shared>,
98
    Path((owner, name, hash)): Path<(String, String, String)>,
99
) -> Result<Page<ChangePage>, Error> {
100
    let shell = Shell::new(&app, &format!("/{owner}/{name}/change/{hash}"));
101
    let (repo, row) = lookup(&app, &shell, &owner, &name).await?;
102
    let spec = hash.clone();
103
    let c = app
104
        .repos
105
        .read(&repo, move |repo| {
106
            let h = view::resolve_change(repo, &spec)?;
107
            view::change(repo, &h)
108
        })
109
        .await
110
        .map_err(|e| Error::from_repo(shell.clone(), e))?;
111
    let files = render_files(&c.files, &c.bases);
112
    let body_html =
113
        if c.view.body.is_empty() { String::new() } else { format!("<pre class=\"msg\">{}</pre>", render::escape(&c.view.body)) };
114
    Ok(Page(ChangePage { shell, repo: RepoShell::new(&app, &row, "changes"), c, files, body_html }))
115
}
116
117
#[derive(Template)]
118
#[template(path = "compare.html")]
119
pub struct ComparePage {
120
    shell: Shell,
121
    repo: RepoShell,
122
    cmp: view::Compare,
123
    files: Vec<FileRows>,
124
}
125
126
/// `/compare/{base}...{head}`.
127
pub async fn compare(
128
    State(app): State<Shared>,
129
    Path((owner, name, spec)): Path<(String, String, String)>,
130
) -> Result<Page<ComparePage>, Error> {
131
    let shell = Shell::new(&app, &format!("/{owner}/{name}/compare/{spec}"));
132
    let (repo, row) = lookup(&app, &shell, &owner, &name).await?;
133
    let Some((a, b)) = spec.split_once("...") else {
134
        return Err(Error::bad(shell, "compare takes base...head"));
135
    };
136
    let (a, b) = (a.to_string(), b.to_string());
137
    let cmp = app
138
        .repos
139
        .read(&repo, move |repo| {
140
            let base = view::resolve(repo, &a)?;
141
            let head = view::resolve(repo, &b)?;
142
            view::compare(repo, base, head)
143
        })
144
        .await
145
        .map_err(|e| Error::from_repo(shell.clone(), e))?;
146
    let files = render_files(&cmp.files, &cmp.bases);
147
    Ok(Page(ComparePage { shell, repo: RepoShell::new(&app, &row, "changes"), cmp, files }))
148
}