1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
|
// Back - An extremely simple git issue tracking system. Inspired by tvix's
// panettone
//
// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This file is part of Back.
//
// You should have received a copy of the License along with this program.
// If not, see <https://www.gnu.org/licenses/agpl.txt>.
use crate::{
config::BackConfig,
error::{self, Error},
git_bug::{
dag::issues_from_repository,
issue::{CollapsedIssue, Status},
},
};
use prefix::BackPrefix;
use rocket::{
get,
response::content::{RawCss, RawHtml},
State,
};
mod issue_html;
pub mod prefix;
#[get("/style.css")]
pub fn styles() -> RawCss<String> {
RawCss(include_str!("../../assets/style.css").to_owned())
}
pub fn issue_list_boilerplate(
config: &State<BackConfig>,
wanted_status: Status,
counter_status: Status,
) -> error::Result<RawHtml<String>> {
let repository = &config.repository;
let mut issue_list = issues_from_repository(&repository.to_thread_local())?
.into_iter()
.map(|issue| issue.collapse())
.collect::<Vec<CollapsedIssue>>();
// Sort by date descending.
issue_list.sort_by_key(|issue| unsafe { issue.timestamp.to_unsafe() });
issue_list.reverse();
let issue_list_str = issue_list.into_iter().fold(String::new(), |acc, issue| {
format!("{}{}", acc, {
if issue.status == wanted_status {
let issue_entry = issue.to_list_entry();
issue_entry.0
} else {
String::new()
}
})
});
let counter_status_lower = counter_status.to_string().to_lowercase();
Ok(RawHtml(format!(
r#"
<!DOCTYPE html>
<html lang="en">
<head>
<title>Back</title>
<link href="/style.css" rel="stylesheet" type="text/css">
<meta content="width=device-width,initial-scale=1" name="viewport">
</head>
<body>
<div class="content">
<header>
<h1>{wanted_status} Issues</h1>
</header>
<main>
<div class="issue-links">
<a href="/issues/{counter_status_lower}/">View {counter_status} issues</a>
<a href="{}">Source code</a>
<!--
<form class="issue-search" method="get">
<input name="search" title="Issue search query" type="search">
<input class="sr-only" type="submit" value="Search Issues">
</form>
-->
</div>
<ol class="issue-list">
{issue_list_str}
</ol>
</main>
</div>
</body>
</html>
"#,
config.source_code_repository_url
)))
}
#[get("/issues/open")]
pub fn open(config: &State<BackConfig>) -> error::Result<RawHtml<String>> {
issue_list_boilerplate(config, Status::Open, Status::Closed)
}
#[get("/issues/closed")]
pub fn closed(config: &State<BackConfig>) -> error::Result<RawHtml<String>> {
issue_list_boilerplate(config, Status::Closed, Status::Open)
}
#[get("/issues/feed")]
pub fn feed(config: &State<BackConfig>) -> error::Result<RawHtml<String>> {
use rss::{ChannelBuilder, Item, ItemBuilder};
let items: Vec<Item> = issues_from_repository(&config.repository.to_thread_local())?
.into_iter()
.map(|issue| issue.collapse())
.map(|issue| {
ItemBuilder::default()
.title(issue.title.to_string())
.author(issue.author.to_string())
.description(issue.message.to_string())
.pub_date(issue.timestamp.to_string())
.link(format!("{}/issue/{}", &config.root.to_string(), issue.id))
.build()
})
.collect();
let channel = ChannelBuilder::default()
.title("Issues")
.link(config.root.to_string())
.description(format!("The rss feed for issues on {}.", config.root))
.items(items)
.build();
Ok(RawHtml(channel.to_string()))
}
#[get("/issue/<prefix>")]
pub fn show_issue(
config: &State<BackConfig>,
prefix: Result<BackPrefix, gix::hash::prefix::from_hex::Error>,
) -> error::Result<RawHtml<String>> {
// NOTE(@bpeetz): Explicitly unwrap the `prefix` here (instead of taking the unwrapped value as
// argument), to avoid triggering rockets "errors forward to the next route" feature.
// This ensures, that our error message actually reaches the user. <2024-12-26>
let prefix = prefix?;
let repository = config.repository.to_thread_local();
let all_issues: Vec<CollapsedIssue> = issues_from_repository(&repository)?
.into_iter()
.map(|val| val.collapse())
.collect();
let maybe_issue = all_issues
.iter()
.find(|issue| issue.id.to_string().starts_with(&prefix.to_string()));
match maybe_issue {
Some(issue) => Ok(issue.to_html(config)),
None => Err(Error::IssuesPrefixMissing { prefix }),
}
}
|