text/x-rust
•
8.08 KB
•
301 lines
use std::sync::Arc;
use askama::Template;
use axum::{
Form,
body::Body,
extract::State,
http::Response,
response::{Html, IntoResponse, Redirect},
};
use rubhub_auth_store::User;
use serde::Deserialize;
use tokio::sync::mpsc;
use tower_cookies::Cookies;
use crate::{
AccessType, GlobalState, Project, UserModel,
extractors::PathUserProject,
models::{CiJobRequest, CiWorkflow, ContentPage},
services::session,
views::ThemedRender,
};
const WORKFLOW_DIR: &str = ".rubhub/workflows";
#[derive(Template)]
#[template(path = "project/ci/new.html")]
struct CiNewTemplate<'a> {
owner: Arc<User>,
project: &'a Project,
access_level: AccessType,
branches: Vec<String>,
default_branch: &'a str,
workflow_content: String,
message: Option<&'a str>,
logged_in_user: Option<Arc<User>>,
sidebar_projects: Vec<Project>,
content_pages: Vec<ContentPage>,
active_tab: &'static str,
selected_branch: String,
}
#[derive(Debug, Deserialize)]
pub struct ManualJobForm {
pub branch: String,
pub workflow_yaml: String,
}
pub async fn ci_new_get(
State(state): State<GlobalState>,
cookies: Cookies,
PathUserProject(owner, project): PathUserProject,
) -> Response<Body> {
let current_user = match session::current_user(&state, &cookies).await {
Ok(user) => user,
Err(_) => return Redirect::to("/login").into_response(),
};
let access_level = project.access_level(Some(current_user.slug.clone())).await;
if !access_level.is_allowed(AccessType::Write) {
return Redirect::to(&format!("/~{}/{}/ci", owner.slug, project.slug)).into_response();
}
if !state.ci_available() {
return Redirect::to(&format!("/~{}/{}/ci", owner.slug, project.slug)).into_response();
}
render_new_ci_page(&state, current_user, owner, project, None).await
}
pub async fn ci_new_post(
State(state): State<GlobalState>,
cookies: Cookies,
PathUserProject(owner, project): PathUserProject,
Form(form): Form<ManualJobForm>,
) -> Response<Body> {
let current_user = match session::current_user(&state, &cookies).await {
Ok(user) => user,
Err(_) => return Redirect::to("/login").into_response(),
};
let access_level = project.access_level(Some(current_user.slug.clone())).await;
if !access_level.is_allowed(AccessType::Write) {
return Redirect::to(&format!("/~{}/{}/ci", owner.slug, project.slug)).into_response();
}
if !state.ci_available() {
return Redirect::to(&format!("/~{}/{}/ci", owner.slug, project.slug)).into_response();
}
// Parse YAML
let workflow: CiWorkflow = match serde_yaml::from_str(&form.workflow_yaml) {
Ok(w) => w,
Err(e) => {
return render_new_ci_page_with_error(
&state,
current_user,
owner,
project,
&format!("Invalid YAML: {}", e),
&form.workflow_yaml,
)
.await;
}
};
// Get commit hash for selected branch
let summary = match state.repo.get_git_summary(&owner.slug, &project.slug).await {
Some(s) => s,
None => {
return render_new_ci_page_with_error(
&state,
current_user,
owner,
project,
"Failed to get repository info",
&form.workflow_yaml,
)
.await;
}
};
let commit_hash = match summary.branches_raw().get(&form.branch) {
Some(hash) => hash.clone(),
None => {
return render_new_ci_page_with_error(
&state,
current_user,
owner,
project,
"Branch not found",
&form.workflow_yaml,
)
.await;
}
};
// Create job request
let request = CiJobRequest {
owner: owner.slug.clone(),
project: project.slug.clone(),
branch: form.branch,
commit_hash,
workflow_name: workflow.name.clone(),
workflow,
};
// Get the CI sender and send the job
let sender = state
.ci_sender()
.and_then(|s| s.downcast_ref::<mpsc::Sender<CiJobRequest>>());
match sender {
Some(tx) => {
if let Err(e) = tx.send(request).await {
return render_new_ci_page_with_error(
&state,
current_user,
owner,
project,
&format!("Failed to queue job: {}", e),
&form.workflow_yaml,
)
.await;
}
}
None => {
return render_new_ci_page_with_error(
&state,
current_user,
owner,
project,
"CI system not available",
&form.workflow_yaml,
)
.await;
}
}
// Redirect to CI list
Redirect::to(&format!("/~{}/{}/ci", owner.slug, project.slug)).into_response()
}
async fn render_new_ci_page(
state: &GlobalState,
current_user: Arc<User>,
owner: Arc<User>,
project: Project,
message: Option<&str>,
) -> Response<Body> {
let workflow_content =
get_first_workflow_content(state, &owner.slug, &project.slug, &project.main_branch).await;
render_new_ci_page_inner(
state,
current_user,
owner,
project,
message,
&workflow_content,
)
.await
}
async fn render_new_ci_page_with_error(
state: &GlobalState,
current_user: Arc<User>,
owner: Arc<User>,
project: Project,
message: &str,
workflow_content: &str,
) -> Response<Body> {
render_new_ci_page_inner(
state,
current_user,
owner,
project,
Some(message),
workflow_content,
)
.await
}
async fn render_new_ci_page_inner(
state: &GlobalState,
current_user: Arc<User>,
owner: Arc<User>,
project: Project,
message: Option<&str>,
workflow_content: &str,
) -> Response<Body> {
let sidebar_projects = current_user.sidebar_projects(state).await;
let access_level = project.access_level(Some(current_user.slug.clone())).await;
let content_pages = state.config.content_pages.clone();
// Get branches
let branches: Vec<String> = match state.repo.get_git_summary(&owner.slug, &project.slug).await {
Some(s) => s.branches().iter().map(|s| s.to_string()).collect(),
None => vec![project.main_branch.clone()],
};
let template = CiNewTemplate {
owner,
project: &project,
access_level,
branches,
default_branch: &project.main_branch,
workflow_content: workflow_content.to_string(),
message,
logged_in_user: Some(current_user),
sidebar_projects,
content_pages,
active_tab: "ci",
selected_branch: project.main_branch.clone(),
};
Html(template.render_with_theme()).into_response()
}
async fn get_first_workflow_content(
state: &GlobalState,
owner: &str,
project: &str,
branch: &str,
) -> String {
let tree = match state
.repo
.get_git_tree(owner, project, branch, WORKFLOW_DIR)
.await
{
Ok(entries) => entries,
Err(_) => return default_workflow_yaml(),
};
let workflow_file = tree
.iter()
.find(|e| e.filename.ends_with(".yaml") || e.filename.ends_with(".yml"));
match workflow_file {
Some(entry) => {
let path = format!("{}/{}", WORKFLOW_DIR, entry.filename);
match state.repo.get_git_file(owner, project, branch, &path).await {
Ok(bytes) => String::from_utf8(bytes).unwrap_or_else(|_| default_workflow_yaml()),
Err(_) => default_workflow_yaml(),
}
}
None => default_workflow_yaml(),
}
}
fn default_workflow_yaml() -> String {
r#"name: Manual Job
jobs:
- name: build
steps:
- name: Run commands
run: |
echo "Hello from manual CI job"
"#
.to_string()
}