Skip to content

Commit bb07e36

Browse files
committed
Add queue page
- Add queue table - Add queue statistics - Redirect homepage to queue page - Replace original homepage with /help page
1 parent 4bc464f commit bb07e36

14 files changed

+444
-172
lines changed

.sqlx/query-4e0aaa8eea9ccadab0fc5514558a7a72f05ef3ba1644f43ae5e5d54bd64ca533.json

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/database/client.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ use super::operations::{
1212
approve_pull_request, create_build, create_pull_request, create_workflow,
1313
delegate_pull_request, find_build, find_pr_by_build, get_nonclosed_pull_requests,
1414
get_nonclosed_pull_requests_by_base_branch, get_prs_with_unknown_mergeable_state,
15-
get_pull_request, get_repository, get_running_builds, get_workflow_urls_for_build,
16-
get_workflows_for_build, insert_repo_if_not_exists, set_pr_priority, set_pr_rollup,
17-
set_pr_status, unapprove_pull_request, undelegate_pull_request, update_build_status,
18-
update_mergeable_states_by_base_branch, update_pr_build_id, update_pr_mergeable_state,
19-
update_workflow_status, upsert_pull_request, upsert_repository,
15+
get_pull_request, get_repository, get_repository_by_name, get_running_builds,
16+
get_workflow_urls_for_build, get_workflows_for_build, insert_repo_if_not_exists,
17+
set_pr_priority, set_pr_rollup, set_pr_status, unapprove_pull_request, undelegate_pull_request,
18+
update_build_status, update_mergeable_states_by_base_branch, update_pr_build_id,
19+
update_pr_mergeable_state, update_workflow_status, upsert_pull_request, upsert_repository,
2020
};
2121
use super::{ApprovalInfo, DelegatedPermission, MergeableState, RunId};
2222

@@ -272,6 +272,10 @@ impl PgDbClient {
272272
insert_repo_if_not_exists(&self.pool, repo, tree_state).await
273273
}
274274

275+
pub async fn repo_by_name(&self, repo_name: &str) -> anyhow::Result<Option<RepoModel>> {
276+
get_repository_by_name(&self.pool, repo_name).await
277+
}
278+
275279
pub async fn upsert_repository(
276280
&self,
277281
repo: &GithubRepoName,

src/database/mod.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,18 @@ pub enum BuildStatus {
251251
Timeouted,
252252
}
253253

254+
impl Display for BuildStatus {
255+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256+
match self {
257+
BuildStatus::Pending => write!(f, "pending"),
258+
BuildStatus::Success => write!(f, "success"),
259+
BuildStatus::Failure => write!(f, "failure"),
260+
BuildStatus::Cancelled => write!(f, "cancelled"),
261+
BuildStatus::Timeouted => write!(f, "timeouted"),
262+
}
263+
}
264+
}
265+
254266
/// Represents a single (merged) commit.
255267
#[derive(Debug, sqlx::Type)]
256268
#[sqlx(type_name = "build")]
@@ -354,6 +366,20 @@ impl TreeState {
354366
pub fn is_closed(&self) -> bool {
355367
matches!(self, TreeState::Closed { .. })
356368
}
369+
370+
pub fn priority(&self) -> Option<u32> {
371+
match self {
372+
TreeState::Closed { priority, .. } => Some(*priority),
373+
TreeState::Open => None,
374+
}
375+
}
376+
377+
pub fn comment_source(&self) -> Option<&str> {
378+
match self {
379+
TreeState::Closed { source, .. } => Some(source),
380+
TreeState::Open => None,
381+
}
382+
}
357383
}
358384

359385
impl sqlx::Type<sqlx::Postgres> for TreeState {

src/database/operations.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,38 @@ pub(crate) async fn insert_repo_if_not_exists(
817817
.await
818818
}
819819

820+
/// Returns the first match found for a repository by name without owner (via `/{repo_name}`).
821+
pub(crate) async fn get_repository_by_name(
822+
executor: impl PgExecutor<'_>,
823+
repo_name: &str,
824+
) -> anyhow::Result<Option<RepoModel>> {
825+
measure_db_query("get_repository_by_name", || async {
826+
let search_pattern = format!("%/{repo_name}");
827+
let repo = sqlx::query_as!(
828+
RepoModel,
829+
r#"
830+
SELECT
831+
id,
832+
name as "name: GithubRepoName",
833+
(
834+
tree_state,
835+
treeclosed_src
836+
) AS "tree_state!: TreeState",
837+
created_at
838+
FROM repository
839+
WHERE name LIKE $1
840+
LIMIT 1
841+
"#,
842+
search_pattern
843+
)
844+
.fetch_optional(executor)
845+
.await?;
846+
847+
Ok(repo)
848+
})
849+
.await
850+
}
851+
820852
/// Updates the tree state of a repository.
821853
pub(crate) async fn upsert_repository(
822854
executor: impl PgExecutor<'_>,

src/github/error.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use anyhow::Error as AnyhowError;
2+
use axum::http::StatusCode;
3+
use axum::response::{IntoResponse, Response};
4+
5+
pub struct AppError(pub AnyhowError);
6+
7+
impl IntoResponse for AppError {
8+
fn into_response(self) -> Response {
9+
let msg = format!("Something went wrong: {}", self.0);
10+
tracing::error!("{msg}");
11+
(StatusCode::INTERNAL_SERVER_ERROR, msg).into_response()
12+
}
13+
}
14+
15+
impl<E> From<E> for AppError
16+
where
17+
E: Into<AnyhowError>,
18+
{
19+
fn from(err: E) -> Self {
20+
Self(err.into())
21+
}
22+
}

src/github/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ use std::str::FromStr;
77
use url::Url;
88

99
pub mod api;
10+
mod error;
1011
mod labels;
1112
pub mod server;
1213
mod webhook;
1314

1415
pub use api::operations::MergeError;
16+
pub use error::AppError;
1517
pub use labels::{LabelModification, LabelTrigger};
1618
pub use webhook::WebhookSecret;
1719

src/github/server.rs

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@ use crate::bors::mergeable_queue::{
44
handle_mergeable_queue_item,
55
};
66
use crate::bors::{
7-
BorsContext, RepositoryState, handle_bors_global_event, handle_bors_repository_event,
7+
BorsContext, RepositoryState, RollupMode, handle_bors_global_event,
8+
handle_bors_repository_event,
89
};
910
use crate::github::webhook::GitHubWebhook;
1011
use crate::github::webhook::WebhookSecret;
11-
use crate::templates::{HtmlTemplate, IndexTemplate, NotFoundTemplate, RepositoryView};
12+
use crate::templates::{
13+
HelpTemplate, HtmlTemplate, NotFoundTemplate, PullRequestStats, QueueTemplate, RepositoryView,
14+
};
1215
use crate::{BorsGlobalEvent, BorsRepositoryEvent, PgDbClient, TeamApiClient};
1316

17+
use super::AppError;
1418
use anyhow::Error;
1519
use axum::Router;
16-
use axum::extract::State;
20+
use axum::extract::{Path, State};
1721
use axum::http::StatusCode;
18-
use axum::response::{IntoResponse, Response};
22+
use axum::response::{IntoResponse, Redirect, Response};
1923
use axum::routing::{get, post};
2024
use octocrab::Octocrab;
2125
use std::any::Any;
@@ -66,6 +70,8 @@ pub type ServerStateRef = Arc<ServerState>;
6670
pub fn create_app(state: ServerState) -> Router {
6771
Router::new()
6872
.route("/", get(index_handler))
73+
.route("/help", get(help_handler))
74+
.route("/queue/{repo_name}", get(queue_handler))
6975
.route("/github", post(github_webhook_handler))
7076
.route("/health", get(health_handler))
7177
.layer(ConcurrencyLimitLayer::new(100))
@@ -87,7 +93,11 @@ async fn health_handler() -> impl IntoResponse {
8793
(StatusCode::OK, "")
8894
}
8995

90-
async fn index_handler(State(state): State<ServerStateRef>) -> impl IntoResponse {
96+
async fn index_handler() -> impl IntoResponse {
97+
Redirect::permanent("/queue/rust")
98+
}
99+
100+
async fn help_handler(State(state): State<ServerStateRef>) -> impl IntoResponse {
91101
let mut repos = Vec::with_capacity(state.repositories.len());
92102
for repo in state.repositories.keys() {
93103
let treeclosed = state
@@ -103,7 +113,49 @@ async fn index_handler(State(state): State<ServerStateRef>) -> impl IntoResponse
103113
});
104114
}
105115

106-
HtmlTemplate(IndexTemplate { repos })
116+
HtmlTemplate(HelpTemplate { repos })
117+
}
118+
119+
async fn queue_handler(
120+
Path(repo_name): Path<String>,
121+
State(state): State<ServerStateRef>,
122+
) -> Result<impl IntoResponse, AppError> {
123+
let repo = match state.db.repo_by_name(&repo_name).await? {
124+
Some(repo) => repo,
125+
None => {
126+
return Ok((
127+
StatusCode::NOT_FOUND,
128+
format!("Repository {repo_name} not found"),
129+
)
130+
.into_response());
131+
}
132+
};
133+
134+
let prs = state.db.get_nonclosed_pull_requests(&repo.name).await?;
135+
136+
// TODO: add failed count
137+
let (approved_count, rolled_up_count) = prs.iter().fold((0, 0), |(approved, rolled_up), pr| {
138+
let is_approved = if pr.is_approved() { 1 } else { 0 };
139+
let is_rolled_up = if matches!(pr.rollup, Some(RollupMode::Always)) {
140+
1
141+
} else {
142+
0
143+
};
144+
(approved + is_approved, rolled_up + is_rolled_up)
145+
});
146+
147+
Ok(HtmlTemplate(QueueTemplate {
148+
repo_name: repo.name.name().to_string(),
149+
repo_url: format!("https://github.com/{}", repo.name),
150+
tree_state: repo.tree_state,
151+
stats: PullRequestStats {
152+
total_count: prs.len(),
153+
approved_count,
154+
rolled_up_count,
155+
},
156+
prs,
157+
})
158+
.into_response())
107159
}
108160

109161
/// Axum handler that receives a webhook and sends it to a webhook channel.

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ mod utils;
1212
pub use bors::{BorsContext, CommandParser, event::BorsGlobalEvent, event::BorsRepositoryEvent};
1313
pub use database::{PgDbClient, TreeState};
1414
pub use github::{
15-
WebhookSecret,
15+
AppError, WebhookSecret,
1616
api::create_github_client,
1717
api::load_repositories,
1818
server::{BorsProcess, ServerState, create_app, create_bors_process},

src/templates.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::database::{MergeableState::*, PullRequestModel, TreeState};
12
use askama::Template;
23
use axum::response::{Html, IntoResponse, Response};
34
use http::StatusCode;
@@ -21,8 +22,8 @@ where
2122
}
2223

2324
#[derive(Template)]
24-
#[template(path = "index.html")]
25-
pub struct IndexTemplate {
25+
#[template(path = "help.html")]
26+
pub struct HelpTemplate {
2627
pub repos: Vec<RepositoryView>,
2728
}
2829

@@ -31,6 +32,22 @@ pub struct RepositoryView {
3132
pub treeclosed: bool,
3233
}
3334

35+
pub struct PullRequestStats {
36+
pub total_count: usize,
37+
pub approved_count: usize,
38+
pub rolled_up_count: usize,
39+
}
40+
41+
#[derive(Template)]
42+
#[template(path = "queue.html")]
43+
pub struct QueueTemplate {
44+
pub repo_name: String,
45+
pub repo_url: String,
46+
pub stats: PullRequestStats,
47+
pub prs: Vec<PullRequestModel>,
48+
pub tree_state: TreeState,
49+
}
50+
3451
#[derive(Template)]
3552
#[template(path = "not_found.html")]
3653
pub struct NotFoundTemplate {}

templates/base.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>{% block title %}{% endblock %}</title>
7+
{% block head %}{% endblock %}
8+
</head>
9+
<body>
10+
{% block body %}{% endblock %}
11+
</body>
12+
</html>

0 commit comments

Comments
 (0)