diff --git a/Cargo.toml b/Cargo.toml index 7debf77..fb872b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,10 @@ path = "lib.rs" [[bin]] name = "appks" path = "main.rs" + +[[bin]] +name = "gen_openapi" +path = "gen_openapi.rs" [dependencies] sqlx = { version = "0.9.0", features = ["postgres","runtime-tokio","chrono","uuid","json"] } tokio = { version = "1.52.3", features = ["full"] } diff --git a/api/mod.rs b/api/mod.rs index 8baefd6..53c9f6b 100644 --- a/api/mod.rs +++ b/api/mod.rs @@ -1,8 +1,10 @@ pub mod auth; pub mod issue; pub mod openapi; +pub mod pr; pub mod repo; pub mod response; pub mod routes; pub mod user; +pub mod wiki; pub mod workspace; diff --git a/api/openapi.rs b/api/openapi.rs index 7af80ad..e539f70 100644 --- a/api/openapi.rs +++ b/api/openapi.rs @@ -7,6 +7,8 @@ use crate::api::auth::register::RegisterResponse; use crate::api::issue::lock::LockIssueParams; use crate::api::issue::subscribers::MuteIssueParams; use crate::api::issue::transfer::TransferIssueParams; +use crate::api::pr::lock::LockPrParams; +use crate::api::pr::subscriptions::MutePrParams; use crate::api::repo::accept_invitation::AcceptInvitationParams; use crate::api::repo::set_branch_protection::SetBranchProtectionParams; use crate::api::repo::transfer_owner::TransferOwnerParams; @@ -18,6 +20,11 @@ use crate::models::issues::{ Issue, IssueAssignee, IssueComment, IssueEvent, IssueLabel, IssueLabelRelation, IssueMilestone, IssuePrRelation, IssueReaction, IssueRepoRelation, IssueSubscriber, IssueTemplate, }; +use crate::models::prs::{ + PrAssignee, PrCheckRun, PrCommit, PrEvent, PrFile, PrLabel, PrLabelRelation, PrMergeStrategy, + PrReaction, PrReview, PrReviewComment, PrStatus, PrSubscription, PullRequest, +}; +use crate::models::wiki::{WikiPage, WikiPageRevision}; use crate::models::repos::{ BranchProtectionRule, Repo, RepoBranch, RepoCommitComment, RepoCommitStatus, RepoDeployKey, RepoFork, RepoInvitation, RepoMember, RepoRelease, RepoStar, RepoStats, RepoTag, RepoWatch, @@ -52,6 +59,17 @@ use crate::service::issues::pr_relations::LinkPrParams; use crate::service::issues::reactions::CreateIssueReactionParams; use crate::service::issues::repo_relations::LinkRepoParams; use crate::service::issues::templates::{CreateTemplateParams, UpdateTemplateParams}; +use crate::service::pr::check_runs::{CreateCheckRunParams, UpdateCheckRunParams}; +use crate::service::pr::core::{CreatePrParams, MergePrParams, PrListFilters, UpdatePrParams}; +use crate::service::pr::labels::{CreatePrLabelParams, UpdatePrLabelParams}; +use crate::service::pr::merge_strategy::UpdateMergeStrategyParams; +use crate::service::pr::reactions::CreateReactionParams; +use crate::service::pr::reviews::{ + AddReplyParams, CreateReviewParams, DismissReviewParams, ReviewCommentParams, + SubmitReviewParams, +}; +use crate::api::wiki::compare_revisions::WikiCompareResult; +use crate::service::wiki::core::{CreateWikiPageParams, UpdateWikiPageParams}; use crate::service::repo::branches::CreateBranchParams; use crate::service::repo::commit_status::{CreateCommitCommentParams, CreateCommitStatusParams}; use crate::service::repo::core::{CreateRepoParams, UpdateRepoParams}; @@ -92,14 +110,16 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara info( title = "AppKS API", version = "0.1.0", - description = "AppKS collaborative development platform HTTP API. Auth endpoints use server-side sessions backed by Redis and a signed/encrypted session cookie. Sensitive password fields are RSA-OAEP-SHA256 encrypted per session before transmission." + description = "AppKS collaborative development platform HTTP API." ), tags( (name = "Auth", description = "Authentication, registration, session and email security endpoints."), (name = "User", description = "User account management, profile, appearance, notification settings, SSH/GPG keys, sessions, devices, OAuth accounts, security logs, and personal access tokens."), (name = "Workspaces", description = "Workspace CRUD, archiving, ownership transfer, and avatar management."), (name = "Repos", description = "Repository management including branches, tags, releases, forks, stars, watches, members, invitations, deploy keys, webhooks, protection rules, commit statuses, and statistics."), - (name = "Issues", description = "Issue tracking, comments, labels, milestones, assignees, events, reactions, subscribers, templates, and cross-references with repos and pull requests."), + (name = "Issues", description = "Issue tracking, comments, labels, milestones, assignees, events, reactions, subscribers, templates, and cross-references."), + (name = "Pull Requests", description = "Pull request lifecycle including reviews, check runs, merge strategies, labels, assignees, events, reactions, and subscriptions."), + (name = "Wiki", description = "Wiki page management including CRUD operations, revision history, version comparison, and page reversion."), ), paths( // Auth @@ -146,7 +166,7 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara crate::api::user::list_security_logs::list_security_logs, crate::api::user::list_personal_access_tokens::list_tokens, crate::api::user::revoke_personal_access_token::revoke_token, - // Issues - Core + // Issues crate::api::issue::list::list, crate::api::issue::get::get, crate::api::issue::create::create, @@ -156,53 +176,107 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara crate::api::issue::delete::delete, crate::api::issue::lock::lock, crate::api::issue::transfer::transfer, - // Issues - Comments crate::api::issue::list_comments::list_comments, crate::api::issue::create_comment::create_comment, crate::api::issue::update_comment::update_comment, crate::api::issue::delete_comment::delete_comment, - // Issues - Labels (repo-level) crate::api::issue::list_labels::list_labels, crate::api::issue::create_label::create_label, crate::api::issue::update_label::update_label, crate::api::issue::delete_label::delete_label, - // Issues - Label relations (issue-level) crate::api::issue::list_issue_labels::list_issue_labels, crate::api::issue::assign_label::assign_label, crate::api::issue::unassign_label::unassign_label, - // Issues - Milestones (repo-level) crate::api::issue::list_milestones::list_milestones, crate::api::issue::create_milestone::create_milestone, crate::api::issue::update_milestone::update_milestone, crate::api::issue::delete_milestone::delete_milestone, - // Issues - Assignees crate::api::issue::list_assignees::list_assignees, crate::api::issue::assign_issue::assign_issue, crate::api::issue::unassign_issue::unassign_issue, - // Issues - Events crate::api::issue::list_events::list_events, - // Issues - Reactions crate::api::issue::reactions::list_reactions, crate::api::issue::reactions::add_reaction, crate::api::issue::reactions::remove_reaction, - // Issues - Subscribers crate::api::issue::subscribers::list_subscribers, crate::api::issue::subscribers::subscribe, crate::api::issue::subscribers::unsubscribe, crate::api::issue::subscribers::mute, - // Issues - Templates (repo-level) crate::api::issue::templates::list_templates, crate::api::issue::templates::create_template, crate::api::issue::templates::update_template, crate::api::issue::templates::delete_template, - // Issues - Repo relations crate::api::issue::repo_relations::list_repo_relations, crate::api::issue::repo_relations::link_repo, crate::api::issue::repo_relations::unlink_repo, - // Issues - PR relations crate::api::issue::pr_relations::list_pr_relations, crate::api::issue::pr_relations::link_pr, crate::api::issue::pr_relations::unlink_pr, + // Pull Requests - Core + crate::api::pr::list::list, + crate::api::pr::get::get, + crate::api::pr::create::create, + crate::api::pr::update::update, + crate::api::pr::mark_ready::mark_ready, + crate::api::pr::close::close, + crate::api::pr::reopen::reopen, + crate::api::pr::delete::delete, + crate::api::pr::lock::lock, + crate::api::pr::merge::merge, + // Pull Requests - Commits & Files + crate::api::pr::list_commits::list_commits, + crate::api::pr::list_files::list_files, + // Pull Requests - Status & Merge Strategy + crate::api::pr::get_status::get_status, + crate::api::pr::merge_strategy::get_merge_strategy, + crate::api::pr::merge_strategy::update_merge_strategy, + // Pull Requests - Labels + crate::api::pr::labels::list_labels, + crate::api::pr::labels::create_label, + crate::api::pr::labels::update_label, + crate::api::pr::labels::delete_label, + crate::api::pr::labels::list_label_relations, + crate::api::pr::labels::assign_label, + crate::api::pr::labels::unassign_label, + // Pull Requests - Assignees + crate::api::pr::assignees::list_assignees, + crate::api::pr::assignees::assign_user, + crate::api::pr::assignees::unassign_user, + // Pull Requests - Reviews + crate::api::pr::reviews::list_reviews, + crate::api::pr::reviews::create_review, + crate::api::pr::reviews::submit_review, + crate::api::pr::reviews::dismiss_review, + crate::api::pr::reviews::list_review_comments, + crate::api::pr::reviews::add_review_reply, + crate::api::pr::reviews::update_review_comment, + crate::api::pr::reviews::delete_review_comment, + // Pull Requests - Check Runs + crate::api::pr::check_runs::list_check_runs, + crate::api::pr::check_runs::create_check_run, + crate::api::pr::check_runs::update_check_run, + crate::api::pr::check_runs::delete_check_run, + // Pull Requests - Events + crate::api::pr::events::list_events, + // Pull Requests - Reactions + crate::api::pr::reactions::list_reactions, + crate::api::pr::reactions::add_reaction, + crate::api::pr::reactions::remove_reaction, + // Pull Requests - Subscriptions + crate::api::pr::subscriptions::list_subscriptions, + crate::api::pr::subscriptions::subscribe, + crate::api::pr::subscriptions::unsubscribe, + crate::api::pr::subscriptions::mute, + // Wiki + crate::api::wiki::list_pages::list_pages, + crate::api::wiki::get_page::get_page, + crate::api::wiki::create_page::create_page, + crate::api::wiki::update_page::update_page, + crate::api::wiki::delete_page::delete_page, + crate::api::wiki::revert_page::revert_page, + crate::api::wiki::list_revisions::list_revisions, + crate::api::wiki::get_revision::get_revision, + crate::api::wiki::compare_revisions::compare_revisions, // Workspaces crate::api::workspace::list::handle, crate::api::workspace::get::handle, @@ -434,6 +508,75 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara ApiResponse>, IssuePrRelation, LinkPrParams, + // Pull Requests + ApiResponse, + ApiResponse>, + PullRequest, + CreatePrParams, + UpdatePrParams, + PrListFilters, + MergePrParams, + LockPrParams, + ApiResponse, + ApiResponse>, + PrCommit, + ApiResponse, + ApiResponse>, + PrFile, + ApiResponse, + PrStatus, + ApiResponse, + PrMergeStrategy, + UpdateMergeStrategyParams, + ApiResponse, + ApiResponse>, + PrLabel, + CreatePrLabelParams, + UpdatePrLabelParams, + ApiResponse, + ApiResponse>, + PrLabelRelation, + ApiResponse, + ApiResponse>, + PrAssignee, + ApiResponse, + ApiResponse>, + PrReview, + CreateReviewParams, + ReviewCommentParams, + SubmitReviewParams, + DismissReviewParams, + AddReplyParams, + ApiResponse, + ApiResponse>, + PrReviewComment, + ApiResponse, + ApiResponse>, + PrCheckRun, + CreateCheckRunParams, + UpdateCheckRunParams, + ApiResponse, + ApiResponse>, + PrEvent, + ApiResponse, + ApiResponse>, + PrReaction, + CreateReactionParams, + ApiResponse, + ApiResponse>, + PrSubscription, + MutePrParams, + // Wiki + ApiResponse, + ApiResponse>, + WikiPage, + CreateWikiPageParams, + UpdateWikiPageParams, + ApiResponse, + ApiResponse>, + ApiResponse, + WikiCompareResult, + WikiPageRevision, // Workspaces ApiResponse, ApiResponse>, diff --git a/api/pr/close.rs b/api/pr/close.rs new file mode 100644 index 0000000..00c851d --- /dev/null +++ b/api/pr/close.rs @@ -0,0 +1,55 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::prs::PullRequest; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct PathParams { + /// Workspace name (unique identifier) + pub workspace_name: String, + /// Repository name (unique within the workspace) + pub repo_name: String, + /// PR number (unique within the repository) + pub number: i64, +} + +/// Close a pull request +/// +/// Closes an open PR without merging. +/// Requires write access to the PR. +/// +/// Returns the closed PR. +#[utoipa::path( + post, + path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/close", + tag = "Pull Requests", + operation_id = "prClose", + params(PathParams), + responses( + (status = 200, description = "PR closed successfully.", body = ApiResponse), + (status = 400, description = "PR is not open", body = ApiErrorResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 403, description = "Insufficient permissions", body = ApiErrorResponse), + (status = 404, description = "PR not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn close( + service: web::Data, + session: Session, + path: web::Path, +) -> Result { + let pr = service + .pr + .pr_close(&session, &path.workspace_name, &path.repo_name, path.number) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(pr))) +} diff --git a/api/pr/create.rs b/api/pr/create.rs new file mode 100644 index 0000000..facf910 --- /dev/null +++ b/api/pr/create.rs @@ -0,0 +1,76 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::prs::PullRequest; +use crate::service::pr::core::CreatePrParams; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct PathParams { + /// Workspace name (unique identifier) + pub workspace_name: String, + /// Repository name (unique within the workspace) + pub repo_name: String, +} + +/// Create a pull request +/// +/// Creates a new pull request proposing changes from a source branch to a target branch. +/// Requires at least Member role in the repository. +/// +/// Parameters: +/// - title: PR title (required) +/// - body: PR description in markdown (optional) +/// - source_repo_id: Source repository ID (supports cross-repo PRs from forks) +/// - source_branch: Source branch name (must exist) +/// - target_branch: Target branch name (must exist in the repo) +/// - head_commit_sha: Head commit SHA from the source branch +/// - base_commit_sha: Base commit SHA for diff calculation (optional) +/// - draft: Whether this is a draft PR (optional, defaults to false) +/// +/// Effects: +/// - PR is created with auto-incrementing number +/// - PR status tracking is initialized +/// - Author is automatically subscribed +/// - Repository stats are updated +/// +/// Returns the created PR with full metadata. +#[utoipa::path( + post, + path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs", + tag = "Pull Requests", + operation_id = "prCreate", + params(PathParams), + request_body( + content = CreatePrParams, + description = "PR creation parameters", + content_type = "application/json" + ), + responses( + (status = 201, description = "PR created successfully. Returns the newly created PR with full metadata.", body = ApiResponse), + (status = 400, description = "Invalid parameters: empty title, non-existent branch/commit, or invalid fork relationship", body = ApiErrorResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 403, description = "Insufficient permissions (requires Member role or higher)", body = ApiErrorResponse), + (status = 404, description = "Repository or workspace not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn create( + service: web::Data, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + let pr = service + .pr + .pr_create(&session, &path.workspace_name, &path.repo_name, params.into_inner()) + .await?; + Ok(HttpResponse::Created().json(ApiResponse::new(pr))) +} diff --git a/api/pr/delete.rs b/api/pr/delete.rs new file mode 100644 index 0000000..a1d15ee --- /dev/null +++ b/api/pr/delete.rs @@ -0,0 +1,52 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct PathParams { + /// Workspace name (unique identifier) + pub workspace_name: String, + /// Repository name (unique within the workspace) + pub repo_name: String, + /// PR number (unique within the repository) + pub number: i64, +} + +/// Delete a pull request +/// +/// Soft-deletes a PR. Requires Admin role in the repository. +/// +/// Returns success message on completion. +#[utoipa::path( + delete, + path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}", + tag = "Pull Requests", + operation_id = "prDelete", + params(PathParams), + responses( + (status = 200, description = "PR deleted successfully.", body = ApiResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 403, description = "Insufficient permissions (requires Admin role)", body = ApiErrorResponse), + (status = 404, description = "PR not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn delete( + service: web::Data, + session: Session, + path: web::Path, +) -> Result { + service + .pr + .pr_delete(&session, &path.workspace_name, &path.repo_name, path.number) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new("PR deleted".to_string()))) +} diff --git a/api/pr/get.rs b/api/pr/get.rs new file mode 100644 index 0000000..1aab462 --- /dev/null +++ b/api/pr/get.rs @@ -0,0 +1,52 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::prs::PullRequest; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct PathParams { + /// Workspace name (unique identifier) + pub workspace_name: String, + /// Repository name (unique within the workspace) + pub repo_name: String, + /// PR number (unique within the repository) + pub number: i64, +} + +/// Get a pull request by number +/// +/// Returns detailed information about a specific pull request. +/// Requires read access to the repository. +#[utoipa::path( + get, + path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}", + tag = "Pull Requests", + operation_id = "prGet", + params(PathParams), + responses( + (status = 200, description = "PR retrieved successfully. Returns complete PR with all metadata.", body = ApiResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 403, description = "Insufficient permissions to access this repository", body = ApiErrorResponse), + (status = 404, description = "Repository, workspace, or PR not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn get( + service: web::Data, + session: Session, + path: web::Path, +) -> Result { + let pr = service + .pr + .pr_get(&session, &path.workspace_name, &path.repo_name, path.number) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(pr))) +} diff --git a/api/pr/get_status.rs b/api/pr/get_status.rs new file mode 100644 index 0000000..a2b0530 --- /dev/null +++ b/api/pr/get_status.rs @@ -0,0 +1,51 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::prs::PrStatus; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct PathParams { + /// Workspace name (unique identifier) + pub workspace_name: String, + /// Repository name (unique within the workspace) + pub repo_name: String, + /// PR number (unique within the repository) + pub number: i64, +} + +/// Get PR status summary +/// +/// Returns the current status of a PR including checks state, mergeability, +/// approval count, and file change statistics. +/// Requires read access to the repository. +#[utoipa::path( + get, + path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/status", + tag = "Pull Requests", + operation_id = "prGetStatus", + params(PathParams), + responses( + (status = 200, description = "PR status retrieved successfully.", body = ApiResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 403, description = "Insufficient permissions", body = ApiErrorResponse), + (status = 404, description = "PR or status not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security(("session_cookie" = [])) +)] +pub async fn get_status( + service: web::Data, + session: Session, + path: web::Path, +) -> Result { + let status = service + .pr + .pr_status(&session, &path.workspace_name, &path.repo_name, path.number) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(status))) +} diff --git a/api/pr/list.rs b/api/pr/list.rs new file mode 100644 index 0000000..56249c4 --- /dev/null +++ b/api/pr/list.rs @@ -0,0 +1,79 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::prs::PullRequest; +use crate::service::pr::core::PrListFilters; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct PathParams { + /// Workspace name (unique identifier) + pub workspace_name: String, + /// Repository name (unique within the workspace) + pub repo_name: String, +} + +#[derive(Debug, Deserialize, IntoParams)] +pub struct QueryParams { + /// Filter by PR state ("open", "closed", "merged") + pub state: Option, + /// Filter by author user ID + pub author_id: Option, + /// Filter by draft status + pub draft: Option, + /// Maximum number of PRs to return (default: 50, max: 100) + pub limit: Option, + /// Number of PRs to skip for pagination (default: 0) + pub offset: Option, +} + +/// List pull requests in a repository +/// +/// Returns a paginated list of pull requests, sorted by number (newest first). +/// Supports filtering by state, author, and draft status. +/// Requires read access to the repository. +#[utoipa::path( + get, + path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs", + tag = "Pull Requests", + operation_id = "prList", + params(PathParams, QueryParams), + responses( + (status = 200, description = "PRs listed successfully. Returns filtered array of PR objects.", body = ApiResponse>), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 403, description = "Insufficient permissions to access this repository", body = ApiErrorResponse), + (status = 404, description = "Repository or workspace not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn list( + service: web::Data, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + let filters = PrListFilters { + state: query.state.clone(), + author_id: query.author_id, + draft: query.draft, + }; + let prs = service + .pr + .pr_list( + &session, + &path.workspace_name, + &path.repo_name, + filters, + query.limit.unwrap_or(50), + query.offset.unwrap_or(0), + ) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(prs))) +} diff --git a/api/pr/list_commits.rs b/api/pr/list_commits.rs new file mode 100644 index 0000000..e74cc23 --- /dev/null +++ b/api/pr/list_commits.rs @@ -0,0 +1,59 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::prs::PrCommit; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct PathParams { + /// Workspace name (unique identifier) + pub workspace_name: String, + /// Repository name (unique within the workspace) + pub repo_name: String, + /// PR number (unique within the repository) + pub number: i64, +} + +#[derive(Debug, Deserialize, IntoParams)] +pub struct QueryParams { + /// Maximum number of commits to return (default: 50, max: 100) + pub limit: Option, + /// Number of commits to skip for pagination (default: 0) + pub offset: Option, +} + +/// List commits in a pull request +/// +/// Returns a paginated list of all commits included in the PR, sorted by position. +/// Requires read access to the repository. +#[utoipa::path( + get, + path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/commits", + tag = "Pull Requests", + operation_id = "prListCommits", + params(PathParams, QueryParams), + responses( + (status = 200, description = "Commits listed successfully.", body = ApiResponse>), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 403, description = "Insufficient permissions", body = ApiErrorResponse), + (status = 404, description = "PR not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security(("session_cookie" = [])) +)] +pub async fn list_commits( + service: web::Data, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + let commits = service + .pr + .pr_commits(&session, &path.workspace_name, &path.repo_name, path.number, query.limit.unwrap_or(50), query.offset.unwrap_or(0)) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(commits))) +} diff --git a/api/pr/list_files.rs b/api/pr/list_files.rs new file mode 100644 index 0000000..d518a26 --- /dev/null +++ b/api/pr/list_files.rs @@ -0,0 +1,60 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::prs::PrFile; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct PathParams { + /// Workspace name (unique identifier) + pub workspace_name: String, + /// Repository name (unique within the workspace) + pub repo_name: String, + /// PR number (unique within the repository) + pub number: i64, +} + +#[derive(Debug, Deserialize, IntoParams)] +pub struct QueryParams { + /// Maximum number of files to return (default: 50, max: 100) + pub limit: Option, + /// Number of files to skip for pagination (default: 0) + pub offset: Option, +} + +/// List changed files in a pull request +/// +/// Returns a paginated list of all files changed in the PR, sorted by path. +/// Includes additions, deletions, and patch diffs for each file. +/// Requires read access to the repository. +#[utoipa::path( + get, + path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/files", + tag = "Pull Requests", + operation_id = "prListFiles", + params(PathParams, QueryParams), + responses( + (status = 200, description = "Files listed successfully.", body = ApiResponse>), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 403, description = "Insufficient permissions", body = ApiErrorResponse), + (status = 404, description = "PR not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security(("session_cookie" = [])) +)] +pub async fn list_files( + service: web::Data, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + let files = service + .pr + .pr_files(&session, &path.workspace_name, &path.repo_name, path.number, query.limit.unwrap_or(50), query.offset.unwrap_or(0)) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(files))) +} diff --git a/api/pr/lock.rs b/api/pr/lock.rs new file mode 100644 index 0000000..52a80c3 --- /dev/null +++ b/api/pr/lock.rs @@ -0,0 +1,66 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::prs::PullRequest; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct PathParams { + /// Workspace name (unique identifier) + pub workspace_name: String, + /// Repository name (unique within the workspace) + pub repo_name: String, + /// PR number (unique within the repository) + pub number: i64, +} + +#[derive(Debug, Deserialize, utoipa::ToSchema)] +pub struct LockPrParams { + /// Whether to lock (true) or unlock (false) the PR conversation + pub locked: bool, +} + +/// Lock or unlock a pull request conversation +/// +/// When locked, only repository maintainers and admins can comment on the PR. +/// Requires write access to the PR. +/// +/// Returns the updated PR. +#[utoipa::path( + put, + path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/lock", + tag = "Pull Requests", + operation_id = "prLock", + params(PathParams), + request_body( + content = LockPrParams, + description = "Lock/unlock parameters", + content_type = "application/json" + ), + responses( + (status = 200, description = "PR lock status updated.", body = ApiResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 403, description = "Insufficient permissions", body = ApiErrorResponse), + (status = 404, description = "PR not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn lock( + service: web::Data, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + let pr = service + .pr + .pr_lock(&session, &path.workspace_name, &path.repo_name, path.number, params.locked) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(pr))) +} diff --git a/api/pr/mark_ready.rs b/api/pr/mark_ready.rs new file mode 100644 index 0000000..da66b99 --- /dev/null +++ b/api/pr/mark_ready.rs @@ -0,0 +1,60 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::prs::PullRequest; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct PathParams { + /// Workspace name (unique identifier) + pub workspace_name: String, + /// Repository name (unique within the workspace) + pub repo_name: String, + /// PR number (unique within the repository) + pub number: i64, +} + +/// Mark a draft PR as ready for review +/// +/// Converts a draft pull request to a ready-for-review state. +/// Requires write access to the PR. +/// +/// Effects: +/// - Draft flag is set to false +/// - A "DraftReady" event is logged +/// - Reviewers are notified that the PR is ready +/// +/// Returns the updated PR. +#[utoipa::path( + post, + path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/ready", + tag = "Pull Requests", + operation_id = "prMarkReady", + params(PathParams), + responses( + (status = 200, description = "PR marked as ready for review.", body = ApiResponse), + (status = 400, description = "PR is already ready for review", body = ApiErrorResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 403, description = "Insufficient permissions", body = ApiErrorResponse), + (status = 404, description = "PR not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn mark_ready( + service: web::Data, + session: Session, + path: web::Path, +) -> Result { + let pr = service + .pr + .pr_mark_ready(&session, &path.workspace_name, &path.repo_name, path.number) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(pr))) +} diff --git a/api/pr/merge.rs b/api/pr/merge.rs new file mode 100644 index 0000000..d653f67 --- /dev/null +++ b/api/pr/merge.rs @@ -0,0 +1,73 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::prs::PullRequest; +use crate::service::pr::core::MergePrParams; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct PathParams { + /// Workspace name (unique identifier) + pub workspace_name: String, + /// Repository name (unique within the workspace) + pub repo_name: String, + /// PR number (unique within the repository) + pub number: i64, +} + +/// Merge a pull request +/// +/// Merges the source branch into the target branch. +/// Requires at least Maintainer role in the repository. +/// +/// Branch protection rules are enforced: +/// - Required approvals count (self-approval not allowed) +/// - Required status checks must pass +/// - Admins can bypass protection rules +/// +/// Parameters: +/// - strategy: Merge strategy ("merge", "squash", "rebase", default: "merge") +/// - squash_title: Custom title for squash merge (optional) +/// - squash_message: Custom message for squash merge (optional) +/// - delete_source_branch: Delete source branch after merge (optional, only same-repo) +/// +/// Returns the merged PR with merge commit SHA. +#[utoipa::path( + post, + path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/merge", + tag = "Pull Requests", + operation_id = "prMerge", + params(PathParams), + request_body( + content = MergePrParams, + description = "Merge parameters", + content_type = "application/json" + ), + responses( + (status = 200, description = "PR merged successfully. Returns the merged PR with merge commit SHA.", body = ApiResponse), + (status = 400, description = "Cannot merge: PR not open, is draft, or branch protection requirements not met", body = ApiErrorResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 403, description = "Insufficient permissions (requires Maintainer role or higher)", body = ApiErrorResponse), + (status = 404, description = "PR not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error or git merge failure", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn merge( + service: web::Data, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + let pr = service + .pr + .pr_merge(&session, &path.workspace_name, &path.repo_name, path.number, params.into_inner()) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(pr))) +} diff --git a/api/pr/merge_strategy.rs b/api/pr/merge_strategy.rs new file mode 100644 index 0000000..bf5b4c4 --- /dev/null +++ b/api/pr/merge_strategy.rs @@ -0,0 +1,98 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::prs::PrMergeStrategy; +use crate::service::pr::merge_strategy::UpdateMergeStrategyParams; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct PathParams { + /// Workspace name (unique identifier) + pub workspace_name: String, + /// Repository name (unique within the workspace) + pub repo_name: String, + /// PR number (unique within the repository) + pub number: i64, +} + +/// Get PR merge strategy +/// +/// Returns the current merge strategy settings for a PR. +/// Requires read access to the repository. +#[utoipa::path( + get, + path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/merge-strategy", + tag = "Pull Requests", + operation_id = "prGetMergeStrategy", + params(PathParams), + responses( + (status = 200, description = "Merge strategy retrieved successfully.", body = ApiResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 404, description = "PR or merge strategy not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security(("session_cookie" = [])) +)] +pub async fn get_merge_strategy( + service: web::Data, + session: Session, + path: web::Path, +) -> Result { + let strategy = service + .pr + .pr_merge_strategy(&session, &path.workspace_name, &path.repo_name, path.number) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(strategy))) +} + +/// Update PR merge strategy +/// +/// Updates the merge strategy settings for a PR. +/// Requires write access to the PR. +/// +/// Updatable fields: +/// - strategy: Merge strategy type ("merge", "squash", "rebase") +/// - auto_merge: Enable auto-merge when all checks pass +/// - squash_title: Custom title for squash merge +/// - squash_message: Custom message for squash merge +/// - delete_source_branch: Delete source branch after merge +/// - merge_when_checks_pass: Auto-merge when checks pass +/// +/// All fields are optional; only provided fields are updated. +#[utoipa::path( + put, + path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/merge-strategy", + tag = "Pull Requests", + operation_id = "prUpdateMergeStrategy", + params(PathParams), + request_body( + content = UpdateMergeStrategyParams, + description = "Merge strategy update parameters (all fields optional)", + content_type = "application/json" + ), + responses( + (status = 200, description = "Merge strategy updated successfully.", body = ApiResponse), + (status = 400, description = "Invalid parameters: unsupported strategy", body = ApiErrorResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 403, description = "Insufficient permissions", body = ApiErrorResponse), + (status = 404, description = "PR not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security(("session_cookie" = [])) +)] +pub async fn update_merge_strategy( + service: web::Data, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + let strategy = service + .pr + .pr_update_merge_strategy(&session, &path.workspace_name, &path.repo_name, path.number, params.into_inner()) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(strategy))) +} diff --git a/api/pr/reopen.rs b/api/pr/reopen.rs new file mode 100644 index 0000000..689663c --- /dev/null +++ b/api/pr/reopen.rs @@ -0,0 +1,55 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::prs::PullRequest; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct PathParams { + /// Workspace name (unique identifier) + pub workspace_name: String, + /// Repository name (unique within the workspace) + pub repo_name: String, + /// PR number (unique within the repository) + pub number: i64, +} + +/// Reopen a pull request +/// +/// Reopens a closed (but not merged) PR. +/// Requires write access to the PR. +/// +/// Returns the reopened PR. +#[utoipa::path( + post, + path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}/reopen", + tag = "Pull Requests", + operation_id = "prReopen", + params(PathParams), + responses( + (status = 200, description = "PR reopened successfully.", body = ApiResponse), + (status = 400, description = "PR is not closed or already merged", body = ApiErrorResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 403, description = "Insufficient permissions", body = ApiErrorResponse), + (status = 404, description = "PR not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn reopen( + service: web::Data, + session: Session, + path: web::Path, +) -> Result { + let pr = service + .pr + .pr_reopen(&session, &path.workspace_name, &path.repo_name, path.number) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(pr))) +} diff --git a/api/pr/update.rs b/api/pr/update.rs new file mode 100644 index 0000000..2a0b470 --- /dev/null +++ b/api/pr/update.rs @@ -0,0 +1,63 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::prs::PullRequest; +use crate::service::pr::core::UpdatePrParams; +use crate::service::AppService; +use crate::session::Session; + +#[derive(Debug, Deserialize, IntoParams)] +pub struct PathParams { + /// Workspace name (unique identifier) + pub workspace_name: String, + /// Repository name (unique within the workspace) + pub repo_name: String, + /// PR number (unique within the repository) + pub number: i64, +} + +/// Update a pull request +/// +/// Updates an existing pull request's metadata such as title, body, target branch, and draft status. +/// Requires write access to the PR (author or repository member). +/// +/// All fields are optional; only provided fields are updated. +/// Returns the updated PR with full metadata. +#[utoipa::path( + put, + path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/prs/{number}", + tag = "Pull Requests", + operation_id = "prUpdate", + params(PathParams), + request_body( + content = UpdatePrParams, + description = "PR update parameters (all fields optional)", + content_type = "application/json" + ), + responses( + (status = 200, description = "PR updated successfully. Returns the updated PR with full metadata.", body = ApiResponse), + (status = 400, description = "Invalid parameters", body = ApiErrorResponse), + (status = 401, description = "Authentication required or session expired", body = ApiErrorResponse), + (status = 403, description = "Insufficient permissions to edit this PR", body = ApiErrorResponse), + (status = 404, description = "Repository, workspace, or PR not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ), + security( + ("session_cookie" = []) + ) +)] +pub async fn update( + service: web::Data, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + let pr = service + .pr + .pr_update(&session, &path.workspace_name, &path.repo_name, path.number, params.into_inner()) + .await?; + Ok(HttpResponse::Ok().json(ApiResponse::new(pr))) +} diff --git a/api/routes.rs b/api/routes.rs index 44e5917..044fae9 100644 --- a/api/routes.rs +++ b/api/routes.rs @@ -3,8 +3,10 @@ use actix_web::web::scope; use crate::api::auth; use crate::api::issue; +use crate::api::pr; use crate::api::repo; use crate::api::user; +use crate::api::wiki; use crate::api::workspace; pub fn init_routes(cfg: &mut web::ServiceConfig) { @@ -17,7 +19,12 @@ pub fn init_routes(cfg: &mut web::ServiceConfig) { .service( scope("/workspaces/{workspace_name}") .configure(issue::configure) - .service(scope("/repos/{repo_name}").configure(issue::configure_repo_level)), + .service( + scope("/repos/{repo_name}") + .configure(issue::configure_repo_level) + .configure(pr::configure) + .configure(wiki::configure), + ), ), ); } diff --git a/gen_openapi.rs b/gen_openapi.rs new file mode 100644 index 0000000..d302201 --- /dev/null +++ b/gen_openapi.rs @@ -0,0 +1,12 @@ +use utoipa::OpenApi; +use appks::api::openapi::OpenApiDoc; + +fn main() { + let json = OpenApiDoc::openapi() + .to_pretty_json(); + if let Ok(json) = json { + if let Err(e) = std::fs::write("openapi.json", json) { + eprintln!("{}", e); + } + } +} \ No newline at end of file diff --git a/models/prs/pr_assignees.rs b/models/prs/pr_assignees.rs index 3f08e06..82f7a77 100644 --- a/models/prs/pr_assignees.rs +++ b/models/prs/pr_assignees.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct PrAssignee { pub id: Uuid, pub pull_request_id: Uuid, diff --git a/models/prs/pr_check_runs.rs b/models/prs/pr_check_runs.rs index 08715ea..12c7b32 100644 --- a/models/prs/pr_check_runs.rs +++ b/models/prs/pr_check_runs.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct PrCheckRun { pub id: Uuid, pub pull_request_id: Uuid, diff --git a/models/prs/pr_commits.rs b/models/prs/pr_commits.rs index 568a9c1..e2e434a 100644 --- a/models/prs/pr_commits.rs +++ b/models/prs/pr_commits.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct PrCommit { pub id: Uuid, pub pull_request_id: Uuid, diff --git a/models/prs/pr_events.rs b/models/prs/pr_events.rs index e1b2fb8..2978df3 100644 --- a/models/prs/pr_events.rs +++ b/models/prs/pr_events.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct PrEvent { pub id: Uuid, pub pull_request_id: Uuid, diff --git a/models/prs/pr_files.rs b/models/prs/pr_files.rs index 5657c70..14505e2 100644 --- a/models/prs/pr_files.rs +++ b/models/prs/pr_files.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct PrFile { pub id: Uuid, pub pull_request_id: Uuid, diff --git a/models/prs/pr_label_relations.rs b/models/prs/pr_label_relations.rs index 51706c9..d340aa7 100644 --- a/models/prs/pr_label_relations.rs +++ b/models/prs/pr_label_relations.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct PrLabelRelation { pub id: Uuid, pub pull_request_id: Uuid, diff --git a/models/prs/pr_labels.rs b/models/prs/pr_labels.rs index 1486ea9..9289bfc 100644 --- a/models/prs/pr_labels.rs +++ b/models/prs/pr_labels.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct PrLabel { pub id: Uuid, pub repo_id: Uuid, diff --git a/models/prs/pr_merge_strategy.rs b/models/prs/pr_merge_strategy.rs index b0e4273..c4b63ca 100644 --- a/models/prs/pr_merge_strategy.rs +++ b/models/prs/pr_merge_strategy.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct PrMergeStrategy { pub pull_request_id: Uuid, pub strategy: MergeStrategyKind, diff --git a/models/prs/pr_reactions.rs b/models/prs/pr_reactions.rs index 49b952c..a0ad1be 100644 --- a/models/prs/pr_reactions.rs +++ b/models/prs/pr_reactions.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct PrReaction { pub id: Uuid, pub pull_request_id: Uuid, diff --git a/models/prs/pr_review.rs b/models/prs/pr_review.rs index 5257f47..8cdd3b3 100644 --- a/models/prs/pr_review.rs +++ b/models/prs/pr_review.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct PrReview { pub id: Uuid, pub pull_request_id: Uuid, diff --git a/models/prs/pr_review_comment.rs b/models/prs/pr_review_comment.rs index 3573b86..6f30a3e 100644 --- a/models/prs/pr_review_comment.rs +++ b/models/prs/pr_review_comment.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct PrReviewComment { pub id: Uuid, pub review_id: Uuid, diff --git a/models/prs/pr_status.rs b/models/prs/pr_status.rs index 797d983..df0c888 100644 --- a/models/prs/pr_status.rs +++ b/models/prs/pr_status.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct PrStatus { pub pull_request_id: Uuid, pub head_commit_sha: String, diff --git a/models/prs/pr_subscriptions.rs b/models/prs/pr_subscriptions.rs index f215f63..3024d46 100644 --- a/models/prs/pr_subscriptions.rs +++ b/models/prs/pr_subscriptions.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct PrSubscription { pub id: Uuid, pub pull_request_id: Uuid, diff --git a/models/prs/pull_request.rs b/models/prs/pull_request.rs index 577433f..2ccc524 100644 --- a/models/prs/pull_request.rs +++ b/models/prs/pull_request.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct PullRequest { pub id: Uuid, pub repo_id: Uuid, diff --git a/service/workspace/core.rs b/service/workspace/core.rs index 1fdb338..ed1ef29 100644 --- a/service/workspace/core.rs +++ b/service/workspace/core.rs @@ -500,7 +500,7 @@ impl WorkspaceService { } } - pub(crate) async fn find_workspace_by_id( + pub async fn find_workspace_by_id( &self, workspace_id: Uuid, ) -> Result {