Login
4 branches 0 tags
Ben (Desktop/Arch) Code cleanup ada8ea6 12 days ago 251 Commits
rubhub / src / controllers / project / ci / new.rs
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()
}