feat(api): add pull request and wiki API endpoints with OpenAPI generator

- Add gen_openapi binary for generating OpenAPI specification
- Implement comprehensive pull request API endpoints including core operations
- Add pull request reviews, check runs, labels, assignees, and events APIs
- Include pull request status and merge strategy management endpoints
- Add wiki page CRUD operations with revision history and comparison
- Update OpenAPI documentation with Pull Requests and Wiki tags
- Modify workspace find function visibility for external access
- Integrate new API modules into main OpenAPI router configuration
This commit is contained in:
zhenyi
2026-06-07 19:58:02 +08:00
parent b660db7a91
commit 3a22c4265d
34 changed files with 1097 additions and 30 deletions
+55
View File
@@ -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<PullRequest>),
(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<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let pr = service
.pr
.pr_close(&session, &path.workspace_name, &path.repo_name, path.number)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(pr)))
}
+76
View File
@@ -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<PullRequest>),
(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<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<CreatePrParams>,
) -> Result<HttpResponse, AppError> {
let pr = service
.pr
.pr_create(&session, &path.workspace_name, &path.repo_name, params.into_inner())
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(pr)))
}
+52
View File
@@ -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<String>),
(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<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.pr
.pr_delete(&session, &path.workspace_name, &path.repo_name, path.number)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("PR deleted".to_string())))
}
+52
View File
@@ -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<PullRequest>),
(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<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let pr = service
.pr
.pr_get(&session, &path.workspace_name, &path.repo_name, path.number)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(pr)))
}
+51
View File
@@ -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<PrStatus>),
(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<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let status = service
.pr
.pr_status(&session, &path.workspace_name, &path.repo_name, path.number)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(status)))
}
+79
View File
@@ -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<String>,
/// Filter by author user ID
pub author_id: Option<uuid::Uuid>,
/// Filter by draft status
pub draft: Option<bool>,
/// Maximum number of PRs to return (default: 50, max: 100)
pub limit: Option<i64>,
/// Number of PRs to skip for pagination (default: 0)
pub offset: Option<i64>,
}
/// 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<Vec<PullRequest>>),
(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<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
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)))
}
+59
View File
@@ -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<i64>,
/// Number of commits to skip for pagination (default: 0)
pub offset: Option<i64>,
}
/// 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<Vec<PrCommit>>),
(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<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
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)))
}
+60
View File
@@ -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<i64>,
/// Number of files to skip for pagination (default: 0)
pub offset: Option<i64>,
}
/// 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<Vec<PrFile>>),
(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<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
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)))
}
+66
View File
@@ -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<PullRequest>),
(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<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<LockPrParams>,
) -> Result<HttpResponse, AppError> {
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)))
}
+60
View File
@@ -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<PullRequest>),
(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<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let pr = service
.pr
.pr_mark_ready(&session, &path.workspace_name, &path.repo_name, path.number)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(pr)))
}
+73
View File
@@ -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<PullRequest>),
(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<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<MergePrParams>,
) -> Result<HttpResponse, AppError> {
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)))
}
+98
View File
@@ -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<PrMergeStrategy>),
(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<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
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<PrMergeStrategy>),
(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<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<UpdateMergeStrategyParams>,
) -> Result<HttpResponse, AppError> {
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)))
}
+55
View File
@@ -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<PullRequest>),
(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<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let pr = service
.pr
.pr_reopen(&session, &path.workspace_name, &path.repo_name, path.number)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(pr)))
}
+63
View File
@@ -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<PullRequest>),
(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<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<UpdatePrParams>,
) -> Result<HttpResponse, AppError> {
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)))
}