diff options
Diffstat (limited to 'src/web/mod.rs')
-rw-r--r-- | src/web/mod.rs | 125 |
1 files changed, 87 insertions, 38 deletions
diff --git a/src/web/mod.rs b/src/web/mod.rs index 8e2e9b0..0f9835a 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -9,21 +9,28 @@ // You should have received a copy of the License along with this program. // If not, see <https://www.gnu.org/licenses/agpl.txt>. -use std::{convert::Infallible, net::SocketAddr, path::PathBuf, sync::Arc}; +use std::{cell::OnceCell, convert::Infallible, net::SocketAddr, path::PathBuf, sync::Arc}; use bytes::Bytes; +use git_bug::{ + entities::issue::{Issue, data::status::Status, query::MatchKeyValue}, + query::{Matcher, ParseMode, Query}, + replica::entity::{id::prefix::IdPrefix, snapshot::Snapshot}, +}; use http_body_util::combinators::BoxBody; use hyper::{Method, Request, Response, StatusCode, server::conn::http1, service::service_fn}; use hyper_util::rt::TokioIo; -use log::{error, info}; -use responses::{html_response, html_response_status, html_response_status_content_type}; +use log::{error, info, warn}; +use responses::{html_response, html_response_status, html_response_status_content_type, redirect}; use tokio::net::TcpListener; +use url::Url; -use crate::{config::BackConfig, error, git_bug::issue::Status}; +use crate::{config::BackConfig, error}; mod generate; mod responses; +#[allow(clippy::unused_async, clippy::too_many_lines)] async fn match_uri( config: Arc<BackConfig>, req: Request<hyper::body::Incoming>, @@ -36,36 +43,69 @@ async fn match_uri( } let output = || -> Result<Response<BoxBody<Bytes, Infallible>>, error::Error> { - match req.uri().path().trim_end_matches("/") { + match req.uri().path().trim_matches('/') { "" => Ok(html_response(generate::repos(&config)?)), - "/style.css" => Ok(responses::html_response_status_content_type( + "style.css" => Ok(html_response_status_content_type( include_str!("../../assets/style.css"), StatusCode::OK, "text/css", )), + "search.png" => Ok(html_response_status_content_type( + &include_bytes!("../../assets/search.png")[..], + StatusCode::OK, + "image/png", + )), - path if path.ends_with("/issues/open") => { - let repo_path = PathBuf::from( - path.strip_suffix("/issues/open") - .expect("This suffix exists") - .strip_prefix("/") - .expect("This also exists"), - ); - - let issues = generate::issues(&config, Status::Open, Status::Closed, &repo_path)?; - Ok(html_response(issues)) - } - path if path.ends_with("/issues/closed") => { - let repo_path = PathBuf::from( - path.strip_suffix("/issues/closed") - .expect("This suffix exists") - .strip_prefix("/") - .expect("This also exists"), - ); + path if path.ends_with("/issues") => { + let repo_path = + PathBuf::from(path.strip_suffix("/issues").expect("This suffix exists")); + + let replica = config + .repositories()? + .get(&repo_path)? + .open(&config.scan_path)?; + + let query = { + // HACK(@bpeetz): We cannot use the uri directly, because that would require us + // to parse the query string. As such, we need to turn into into a url::Url, + // which provides this. Unfortunately, this re-parsing is the “officially” + // sanctioned way of doing this. <2025-05-28> + let url: Url = format!("https://{}{}", config.root_url, req.uri()) + .parse() + .expect("Was a url before"); + + let mut query = OnceCell::new(); + for (name, value) in url.query_pairs() { + if name == "query" && query.get().is_none() { + let val = + Query::from_continuous_str(&replica, &value, ParseMode::Relaxed) + .map_err(|err| error::Error::InvalidQuery { + err, + query: value.to_string(), + })?; + query.set(val).expect("We checked for initialized"); + } else { + warn!("Unknown url query key: {name}"); + } + } + + query.take() + }; - let issues = generate::issues(&config, Status::Closed, Status::Open, &repo_path)?; - Ok(html_response(issues)) + if let Some(query) = query { + let issues = generate::issues(&config, &repo_path, &replica, &query)?; + Ok(html_response(issues)) + } else { + let query = Query::<Snapshot<Issue>>::from_matcher(Matcher::Match { + key_value: MatchKeyValue::Status(Status::Open), + }); + Ok(redirect(&format!( + "/{}/issues/?query={}", + repo_path.display(), + query + ))) + } } path if path.ends_with("/issues/feed") => { let repo_path = PathBuf::from( @@ -87,22 +127,30 @@ async fn match_uri( let (repo_path, prefix) = { let split: Vec<&str> = path.split("/issue/").collect(); - let prefix = - gix::hash::Prefix::from_hex(split[1]).map_err(error::Error::from)?; + let prefix = IdPrefix::from_hex_bytes(split[1].as_bytes()).map_err(|err| { + error::Error::IssuesPrefixParse { + prefix: split[1].to_owned(), + error: err, + } + })?; - let repo_path = - PathBuf::from(split[0].strip_prefix("/").expect("This prefix exists")); + let repo_path = PathBuf::from(split[0]); (repo_path, prefix) }; Ok(html_response(generate::issue(&config, &repo_path, prefix)?)) } - other => Ok(responses::html_response_status_content_type( - format!("'{}' not found", other), - StatusCode::NOT_FOUND, - "text/plain", - )), + other if config.repositories()?.get(&PathBuf::from(other)).is_ok() => { + Ok(redirect(&format!("/{other}/issues/?query=status:open"))) + } + other if config.repositories()?.get(&PathBuf::from(other)).is_err() => { + Err(error::Error::NotGitBug { + path: PathBuf::from(other), + }) + } + + other => Err(error::Error::NotFound(PathBuf::from(other))), } }; match output() { @@ -111,13 +159,14 @@ async fn match_uri( } } -pub async fn main(config: Arc<BackConfig>) -> Result<(), error::Error> { +pub(crate) async fn main(config: Arc<BackConfig>) -> Result<(), error::Error> { let addr: SocketAddr = ([127, 0, 0, 1], 8000).into(); let listener = TcpListener::bind(addr) .await .map_err(|err| error::Error::TcpBind { addr, err })?; - info!("Listening on http://{}", addr); + info!("Listening on http://{addr}"); + loop { let (stream, _) = listener .accept() @@ -131,7 +180,7 @@ pub async fn main(config: Arc<BackConfig>) -> Result<(), error::Error> { tokio::task::spawn(async move { if let Err(err) = http1::Builder::new().serve_connection(io, service).await { - error!("Error serving connection: {:?}", err); + error!("Error serving connection: {err:?}"); } }); } |