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