feat(api): add comprehensive repository management API endpoints
- Introduce new repo module with complete repository functionality - Add endpoints for repository CRUD operations (create, get, update, archive, delete) - Implement branch management with create, list, delete and protection features - Add tag management with create, list and delete operations - Include release management with create, update and delete capabilities - Support repository forking with sync functionality - Implement starring and watching mechanisms for repositories - Add member management with roles and invitations - Provide deploy key management for CI/CD integration - Create webhook management for external integrations - Implement branch protection rules with approval requirements - Add commit status and comment functionality for code reviews - Include merge checking logic for pull requests - Register all new endpoints in OpenAPI documentation - Configure routes to handle new repository-specific paths
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod openapi;
|
pub mod openapi;
|
||||||
|
pub mod repo;
|
||||||
pub mod response;
|
pub mod response;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
pub mod workspace;
|
pub mod workspace;
|
||||||
|
|||||||
+152
@@ -5,9 +5,18 @@ use crate::api::auth::regenerate_2fa_backup_codes::{
|
|||||||
};
|
};
|
||||||
use crate::api::auth::register::RegisterResponse;
|
use crate::api::auth::register::RegisterResponse;
|
||||||
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse, ApiResponse};
|
use crate::api::response::{ApiEmptyResponse, ApiErrorResponse, ApiResponse};
|
||||||
|
use crate::api::repo::accept_invitation::AcceptInvitationParams;
|
||||||
|
use crate::api::repo::set_branch_protection::SetBranchProtectionParams;
|
||||||
|
use crate::api::repo::transfer_owner::TransferOwnerParams;
|
||||||
|
use crate::service::repo::watches::WatchParams;
|
||||||
use crate::api::workspace::accept_invitation::AcceptInvitationRequest;
|
use crate::api::workspace::accept_invitation::AcceptInvitationRequest;
|
||||||
use crate::api::workspace::review_approval::ReviewApprovalRequest;
|
use crate::api::workspace::review_approval::ReviewApprovalRequest;
|
||||||
use crate::api::workspace::transfer_owner::TransferOwnerRequest;
|
use crate::api::workspace::transfer_owner::TransferOwnerRequest;
|
||||||
|
use crate::models::repos::{
|
||||||
|
BranchProtectionRule, Repo, RepoBranch, RepoCommitComment,
|
||||||
|
RepoCommitStatus, RepoDeployKey, RepoFork, RepoInvitation, RepoMember, RepoRelease,
|
||||||
|
RepoStar, RepoStats, RepoTag, RepoWatch, RepoWebhook,
|
||||||
|
};
|
||||||
use crate::models::workspaces::{
|
use crate::models::workspaces::{
|
||||||
Workspace, WorkspaceAuditLog, WorkspaceBilling, WorkspaceCustomBranding, WorkspaceDomain,
|
Workspace, WorkspaceAuditLog, WorkspaceBilling, WorkspaceCustomBranding, WorkspaceDomain,
|
||||||
WorkspaceIntegration, WorkspaceInvitation, WorkspaceMember, WorkspacePendingApproval,
|
WorkspaceIntegration, WorkspaceInvitation, WorkspaceMember, WorkspacePendingApproval,
|
||||||
@@ -25,6 +34,18 @@ use crate::service::auth::rsa::RsaResponse;
|
|||||||
use crate::service::auth::totp::{
|
use crate::service::auth::totp::{
|
||||||
Disable2FAParams, Enable2FAResponse, Get2FAStatusResponse, Verify2FAParams,
|
Disable2FAParams, Enable2FAResponse, Get2FAStatusResponse, Verify2FAParams,
|
||||||
};
|
};
|
||||||
|
use crate::service::repo::branches::CreateBranchParams;
|
||||||
|
use crate::service::repo::commit_status::{CreateCommitCommentParams, CreateCommitStatusParams};
|
||||||
|
use crate::service::repo::core::{CreateRepoParams, UpdateRepoParams};
|
||||||
|
use crate::service::repo::deploy_keys::AddDeployKeyParams;
|
||||||
|
use crate::service::repo::fork::ForkRepoParams;
|
||||||
|
use crate::service::repo::invitations::CreateRepoInvitationParams;
|
||||||
|
use crate::service::repo::members::{AddRepoMemberParams, UpdateRepoMemberRoleParams};
|
||||||
|
use crate::service::repo::protection::{
|
||||||
|
BranchMergeCheck, CreateProtectionRuleParams, UpdateProtectionRuleParams,
|
||||||
|
};
|
||||||
|
use crate::service::repo::releases::{CreateReleaseParams, UpdateReleaseParams};
|
||||||
|
use crate::service::repo::tags::CreateTagParams;
|
||||||
use crate::service::workspace::approvals::RequestApprovalParams;
|
use crate::service::workspace::approvals::RequestApprovalParams;
|
||||||
use crate::service::workspace::billing::UpdateBillingParams;
|
use crate::service::workspace::billing::UpdateBillingParams;
|
||||||
use crate::service::workspace::branding::UpdateBrandingParams;
|
use crate::service::workspace::branding::UpdateBrandingParams;
|
||||||
@@ -46,6 +67,7 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
|
|||||||
tags(
|
tags(
|
||||||
(name = "Auth", description = "Authentication, registration, session and email security endpoints."),
|
(name = "Auth", description = "Authentication, registration, session and email security endpoints."),
|
||||||
(name = "Workspaces", description = "Workspace CRUD, archiving, ownership transfer, and avatar management."),
|
(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."),
|
||||||
),
|
),
|
||||||
paths(
|
paths(
|
||||||
// Auth
|
// Auth
|
||||||
@@ -110,6 +132,66 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
|
|||||||
crate::api::workspace::request_approval::handle,
|
crate::api::workspace::request_approval::handle,
|
||||||
crate::api::workspace::review_approval::handle,
|
crate::api::workspace::review_approval::handle,
|
||||||
crate::api::workspace::audit_logs::handle,
|
crate::api::workspace::audit_logs::handle,
|
||||||
|
// Repos
|
||||||
|
crate::api::repo::list::list,
|
||||||
|
crate::api::repo::get::get,
|
||||||
|
crate::api::repo::create::create,
|
||||||
|
crate::api::repo::update::update,
|
||||||
|
crate::api::repo::archive::archive,
|
||||||
|
crate::api::repo::unarchive::unarchive,
|
||||||
|
crate::api::repo::delete::delete,
|
||||||
|
crate::api::repo::transfer_owner::transfer_owner,
|
||||||
|
crate::api::repo::list_branches::list_branches,
|
||||||
|
crate::api::repo::create_branch::create_branch,
|
||||||
|
crate::api::repo::set_default_branch::set_default_branch,
|
||||||
|
crate::api::repo::set_branch_protection::set_branch_protection,
|
||||||
|
crate::api::repo::delete_branch::delete_branch,
|
||||||
|
crate::api::repo::list_tags::list_tags,
|
||||||
|
crate::api::repo::create_tag::create_tag,
|
||||||
|
crate::api::repo::delete_tag::delete_tag,
|
||||||
|
crate::api::repo::list_releases::list_releases,
|
||||||
|
crate::api::repo::create_release::create_release,
|
||||||
|
crate::api::repo::update_release::update_release,
|
||||||
|
crate::api::repo::delete_release::delete_release,
|
||||||
|
crate::api::repo::list_forks::list_forks,
|
||||||
|
crate::api::repo::fork_repo::fork_repo,
|
||||||
|
crate::api::repo::sync_fork::sync_fork,
|
||||||
|
crate::api::repo::star_repo::star_repo,
|
||||||
|
crate::api::repo::unstar_repo::unstar_repo,
|
||||||
|
crate::api::repo::list_stargazers::list_stargazers,
|
||||||
|
crate::api::repo::watch_repo::watch_repo,
|
||||||
|
crate::api::repo::unwatch_repo::unwatch_repo,
|
||||||
|
crate::api::repo::list_watchers::list_watchers,
|
||||||
|
crate::api::repo::list_members::list_members,
|
||||||
|
crate::api::repo::add_member::add_member,
|
||||||
|
crate::api::repo::update_member_role::update_member_role,
|
||||||
|
crate::api::repo::remove_member::remove_member,
|
||||||
|
crate::api::repo::leave_repo::leave_repo,
|
||||||
|
crate::api::repo::list_invitations::list_invitations,
|
||||||
|
crate::api::repo::create_invitation::create_invitation,
|
||||||
|
crate::api::repo::revoke_invitation::revoke_invitation,
|
||||||
|
crate::api::repo::accept_invitation::accept_invitation,
|
||||||
|
crate::api::repo::list_deploy_keys::list_deploy_keys,
|
||||||
|
crate::api::repo::add_deploy_key::add_deploy_key,
|
||||||
|
crate::api::repo::delete_deploy_key::delete_deploy_key,
|
||||||
|
crate::api::repo::list_webhooks::list_webhooks,
|
||||||
|
crate::api::repo::create_webhook::create_webhook,
|
||||||
|
crate::api::repo::update_webhook::update_webhook,
|
||||||
|
crate::api::repo::delete_webhook::delete_webhook,
|
||||||
|
crate::api::repo::list_protection_rules::list_protection_rules,
|
||||||
|
crate::api::repo::get_protection_rule::get_protection_rule,
|
||||||
|
crate::api::repo::match_protection::match_protection,
|
||||||
|
crate::api::repo::create_protection_rule::create_protection_rule,
|
||||||
|
crate::api::repo::update_protection_rule::update_protection_rule,
|
||||||
|
crate::api::repo::delete_protection_rule::delete_protection_rule,
|
||||||
|
crate::api::repo::check_branch_merge::check_branch_merge,
|
||||||
|
crate::api::repo::list_commit_statuses::list_commit_statuses,
|
||||||
|
crate::api::repo::create_commit_status::create_commit_status,
|
||||||
|
crate::api::repo::list_commit_comments::list_commit_comments,
|
||||||
|
crate::api::repo::create_commit_comment::create_commit_comment,
|
||||||
|
crate::api::repo::resolve_commit_comment::resolve_commit_comment,
|
||||||
|
crate::api::repo::get_stats::get_stats,
|
||||||
|
crate::api::repo::refresh_stats::refresh_stats,
|
||||||
),
|
),
|
||||||
components(schemas(
|
components(schemas(
|
||||||
ApiEmptyResponse,
|
ApiEmptyResponse,
|
||||||
@@ -193,6 +275,76 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara
|
|||||||
RequestApprovalParams,
|
RequestApprovalParams,
|
||||||
ReviewApprovalRequest,
|
ReviewApprovalRequest,
|
||||||
WorkspaceAuditLog,
|
WorkspaceAuditLog,
|
||||||
|
// Repos
|
||||||
|
ApiResponse<Repo>,
|
||||||
|
ApiResponse<Vec<Repo>>,
|
||||||
|
ApiResponse<RepoBranch>,
|
||||||
|
ApiResponse<Vec<RepoBranch>>,
|
||||||
|
ApiResponse<RepoTag>,
|
||||||
|
ApiResponse<Vec<RepoTag>>,
|
||||||
|
ApiResponse<RepoRelease>,
|
||||||
|
ApiResponse<Vec<RepoRelease>>,
|
||||||
|
ApiResponse<RepoFork>,
|
||||||
|
ApiResponse<Vec<RepoFork>>,
|
||||||
|
ApiResponse<RepoStar>,
|
||||||
|
ApiResponse<Vec<RepoStar>>,
|
||||||
|
ApiResponse<RepoWatch>,
|
||||||
|
ApiResponse<Vec<RepoWatch>>,
|
||||||
|
ApiResponse<RepoMember>,
|
||||||
|
ApiResponse<Vec<RepoMember>>,
|
||||||
|
ApiResponse<RepoInvitation>,
|
||||||
|
ApiResponse<Vec<RepoInvitation>>,
|
||||||
|
ApiResponse<RepoDeployKey>,
|
||||||
|
ApiResponse<Vec<RepoDeployKey>>,
|
||||||
|
ApiResponse<RepoWebhook>,
|
||||||
|
ApiResponse<Vec<RepoWebhook>>,
|
||||||
|
ApiResponse<BranchProtectionRule>,
|
||||||
|
ApiResponse<Vec<BranchProtectionRule>>,
|
||||||
|
ApiResponse<Option<BranchProtectionRule>>,
|
||||||
|
ApiResponse<BranchMergeCheck>,
|
||||||
|
ApiResponse<RepoCommitStatus>,
|
||||||
|
ApiResponse<Vec<RepoCommitStatus>>,
|
||||||
|
ApiResponse<RepoCommitComment>,
|
||||||
|
ApiResponse<Vec<RepoCommitComment>>,
|
||||||
|
ApiResponse<RepoStats>,
|
||||||
|
ApiResponse<String>,
|
||||||
|
Repo,
|
||||||
|
CreateRepoParams,
|
||||||
|
UpdateRepoParams,
|
||||||
|
TransferOwnerParams,
|
||||||
|
RepoBranch,
|
||||||
|
CreateBranchParams,
|
||||||
|
SetBranchProtectionParams,
|
||||||
|
RepoTag,
|
||||||
|
CreateTagParams,
|
||||||
|
RepoRelease,
|
||||||
|
CreateReleaseParams,
|
||||||
|
UpdateReleaseParams,
|
||||||
|
RepoFork,
|
||||||
|
ForkRepoParams,
|
||||||
|
RepoStar,
|
||||||
|
RepoWatch,
|
||||||
|
WatchParams,
|
||||||
|
RepoMember,
|
||||||
|
AddRepoMemberParams,
|
||||||
|
UpdateRepoMemberRoleParams,
|
||||||
|
RepoInvitation,
|
||||||
|
CreateRepoInvitationParams,
|
||||||
|
AcceptInvitationParams,
|
||||||
|
RepoDeployKey,
|
||||||
|
AddDeployKeyParams,
|
||||||
|
RepoWebhook,
|
||||||
|
CreateWebhookParams,
|
||||||
|
UpdateWebhookParams,
|
||||||
|
BranchProtectionRule,
|
||||||
|
CreateProtectionRuleParams,
|
||||||
|
UpdateProtectionRuleParams,
|
||||||
|
BranchMergeCheck,
|
||||||
|
RepoCommitStatus,
|
||||||
|
CreateCommitStatusParams,
|
||||||
|
RepoCommitComment,
|
||||||
|
CreateCommitCommentParams,
|
||||||
|
RepoStats,
|
||||||
))
|
))
|
||||||
)]
|
)]
|
||||||
pub struct OpenApiDoc;
|
pub struct OpenApiDoc;
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoInvitation;
|
||||||
|
use crate::service::AppService;
|
||||||
|
use crate::session::Session;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, ToSchema)]
|
||||||
|
pub struct AcceptInvitationParams {
|
||||||
|
/// Invitation token (received via email)
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accept a repository invitation
|
||||||
|
///
|
||||||
|
/// Accepts a pending repository invitation using the token received via email.
|
||||||
|
/// Requires authentication and a verified email address matching the invitation.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - User is added as a repository member with the invited role
|
||||||
|
/// - User is added to the workspace if not already a member
|
||||||
|
/// - Invitation is marked as accepted
|
||||||
|
///
|
||||||
|
/// Returns the accepted invitation with full metadata.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/repos/invitations/accept",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoAcceptInvitation",
|
||||||
|
request_body(
|
||||||
|
content = AcceptInvitationParams,
|
||||||
|
description = "Invitation acceptance parameters",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Invitation accepted successfully. User is now a member of the repository.", body = ApiResponse<RepoInvitation>),
|
||||||
|
(status = 400, description = "Invalid or expired token, or email doesn't match invitation", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Invitation not found", body = ApiErrorResponse),
|
||||||
|
(status = 409, description = "User is already a member of this repository", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn accept_invitation(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
params: web::Json<AcceptInvitationParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let invitation = service
|
||||||
|
.repo
|
||||||
|
.repo_accept_invitation(&session, ¶ms.token)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(invitation)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoDeployKey;
|
||||||
|
use crate::service::repo::deploy_keys::AddDeployKeyParams;
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a deploy key to a repository
|
||||||
|
///
|
||||||
|
/// Adds an SSH public key for automated deployments and CI/CD access to the repository.
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// - title: Human-readable name for the deploy key (1-100 characters)
|
||||||
|
/// - key: SSH public key in OpenSSH format (e.g., "ssh-rsa AAAA...")
|
||||||
|
/// - read_only: Whether the key has read-only access (default: true)
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Deploy key is added to the repository
|
||||||
|
/// - Key can be used for Git operations (clone, fetch, push if not read-only)
|
||||||
|
/// - Key fingerprint is calculated and stored
|
||||||
|
///
|
||||||
|
/// Returns the created deploy key with metadata including fingerprint.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/deploy-keys",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoAddDeployKey",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = AddDeployKeyParams,
|
||||||
|
description = "Deploy key addition parameters",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Deploy key added successfully. Returns the newly created deploy key with metadata.", body = ApiResponse<RepoDeployKey>),
|
||||||
|
(status = 400, description = "Invalid parameters: title too long or invalid SSH key format", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||||
|
(status = 409, description = "Deploy key with this fingerprint already exists", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn add_deploy_key(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<AddDeployKeyParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let key = service
|
||||||
|
.repo
|
||||||
|
.repo_add_deploy_key(&session, &path.workspace_name, &path.repo_name, params.into_inner())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Created().json(ApiResponse::new(key)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoMember;
|
||||||
|
use crate::service::repo::members::AddRepoMemberParams;
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a member to a repository
|
||||||
|
///
|
||||||
|
/// Grants a user access to the repository with a specific role.
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Requirements:
|
||||||
|
/// - User must exist in the system
|
||||||
|
/// - User must be a member of the workspace
|
||||||
|
/// - Role must be one of: "read", "write", "admin" (cannot assign "owner")
|
||||||
|
///
|
||||||
|
/// Returns the created member record with user information and role.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/members",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoAddMember",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = AddRepoMemberParams,
|
||||||
|
description = "Member addition parameters",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Member added successfully. Returns the newly created member record with user information and role.", body = ApiResponse<RepoMember>),
|
||||||
|
(status = 400, description = "Invalid parameters: invalid role or user doesn't exist", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or user not found", body = ApiErrorResponse),
|
||||||
|
(status = 409, description = "User is already a member of this repository", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn add_member(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<AddRepoMemberParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let member = service
|
||||||
|
.repo
|
||||||
|
.repo_add_member(&session, &path.workspace_name, &path.repo_name, params.into_inner())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Created().json(ApiResponse::new(member)))
|
||||||
|
}
|
||||||
@@ -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::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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Archive a repository
|
||||||
|
///
|
||||||
|
/// Marks a repository as archived, making it read-only. All write operations (push, create issues, etc.) are disabled.
|
||||||
|
/// Requires Owner role in the repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Repository status changes to "archived"
|
||||||
|
/// - All write operations are blocked
|
||||||
|
/// - Repository remains visible based on its visibility setting
|
||||||
|
/// - Can be unarchived later by repository owners
|
||||||
|
///
|
||||||
|
/// Returns success message on completion.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/archive",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoArchive",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Repository archived successfully. Repository is now read-only.", body = ApiResponse<String>),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Owner role)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||||
|
(status = 409, description = "Repository is already archived", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn archive(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_archive(&session, &path.workspace_name, &path.repo_name)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Repository archived".to_string())))
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::service::repo::protection::BranchMergeCheck;
|
||||||
|
use crate::service::AppService;
|
||||||
|
use crate::session::Session;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, IntoParams)]
|
||||||
|
pub struct PathParams {
|
||||||
|
pub workspace_name: String,
|
||||||
|
pub repo_name: String,
|
||||||
|
pub target_branch: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, IntoParams)]
|
||||||
|
pub struct QueryParams {
|
||||||
|
pub pr_number: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a branch meets merge requirements
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches/{target_branch}/merge-check",
|
||||||
|
tag = "Repos",
|
||||||
|
params(PathParams, QueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Merge check completed successfully", body = ApiResponse<BranchMergeCheck>),
|
||||||
|
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Forbidden", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository or pull request not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(("session_cookie" = []))
|
||||||
|
)]
|
||||||
|
pub async fn check_branch_merge(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
query: web::Query<QueryParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let check = service
|
||||||
|
.repo
|
||||||
|
.repo_check_branch_merge_allowed(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
&path.target_branch,
|
||||||
|
query.pr_number,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(check)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::Repo;
|
||||||
|
use crate::service::repo::core::CreateRepoParams;
|
||||||
|
use crate::service::AppService;
|
||||||
|
use crate::session::Session;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, IntoParams)]
|
||||||
|
pub struct PathParams {
|
||||||
|
/// Workspace name (unique identifier)
|
||||||
|
pub workspace_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new repository
|
||||||
|
///
|
||||||
|
/// Creates a new Git repository within the specified workspace. The authenticated user becomes the repository owner.
|
||||||
|
///
|
||||||
|
/// Requirements:
|
||||||
|
/// - User must have at least Member role in the workspace
|
||||||
|
/// - Repository name must be unique within the workspace
|
||||||
|
/// - Name must be 1-100 characters, alphanumeric, hyphens, underscores, and dots allowed
|
||||||
|
///
|
||||||
|
/// Optional parameters:
|
||||||
|
/// - description: Repository description (max 500 characters)
|
||||||
|
/// - visibility: "public", "private", or "internal" (defaults to workspace setting)
|
||||||
|
/// - default_branch: Default branch name (defaults to "main")
|
||||||
|
///
|
||||||
|
/// Returns the created repository with full metadata.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoCreate",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = CreateRepoParams,
|
||||||
|
description = "Repository creation parameters",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Repository created successfully. Returns the newly created repository with full metadata.", body = ApiResponse<Repo>),
|
||||||
|
(status = 400, description = "Invalid parameters: name too long, invalid characters, or invalid visibility", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions to create repositories in this workspace", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Workspace not found", body = ApiErrorResponse),
|
||||||
|
(status = 409, description = "Repository with this name already exists in the workspace", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error or Git initialization failed", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn create(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<CreateRepoParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let repo = service
|
||||||
|
.repo
|
||||||
|
.repo_create(&session, &path.workspace_name, params.into_inner())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Created().json(ApiResponse::new(repo)))
|
||||||
|
}
|
||||||
@@ -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::repos::RepoBranch;
|
||||||
|
use crate::service::repo::branches::CreateBranchParams;
|
||||||
|
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 new branch
|
||||||
|
///
|
||||||
|
/// Creates a new branch in the repository based on an existing commit or branch.
|
||||||
|
/// Requires Write role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// - name: Branch name (1-100 characters, alphanumeric, hyphens, underscores, dots, slashes allowed)
|
||||||
|
/// - from: Source branch name or commit SHA to branch from (defaults to default branch)
|
||||||
|
///
|
||||||
|
/// Returns the created branch with metadata including the initial commit SHA.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoCreateBranch",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = CreateBranchParams,
|
||||||
|
description = "Branch creation parameters",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Branch created successfully. Returns the newly created branch with metadata.", body = ApiResponse<RepoBranch>),
|
||||||
|
(status = 400, description = "Invalid parameters: name too long, invalid characters, or source branch/commit doesn't exist", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Write role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or source branch/commit not found", body = ApiErrorResponse),
|
||||||
|
(status = 409, description = "Branch with this name already exists", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error or Git operation failed", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn create_branch(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<CreateBranchParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let branch = service
|
||||||
|
.repo
|
||||||
|
.repo_create_branch(&session, &path.workspace_name, &path.repo_name, params.into_inner())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Created().json(ApiResponse::new(branch)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoCommitComment;
|
||||||
|
use crate::service::repo::commit_status::CreateCommitCommentParams;
|
||||||
|
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 commit comment
|
||||||
|
///
|
||||||
|
/// Creates a new comment on a specific commit. Comments can be general or inline (attached to a specific file and line).
|
||||||
|
/// Requires Write role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// - commit_sha: Commit SHA to comment on (must exist in repository)
|
||||||
|
/// - body: Comment body in markdown format (1-10000 characters)
|
||||||
|
/// - path: File path for inline comments (optional)
|
||||||
|
/// - line: Line number for inline comments (optional, requires path)
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Comment is attached to the commit
|
||||||
|
/// - Comment author receives notifications for replies
|
||||||
|
/// - Inline comments appear in code review interfaces
|
||||||
|
///
|
||||||
|
/// Returns the created comment with metadata including ID and timestamps.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/comments",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoCreateCommitComment",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = CreateCommitCommentParams,
|
||||||
|
description = "Commit comment creation parameters",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Commit comment created successfully. Returns the newly created comment with metadata.", body = ApiResponse<RepoCommitComment>),
|
||||||
|
(status = 400, description = "Invalid parameters: body too long, line without path, or commit doesn't exist", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Write role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, commit, or file path not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn create_commit_comment(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<CreateCommitCommentParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let comment = service
|
||||||
|
.repo
|
||||||
|
.repo_create_commit_comment(&session, &path.workspace_name, &path.repo_name, params.into_inner())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Created().json(ApiResponse::new(comment)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoCommitStatus;
|
||||||
|
use crate::service::repo::commit_status::CreateCommitStatusParams;
|
||||||
|
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 commit status
|
||||||
|
///
|
||||||
|
/// Creates a new status check for a specific commit, typically used by CI/CD systems.
|
||||||
|
/// Requires Write role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// - commit_sha: Commit SHA to attach the status to (must exist in repository)
|
||||||
|
/// - state: Status state ("pending", "success", "failure", "error")
|
||||||
|
/// - context: Status context name (e.g., "ci/build", "ci/test") - must be unique per commit
|
||||||
|
/// - description: Human-readable description of the status (optional, max 500 characters)
|
||||||
|
/// - target_url: URL with detailed information about the status (optional)
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Status is attached to the commit
|
||||||
|
/// - Can be used by branch protection rules to enforce status checks
|
||||||
|
/// - Multiple statuses can exist for the same commit with different contexts
|
||||||
|
///
|
||||||
|
/// Returns the created status with metadata including ID and timestamps.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/statuses",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoCreateCommitStatus",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = CreateCommitStatusParams,
|
||||||
|
description = "Commit status creation parameters",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Commit status created successfully. Returns the newly created status with metadata.", body = ApiResponse<RepoCommitStatus>),
|
||||||
|
(status = 400, description = "Invalid parameters: invalid state, context too long, or commit doesn't exist", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Write role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or commit not found", body = ApiErrorResponse),
|
||||||
|
(status = 409, description = "Status with this context already exists for this commit (use update instead)", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn create_commit_status(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<CreateCommitStatusParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let status = service
|
||||||
|
.repo
|
||||||
|
.repo_create_commit_status(&session, &path.workspace_name, &path.repo_name, params.into_inner())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Created().json(ApiResponse::new(status)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoInvitation;
|
||||||
|
use crate::service::repo::invitations::CreateRepoInvitationParams;
|
||||||
|
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 repository invitation
|
||||||
|
///
|
||||||
|
/// Sends an invitation email to a user to join the repository with a specific role.
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// - email: Email address of the invitee (must be a valid email)
|
||||||
|
/// - role: Role to assign when invitation is accepted ("read", "write", "admin")
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Invitation email is sent to the invitee
|
||||||
|
/// - Invitation expires after 7 days
|
||||||
|
/// - Invitee must have a verified email to accept
|
||||||
|
///
|
||||||
|
/// Returns the created invitation with metadata including expiration date.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/invitations",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoCreateInvitation",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = CreateRepoInvitationParams,
|
||||||
|
description = "Invitation creation parameters",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Invitation created successfully. Returns the newly created invitation with metadata.", body = ApiResponse<RepoInvitation>),
|
||||||
|
(status = 400, description = "Invalid parameters: invalid email format or invalid role", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||||
|
(status = 409, description = "User is already a member or has a pending invitation", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error or email sending failed", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn create_invitation(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<CreateRepoInvitationParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let invitation = service
|
||||||
|
.repo
|
||||||
|
.repo_create_invitation(&session, &path.workspace_name, &path.repo_name, params.into_inner())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Created().json(ApiResponse::new(invitation)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::BranchProtectionRule;
|
||||||
|
use crate::service::repo::protection::CreateProtectionRuleParams;
|
||||||
|
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 branch protection rule
|
||||||
|
///
|
||||||
|
/// Creates a new branch protection rule that enforces policies on matching branches.
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// - pattern: Branch name pattern (supports wildcards like "feature/*", "release/**")
|
||||||
|
/// - required_approvals: Number of required approvals before merging (0-10)
|
||||||
|
/// - require_status_checks: Whether status checks must pass
|
||||||
|
/// - required_status_checks: List of required status check contexts
|
||||||
|
/// - restrict_pushes: Restrict who can push to matching branches
|
||||||
|
/// - allow_force_pushes: Allow force pushes (only if restrict_pushes is false)
|
||||||
|
/// - allow_deletions: Allow branch deletion (only if restrict_pushes is false)
|
||||||
|
///
|
||||||
|
/// Returns the created protection rule with full configuration.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/protection-rules",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoCreateProtectionRule",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = CreateProtectionRuleParams,
|
||||||
|
description = "Protection rule creation parameters",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Protection rule created successfully. Returns the newly created protection rule with full configuration.", body = ApiResponse<BranchProtectionRule>),
|
||||||
|
(status = 400, description = "Invalid parameters: invalid pattern, negative approvals count, or conflicting settings", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||||
|
(status = 409, description = "Protection rule with this pattern already exists", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn create_protection_rule(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<CreateProtectionRuleParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let rule = service
|
||||||
|
.repo
|
||||||
|
.repo_create_protection_rule(&session, &path.workspace_name, &path.repo_name, params.into_inner())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Created().json(ApiResponse::new(rule)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoRelease;
|
||||||
|
use crate::service::repo::releases::CreateReleaseParams;
|
||||||
|
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 new release
|
||||||
|
///
|
||||||
|
/// Creates a new release in the repository, optionally creating a tag if it doesn't exist.
|
||||||
|
/// Requires Write role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// - tag_name: Tag name for the release (will be created if it doesn't exist)
|
||||||
|
/// - name: Release name/title (required)
|
||||||
|
/// - body: Release notes in markdown format (optional, max 10000 characters)
|
||||||
|
/// - draft: Whether this is a draft release (default: false)
|
||||||
|
/// - prerelease: Whether this is a prerelease (default: false)
|
||||||
|
/// - target_commitish: Commit SHA or branch name to tag (defaults to default branch)
|
||||||
|
///
|
||||||
|
/// Returns the created release with metadata including download URLs for assets.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/releases",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoCreateRelease",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = CreateReleaseParams,
|
||||||
|
description = "Release creation parameters",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Release created successfully. Returns the newly created release with metadata.", body = ApiResponse<RepoRelease>),
|
||||||
|
(status = 400, description = "Invalid parameters: name too long, invalid tag name, or target commit doesn't exist", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Write role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or target commit not found", body = ApiErrorResponse),
|
||||||
|
(status = 409, description = "Release with this tag already exists", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error or Git operation failed", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn create_release(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<CreateReleaseParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let release = service
|
||||||
|
.repo
|
||||||
|
.repo_create_release(&session, &path.workspace_name, &path.repo_name, params.into_inner())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Created().json(ApiResponse::new(release)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoTag;
|
||||||
|
use crate::service::repo::tags::CreateTagParams;
|
||||||
|
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 new tag
|
||||||
|
///
|
||||||
|
/// Creates a new tag in the repository pointing to a specific commit or branch.
|
||||||
|
/// Requires Write role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// - name: Tag name (1-100 characters, typically follows semantic versioning like v1.0.0)
|
||||||
|
/// - target: Commit SHA or branch name to tag (defaults to HEAD of default branch)
|
||||||
|
/// - message: Optional tag message for annotated tags
|
||||||
|
///
|
||||||
|
/// Returns the created tag with metadata including the commit SHA.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/tags",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoCreateTag",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = CreateTagParams,
|
||||||
|
description = "Tag creation parameters",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Tag created successfully. Returns the newly created tag with metadata.", body = ApiResponse<RepoTag>),
|
||||||
|
(status = 400, description = "Invalid parameters: name too long, invalid characters, or target commit/branch doesn't exist", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Write role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or target commit/branch not found", body = ApiErrorResponse),
|
||||||
|
(status = 409, description = "Tag with this name already exists", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error or Git operation failed", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn create_tag(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<CreateTagParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let tag = service
|
||||||
|
.repo
|
||||||
|
.repo_create_tag(&session, &path.workspace_name, &path.repo_name, params.into_inner())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Created().json(ApiResponse::new(tag)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoWebhook;
|
||||||
|
use crate::service::repo::webhooks::CreateWebhookParams;
|
||||||
|
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 webhook in a repository
|
||||||
|
///
|
||||||
|
/// Creates a new webhook that receives HTTP POST notifications for repository events.
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// - url: Webhook endpoint URL (must be HTTPS in production)
|
||||||
|
/// - events: List of events to subscribe to (e.g., "push", "pull_request", "issue")
|
||||||
|
/// - secret: Optional secret for webhook signature verification
|
||||||
|
/// - active: Whether the webhook is active (default: true)
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Webhook is created and starts receiving events
|
||||||
|
/// - Webhook deliveries are logged and can be retried on failure
|
||||||
|
///
|
||||||
|
/// Returns the created webhook with metadata including ID and configuration.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/webhooks",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoCreateWebhook",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = CreateWebhookParams,
|
||||||
|
description = "Webhook creation parameters",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Webhook created successfully. Returns the newly created webhook with metadata.", body = ApiResponse<RepoWebhook>),
|
||||||
|
(status = 400, description = "Invalid parameters: invalid URL format or empty events list", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin 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_webhook(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<CreateWebhookParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let webhook = service
|
||||||
|
.repo
|
||||||
|
.repo_create_webhook(&session, &path.workspace_name, &path.repo_name, params.into_inner())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Created().json(ApiResponse::new(webhook)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a repository
|
||||||
|
///
|
||||||
|
/// Permanently deletes a repository and all associated data including:
|
||||||
|
/// - Git repository and all commits
|
||||||
|
/// - Branches, tags, and releases
|
||||||
|
/// - Issues, pull requests, and comments
|
||||||
|
/// - Webhooks, deploy keys, and protection rules
|
||||||
|
/// - Stars, watches, and forks
|
||||||
|
///
|
||||||
|
/// Requires Owner role in the repository. This action is irreversible.
|
||||||
|
///
|
||||||
|
/// Returns success message on completion.
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoDelete",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Repository deleted successfully. All repository data has been permanently removed.", body = ApiResponse<String>),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Owner role)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error or Git deletion failed", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn delete(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_delete(&session, &path.workspace_name, &path.repo_name)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Repository deleted".to_string())))
|
||||||
|
}
|
||||||
@@ -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::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,
|
||||||
|
/// Branch ID (UUID)
|
||||||
|
pub branch_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a branch
|
||||||
|
///
|
||||||
|
/// Permanently deletes a branch from the repository. The default branch cannot be deleted.
|
||||||
|
/// Requires Write role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Branch is permanently removed from the repository
|
||||||
|
/// - All commits exclusive to this branch remain accessible via their SHA
|
||||||
|
/// - Open pull requests targeting this branch will be closed
|
||||||
|
///
|
||||||
|
/// Returns success message on completion.
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches/{branch_id}",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoDeleteBranch",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Branch deleted successfully.", body = ApiResponse<String>),
|
||||||
|
(status = 400, description = "Cannot delete the default branch", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Write role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or branch not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error or Git operation failed", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn delete_branch(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_delete_branch(&session, &path.workspace_name, &path.repo_name, path.branch_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Branch deleted successfully".to_string())))
|
||||||
|
}
|
||||||
@@ -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::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,
|
||||||
|
/// Deploy key ID (UUID)
|
||||||
|
pub key_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a deploy key from a repository
|
||||||
|
///
|
||||||
|
/// Removes an SSH deploy key from the repository, revoking its access.
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Deploy key is permanently removed from the repository
|
||||||
|
/// - Key can no longer be used for Git operations
|
||||||
|
/// - Automated systems using this key will lose access
|
||||||
|
///
|
||||||
|
/// Returns success message on completion.
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/deploy-keys/{key_id}",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoDeleteDeployKey",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Deploy key deleted successfully.", body = ApiResponse<String>),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or deploy key not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn delete_deploy_key(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_delete_deploy_key(&session, &path.workspace_name, &path.repo_name, path.key_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Deploy key deleted successfully".to_string())))
|
||||||
|
}
|
||||||
@@ -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::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,
|
||||||
|
/// Protection rule ID (UUID)
|
||||||
|
pub rule_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a branch protection rule
|
||||||
|
///
|
||||||
|
/// Permanently removes a branch protection rule from the repository.
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Protection rule is permanently removed
|
||||||
|
/// - Branches matching the pattern are no longer protected by this rule
|
||||||
|
/// - Pushes and merges to matching branches are no longer restricted
|
||||||
|
///
|
||||||
|
/// Returns success message on completion.
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/protection-rules/{rule_id}",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoDeleteProtectionRule",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Protection rule deleted successfully.", body = ApiResponse<String>),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or protection rule not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn delete_protection_rule(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_delete_protection_rule(&session, &path.workspace_name, &path.repo_name, path.rule_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Protection rule deleted successfully".to_string())))
|
||||||
|
}
|
||||||
@@ -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::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,
|
||||||
|
/// Release ID (UUID)
|
||||||
|
pub release_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a release
|
||||||
|
///
|
||||||
|
/// Permanently deletes a release from the repository. The associated tag and commits are not deleted.
|
||||||
|
/// Requires Write role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Release metadata is permanently removed
|
||||||
|
/// - Release assets are deleted
|
||||||
|
/// - The associated tag remains in the repository
|
||||||
|
/// - The tagged commits remain in the repository history
|
||||||
|
///
|
||||||
|
/// Returns success message on completion.
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/releases/{release_id}",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoDeleteRelease",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Release deleted successfully. The release and its assets have been permanently removed.", body = ApiResponse<String>),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Write role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or release not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn delete_release(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_delete_release(&session, &path.workspace_name, &path.repo_name, path.release_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Release deleted successfully".to_string())))
|
||||||
|
}
|
||||||
@@ -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::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,
|
||||||
|
/// Tag ID (UUID)
|
||||||
|
pub tag_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a tag
|
||||||
|
///
|
||||||
|
/// Permanently deletes a tag from the repository. The tagged commit remains accessible via its SHA.
|
||||||
|
/// Requires Write role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Tag is permanently removed from the repository
|
||||||
|
/// - The tagged commit remains in the repository history
|
||||||
|
/// - Releases associated with this tag are not automatically deleted
|
||||||
|
///
|
||||||
|
/// Returns success message on completion.
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/tags/{tag_id}",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoDeleteTag",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Tag deleted successfully.", body = ApiResponse<String>),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Write role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or tag not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error or Git operation failed", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn delete_tag(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_delete_tag(&session, &path.workspace_name, &path.repo_name, path.tag_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Tag deleted successfully".to_string())))
|
||||||
|
}
|
||||||
@@ -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::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,
|
||||||
|
/// Webhook ID (UUID)
|
||||||
|
pub webhook_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a webhook from a repository
|
||||||
|
///
|
||||||
|
/// Permanently removes a webhook from the repository, stopping all event notifications.
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Webhook is permanently removed from the repository
|
||||||
|
/// - Webhook stops receiving event notifications immediately
|
||||||
|
/// - Webhook delivery history is deleted
|
||||||
|
///
|
||||||
|
/// Returns success message on completion.
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/webhooks/{webhook_id}",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoDeleteWebhook",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Webhook deleted successfully.", body = ApiResponse<String>),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or webhook not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn delete_webhook(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_delete_webhook(&session, &path.workspace_name, &path.repo_name, path.webhook_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Webhook deleted successfully".to_string())))
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::Repo;
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::service::repo::fork::ForkRepoParams;
|
||||||
|
|
||||||
|
/// Fork a repository
|
||||||
|
///
|
||||||
|
/// Creates a copy of the repository in the specified workspace or the current user's workspace.
|
||||||
|
/// Requires read access to the source repository and write access to the target workspace.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Creates a new repository with all branches, tags, and commit history
|
||||||
|
/// - Establishes a parent-child relationship between source and fork
|
||||||
|
/// - Fork is initially set to private visibility
|
||||||
|
/// - Current user becomes the owner of the fork
|
||||||
|
///
|
||||||
|
/// Returns the created fork repository with full metadata.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/fork",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoFork",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = ForkRepoParams,
|
||||||
|
description = "Fork parameters (all fields optional)",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Repository forked successfully. Returns the newly created fork with full metadata.", body = ApiResponse<Repo>),
|
||||||
|
(status = 400, description = "Invalid parameters: target name conflicts or invalid characters", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions to fork or create in target workspace", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Source repository or target workspace not found", body = ApiErrorResponse),
|
||||||
|
(status = 409, description = "Fork with this name already exists in target workspace", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error or Git operation failed", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn fork_repo(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<ForkRepoParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let repo = service
|
||||||
|
.repo
|
||||||
|
.repo_fork(&session, &path.workspace_name, &path.repo_name, params.into_inner())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Created().json(ApiResponse::new(repo)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::Repo;
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a specific repository
|
||||||
|
///
|
||||||
|
/// Returns detailed information about a specific repository identified by workspace and repository name.
|
||||||
|
/// Access is determined by repository visibility:
|
||||||
|
/// - Public repositories: accessible to all authenticated users
|
||||||
|
/// - Private repositories: accessible only to repository members and workspace owners
|
||||||
|
/// - Internal repositories: accessible to all workspace members
|
||||||
|
///
|
||||||
|
/// Returns 404 if the repository doesn't exist or the user lacks access permissions.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoGet",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Repository retrieved successfully. Returns complete repository metadata including visibility, default branch, creation date, and statistics.", body = ApiResponse<Repo>),
|
||||||
|
(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 not found or access denied", 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 repo = service
|
||||||
|
.repo
|
||||||
|
.repo_get(&session, &path.workspace_name, &path.repo_name)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(repo)))
|
||||||
|
}
|
||||||
@@ -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::repos::BranchProtectionRule;
|
||||||
|
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,
|
||||||
|
/// Protection rule ID (UUID)
|
||||||
|
pub rule_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a specific branch protection rule
|
||||||
|
///
|
||||||
|
/// Returns detailed information about a specific branch protection rule.
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
///
|
||||||
|
/// Returns the complete protection rule with all configuration details including:
|
||||||
|
/// - Branch name pattern
|
||||||
|
/// - Required approvals and status checks
|
||||||
|
/// - Push and deletion restrictions
|
||||||
|
/// - Creator information and timestamps
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/protection-rules/{rule_id}",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoGetProtectionRule",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Protection rule retrieved successfully. Returns complete protection rule with all configuration details.", body = ApiResponse<BranchProtectionRule>),
|
||||||
|
(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 protection rule not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn get_protection_rule(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let rule = service
|
||||||
|
.repo
|
||||||
|
.repo_get_protection_rule(&session, &path.workspace_name, &path.repo_name, path.rule_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(rule)))
|
||||||
|
}
|
||||||
@@ -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::repos::RepoStats;
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get repository statistics
|
||||||
|
///
|
||||||
|
/// Returns comprehensive statistics for a repository including:
|
||||||
|
/// - Star count and watcher count
|
||||||
|
/// - Fork count
|
||||||
|
/// - Branch and tag counts
|
||||||
|
/// - Commit count
|
||||||
|
/// - Release count
|
||||||
|
/// - Open issues and pull requests count
|
||||||
|
/// - Storage size and bandwidth usage
|
||||||
|
/// - Last push timestamp
|
||||||
|
///
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/stats",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoGetStats",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Repository statistics retrieved successfully. Returns comprehensive repository metrics.", body = ApiResponse<RepoStats>),
|
||||||
|
(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 get_stats(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let stats = service
|
||||||
|
.repo
|
||||||
|
.repo_stats(&session, &path.workspace_name, &path.repo_name)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(stats)))
|
||||||
|
}
|
||||||
@@ -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::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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Leave a repository
|
||||||
|
///
|
||||||
|
/// Removes the current user's access to the repository.
|
||||||
|
/// Requires the user to be a member of the repository.
|
||||||
|
///
|
||||||
|
/// Restrictions:
|
||||||
|
/// - Repository owner cannot leave (use transfer_owner instead)
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - User loses all access to the repository
|
||||||
|
/// - User is removed from all repository activities
|
||||||
|
///
|
||||||
|
/// Returns success message on completion.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/leave",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoLeave",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Left repository successfully.", body = ApiResponse<String>),
|
||||||
|
(status = 400, description = "Repository owner cannot leave the repository", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", 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 leave_repo(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_leave(&session, &path.workspace_name, &path.repo_name)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Left repository successfully".to_string())))
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::Repo;
|
||||||
|
use crate::service::AppService;
|
||||||
|
use crate::session::Session;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, IntoParams)]
|
||||||
|
pub struct PathParams {
|
||||||
|
/// Workspace name (unique identifier)
|
||||||
|
pub workspace_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, IntoParams)]
|
||||||
|
pub struct QueryParams {
|
||||||
|
/// Maximum number of repositories to return (default: 50, max: 100)
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
/// Number of repositories to skip for pagination (default: 0)
|
||||||
|
pub offset: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List repositories in a workspace
|
||||||
|
///
|
||||||
|
/// Returns a paginated list of repositories that the current user has access to within the specified workspace.
|
||||||
|
/// Access is determined by:
|
||||||
|
/// - Public repositories: accessible to all authenticated users
|
||||||
|
/// - Private repositories: accessible only to repository members and workspace owners
|
||||||
|
/// - Internal repositories: accessible to all workspace members
|
||||||
|
///
|
||||||
|
/// The results are sorted by creation date in descending order (newest first).
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoList",
|
||||||
|
params(
|
||||||
|
PathParams,
|
||||||
|
QueryParams,
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Repositories listed successfully. Returns an array of repository objects with metadata.", body = ApiResponse<Vec<Repo>>),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions to access this workspace", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "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 repos = service
|
||||||
|
.repo
|
||||||
|
.repo_list(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
query.limit.unwrap_or(50),
|
||||||
|
query.offset.unwrap_or(0),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(repos)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoBranch;
|
||||||
|
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 {
|
||||||
|
/// Maximum number of branches to return (default: 50, max: 100)
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
/// Number of branches to skip for pagination (default: 0)
|
||||||
|
pub offset: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List branches in a repository
|
||||||
|
///
|
||||||
|
/// Returns a paginated list of all branches in the repository, sorted by name alphabetically.
|
||||||
|
/// Includes branch metadata such as:
|
||||||
|
/// - Branch name and commit SHA
|
||||||
|
/// - Protected status
|
||||||
|
/// - Default branch flag
|
||||||
|
/// - Last push information
|
||||||
|
///
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoListBranches",
|
||||||
|
params(PathParams, QueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Branches listed successfully. Returns an array of branch objects with metadata.", body = ApiResponse<Vec<RepoBranch>>),
|
||||||
|
(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_branches(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
query: web::Query<QueryParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let branches = service
|
||||||
|
.repo
|
||||||
|
.repo_branches(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
query.limit.unwrap_or(50),
|
||||||
|
query.offset.unwrap_or(0),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(branches)))
|
||||||
|
}
|
||||||
@@ -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::repos::RepoCommitComment;
|
||||||
|
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,
|
||||||
|
/// Push commit ID (UUID)
|
||||||
|
pub push_commit_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, IntoParams)]
|
||||||
|
pub struct QueryParams {
|
||||||
|
/// Maximum number of comments to return (default: 50, max: 100)
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
/// Number of comments to skip for pagination (default: 0)
|
||||||
|
pub offset: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List commit comments
|
||||||
|
///
|
||||||
|
/// Returns a paginated list of all comments on a specific commit, sorted by creation date (newest first).
|
||||||
|
/// Includes comment metadata such as:
|
||||||
|
/// - Comment body in markdown format
|
||||||
|
/// - Author information
|
||||||
|
/// - File path and line number (for inline comments)
|
||||||
|
/// - Resolved status
|
||||||
|
/// - Creation and update timestamps
|
||||||
|
///
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/commits/{commit_sha}/comments",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoListCommitComments",
|
||||||
|
params(PathParams, QueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Commit comments listed successfully. Returns an array of comment objects with metadata.", body = ApiResponse<Vec<RepoCommitComment>>),
|
||||||
|
(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 commit not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn list_commit_comments(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
query: web::Query<QueryParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let comments = service
|
||||||
|
.repo
|
||||||
|
.repo_commit_comments(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
path.push_commit_id,
|
||||||
|
query.limit.unwrap_or(50),
|
||||||
|
query.offset.unwrap_or(0),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(comments)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoCommitStatus;
|
||||||
|
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,
|
||||||
|
/// Push commit ID (UUID)
|
||||||
|
pub push_commit_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, IntoParams)]
|
||||||
|
pub struct QueryParams {
|
||||||
|
/// Maximum number of statuses to return (default: 50, max: 100)
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
/// Number of statuses to skip for pagination (default: 0)
|
||||||
|
pub offset: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List commit statuses
|
||||||
|
///
|
||||||
|
/// Returns a paginated list of all status checks for a specific commit, sorted by creation date (newest first).
|
||||||
|
/// Includes status metadata such as:
|
||||||
|
/// - Status state (pending, success, failure, error)
|
||||||
|
/// - Context name (e.g., "ci/build", "ci/test")
|
||||||
|
/// - Description and target URL
|
||||||
|
/// - Creator information
|
||||||
|
///
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/commits/{commit_sha}/statuses",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoListCommitStatuses",
|
||||||
|
params(PathParams, QueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Commit statuses listed successfully. Returns an array of status objects with metadata.", body = ApiResponse<Vec<RepoCommitStatus>>),
|
||||||
|
(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 commit not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn list_commit_statuses(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
query: web::Query<QueryParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let statuses = service
|
||||||
|
.repo
|
||||||
|
.repo_commit_statuses(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
path.push_commit_id,
|
||||||
|
query.limit.unwrap_or(50),
|
||||||
|
query.offset.unwrap_or(0),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(statuses)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoDeployKey;
|
||||||
|
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 {
|
||||||
|
/// Maximum number of deploy keys to return (default: 50, max: 100)
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
/// Number of deploy keys to skip for pagination (default: 0)
|
||||||
|
pub offset: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List deploy keys in a repository
|
||||||
|
///
|
||||||
|
/// Returns a paginated list of all deploy keys in the repository, sorted by creation date (newest first).
|
||||||
|
/// Includes deploy key metadata such as:
|
||||||
|
/// - Key title and fingerprint
|
||||||
|
/// - Read-only status
|
||||||
|
/// - Creator information
|
||||||
|
/// - Creation date and last used date
|
||||||
|
///
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/deploy-keys",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoListDeployKeys",
|
||||||
|
params(PathParams, QueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Deploy keys listed successfully. Returns an array of deploy key objects with metadata.", body = ApiResponse<Vec<RepoDeployKey>>),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin 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 list_deploy_keys(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
query: web::Query<QueryParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let keys = service
|
||||||
|
.repo
|
||||||
|
.repo_deploy_keys(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
query.limit.unwrap_or(50),
|
||||||
|
query.offset.unwrap_or(0),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(keys)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoFork;
|
||||||
|
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 {
|
||||||
|
/// Maximum number of forks to return (default: 50, max: 100)
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
/// Number of forks to skip for pagination (default: 0)
|
||||||
|
pub offset: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List forks of a repository
|
||||||
|
///
|
||||||
|
/// Returns a paginated list of all repositories that have been forked from this repository.
|
||||||
|
/// Includes fork metadata such as:
|
||||||
|
/// - Fork repository information
|
||||||
|
/// - Fork owner and workspace
|
||||||
|
/// - Fork creation date
|
||||||
|
///
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/forks",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoListForks",
|
||||||
|
params(PathParams, QueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Forks listed successfully. Returns an array of fork objects with metadata.", body = ApiResponse<Vec<RepoFork>>),
|
||||||
|
(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_forks(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
query: web::Query<QueryParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let forks = service
|
||||||
|
.repo
|
||||||
|
.repo_forks(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
query.limit.unwrap_or(50),
|
||||||
|
query.offset.unwrap_or(0),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(forks)))
|
||||||
|
}
|
||||||
@@ -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::repos::RepoInvitation;
|
||||||
|
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 {
|
||||||
|
/// Maximum number of invitations to return (default: 50, max: 100)
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
/// Number of invitations to skip for pagination (default: 0)
|
||||||
|
pub offset: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List repository invitations
|
||||||
|
///
|
||||||
|
/// Returns a paginated list of all pending invitations for the repository, sorted by creation date (newest first).
|
||||||
|
/// Includes invitation metadata such as:
|
||||||
|
/// - Invitee email address
|
||||||
|
/// - Invited role
|
||||||
|
/// - Inviter information
|
||||||
|
/// - Expiration date
|
||||||
|
/// - Creation date
|
||||||
|
///
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/invitations",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoListInvitations",
|
||||||
|
params(PathParams, QueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Invitations listed successfully. Returns an array of invitation objects with metadata.", body = ApiResponse<Vec<RepoInvitation>>),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin 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 list_invitations(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
query: web::Query<QueryParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let invitations = service
|
||||||
|
.repo
|
||||||
|
.repo_invitations(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
query.limit.unwrap_or(50),
|
||||||
|
query.offset.unwrap_or(0),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(invitations)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoMember;
|
||||||
|
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 {
|
||||||
|
/// Maximum number of members to return (default: 50, max: 100)
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
/// Number of members to skip for pagination (default: 0)
|
||||||
|
pub offset: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List repository members
|
||||||
|
///
|
||||||
|
/// Returns a paginated list of all members with access to the repository, sorted by join date (oldest first).
|
||||||
|
/// Includes member metadata such as:
|
||||||
|
/// - User information (ID, username, display name)
|
||||||
|
/// - Role (owner, admin, write, read)
|
||||||
|
/// - Join date and last activity
|
||||||
|
///
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/members",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoListMembers",
|
||||||
|
params(PathParams, QueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Members listed successfully. Returns an array of member objects with user information and roles.", body = ApiResponse<Vec<RepoMember>>),
|
||||||
|
(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_members(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
query: web::Query<QueryParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let members = service
|
||||||
|
.repo
|
||||||
|
.repo_members(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
query.limit.unwrap_or(50),
|
||||||
|
query.offset.unwrap_or(0),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(members)))
|
||||||
|
}
|
||||||
@@ -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::repos::BranchProtectionRule;
|
||||||
|
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 {
|
||||||
|
/// Maximum number of protection rules to return (default: 50, max: 100)
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
/// Number of protection rules to skip for pagination (default: 0)
|
||||||
|
pub offset: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List branch protection rules in a repository
|
||||||
|
///
|
||||||
|
/// Returns a paginated list of all branch protection rules in the repository, sorted by pattern alphabetically.
|
||||||
|
/// Includes protection rule metadata such as:
|
||||||
|
/// - Branch name pattern (supports wildcards like "feature/*")
|
||||||
|
/// - Required approvals count
|
||||||
|
/// - Required status checks
|
||||||
|
/// - Restrictions on pushes and deletions
|
||||||
|
/// - Creator information
|
||||||
|
///
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/protection-rules",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoListProtectionRules",
|
||||||
|
params(PathParams, QueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Protection rules listed successfully. Returns an array of protection rule objects with metadata.", body = ApiResponse<Vec<BranchProtectionRule>>),
|
||||||
|
(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_protection_rules(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
query: web::Query<QueryParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let rules = service
|
||||||
|
.repo
|
||||||
|
.repo_protection_rules(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
query.limit.unwrap_or(50),
|
||||||
|
query.offset.unwrap_or(0),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(rules)))
|
||||||
|
}
|
||||||
@@ -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::repos::RepoRelease;
|
||||||
|
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 {
|
||||||
|
/// Maximum number of releases to return (default: 50, max: 100)
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
/// Number of releases to skip for pagination (default: 0)
|
||||||
|
pub offset: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List releases in a repository
|
||||||
|
///
|
||||||
|
/// Returns a paginated list of all releases in the repository, sorted by creation date (newest first).
|
||||||
|
/// Includes release metadata such as:
|
||||||
|
/// - Release name and tag
|
||||||
|
/// - Release notes and description
|
||||||
|
/// - Author and creation date
|
||||||
|
/// - Draft and prerelease status
|
||||||
|
/// - Asset download URLs
|
||||||
|
///
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/releases",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoListReleases",
|
||||||
|
params(PathParams, QueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Releases listed successfully. Returns an array of release objects with metadata.", body = ApiResponse<Vec<RepoRelease>>),
|
||||||
|
(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_releases(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
query: web::Query<QueryParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let releases = service
|
||||||
|
.repo
|
||||||
|
.repo_releases(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
query.limit.unwrap_or(50),
|
||||||
|
query.offset.unwrap_or(0),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(releases)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoStar;
|
||||||
|
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 {
|
||||||
|
/// Maximum number of stargazers to return (default: 50, max: 100)
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
/// Number of stargazers to skip for pagination (default: 0)
|
||||||
|
pub offset: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List stargazers of a repository
|
||||||
|
///
|
||||||
|
/// Returns a paginated list of users who have starred the repository, sorted by star date (newest first).
|
||||||
|
/// Includes stargazer metadata such as:
|
||||||
|
/// - User information (ID, username, display name)
|
||||||
|
/// - Star timestamp
|
||||||
|
///
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/stargazers",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoListStargazers",
|
||||||
|
params(PathParams, QueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Stargazers listed successfully. Returns an array of stargazer objects with user information.", body = ApiResponse<Vec<RepoStar>>),
|
||||||
|
(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_stargazers(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
query: web::Query<QueryParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let stargazers = service
|
||||||
|
.repo
|
||||||
|
.repo_stargazers(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
query.limit.unwrap_or(50),
|
||||||
|
query.offset.unwrap_or(0),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(stargazers)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoTag;
|
||||||
|
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 {
|
||||||
|
/// Maximum number of tags to return (default: 50, max: 100)
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
/// Number of tags to skip for pagination (default: 0)
|
||||||
|
pub offset: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List tags in a repository
|
||||||
|
///
|
||||||
|
/// Returns a paginated list of all tags in the repository, sorted by creation date (newest first).
|
||||||
|
/// Includes tag metadata such as:
|
||||||
|
/// - Tag name and commit SHA
|
||||||
|
/// - Tagger information and timestamp
|
||||||
|
/// - Tag message (for annotated tags)
|
||||||
|
///
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/tags",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoListTags",
|
||||||
|
params(PathParams, QueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Tags listed successfully. Returns an array of tag objects with metadata.", body = ApiResponse<Vec<RepoTag>>),
|
||||||
|
(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_tags(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
query: web::Query<QueryParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let tags = service
|
||||||
|
.repo
|
||||||
|
.repo_tags(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
query.limit.unwrap_or(50),
|
||||||
|
query.offset.unwrap_or(0),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(tags)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoWatch;
|
||||||
|
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 {
|
||||||
|
/// Maximum number of watchers to return (default: 50, max: 100)
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
/// Number of watchers to skip for pagination (default: 0)
|
||||||
|
pub offset: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List watchers of a repository
|
||||||
|
///
|
||||||
|
/// Returns a paginated list of users who are watching the repository, sorted by watch date (newest first).
|
||||||
|
/// Includes watcher metadata such as:
|
||||||
|
/// - User information (ID, username, display name)
|
||||||
|
/// - Watch level (participating, watching, or ignoring)
|
||||||
|
/// - Watch timestamp
|
||||||
|
///
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/watchers",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoListWatchers",
|
||||||
|
params(PathParams, QueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Watchers listed successfully. Returns an array of watcher objects with user information and watch level.", body = ApiResponse<Vec<RepoWatch>>),
|
||||||
|
(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_watchers(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
query: web::Query<QueryParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let watchers = service
|
||||||
|
.repo
|
||||||
|
.repo_watchers(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
query.limit.unwrap_or(50),
|
||||||
|
query.offset.unwrap_or(0),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(watchers)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoWebhook;
|
||||||
|
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 {
|
||||||
|
/// Maximum number of webhooks to return (default: 50, max: 100)
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
/// Number of webhooks to skip for pagination (default: 0)
|
||||||
|
pub offset: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List webhooks in a repository
|
||||||
|
///
|
||||||
|
/// Returns a paginated list of all webhooks in the repository, sorted by creation date (newest first).
|
||||||
|
/// Includes webhook metadata such as:
|
||||||
|
/// - Webhook URL and events
|
||||||
|
/// - Active status
|
||||||
|
/// - Last delivery status and timestamp
|
||||||
|
/// - Creator information
|
||||||
|
///
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/webhooks",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoListWebhooks",
|
||||||
|
params(PathParams, QueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Webhooks listed successfully. Returns an array of webhook objects with metadata.", body = ApiResponse<Vec<RepoWebhook>>),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin 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 list_webhooks(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
query: web::Query<QueryParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let webhooks = service
|
||||||
|
.repo
|
||||||
|
.repo_webhooks(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
query.limit.unwrap_or(50),
|
||||||
|
query.offset.unwrap_or(0),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(webhooks)))
|
||||||
|
}
|
||||||
@@ -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::repos::BranchProtectionRule;
|
||||||
|
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 {
|
||||||
|
/// Branch name to check against protection rules
|
||||||
|
pub branch_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Match a branch name against protection rules
|
||||||
|
///
|
||||||
|
/// Checks if a branch name matches any protection rule in the repository.
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
///
|
||||||
|
/// Returns the matching protection rule if found, or null if no rules match.
|
||||||
|
/// Useful for determining what protections apply to a specific branch before performing operations.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/protection/match",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoMatchProtection",
|
||||||
|
params(PathParams, QueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Branch protection check completed. Returns matching protection rule or null if no rules match.", body = ApiResponse<Option<BranchProtectionRule>>),
|
||||||
|
(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 match_protection(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
query: web::Query<QueryParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let rule = service
|
||||||
|
.repo
|
||||||
|
.repo_match_protection(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
&query.branch_name,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(rule)))
|
||||||
|
}
|
||||||
+207
@@ -0,0 +1,207 @@
|
|||||||
|
use actix_web::web;
|
||||||
|
|
||||||
|
pub mod list;
|
||||||
|
pub mod get;
|
||||||
|
pub mod create;
|
||||||
|
pub mod update;
|
||||||
|
pub mod archive;
|
||||||
|
pub mod unarchive;
|
||||||
|
pub mod delete;
|
||||||
|
pub mod transfer_owner;
|
||||||
|
pub mod list_branches;
|
||||||
|
pub mod create_branch;
|
||||||
|
pub mod set_default_branch;
|
||||||
|
pub mod set_branch_protection;
|
||||||
|
pub mod delete_branch;
|
||||||
|
pub mod list_tags;
|
||||||
|
pub mod create_tag;
|
||||||
|
pub mod delete_tag;
|
||||||
|
pub mod list_releases;
|
||||||
|
pub mod create_release;
|
||||||
|
pub mod update_release;
|
||||||
|
pub mod delete_release;
|
||||||
|
pub mod list_forks;
|
||||||
|
pub mod fork_repo;
|
||||||
|
pub mod sync_fork;
|
||||||
|
pub mod star_repo;
|
||||||
|
pub mod unstar_repo;
|
||||||
|
pub mod list_stargazers;
|
||||||
|
pub mod watch_repo;
|
||||||
|
pub mod unwatch_repo;
|
||||||
|
pub mod list_watchers;
|
||||||
|
pub mod list_members;
|
||||||
|
pub mod add_member;
|
||||||
|
pub mod update_member_role;
|
||||||
|
pub mod remove_member;
|
||||||
|
pub mod leave_repo;
|
||||||
|
pub mod list_invitations;
|
||||||
|
pub mod create_invitation;
|
||||||
|
pub mod revoke_invitation;
|
||||||
|
pub mod accept_invitation;
|
||||||
|
pub mod list_deploy_keys;
|
||||||
|
pub mod add_deploy_key;
|
||||||
|
pub mod delete_deploy_key;
|
||||||
|
pub mod list_webhooks;
|
||||||
|
pub mod create_webhook;
|
||||||
|
pub mod update_webhook;
|
||||||
|
pub mod delete_webhook;
|
||||||
|
pub mod list_protection_rules;
|
||||||
|
pub mod get_protection_rule;
|
||||||
|
pub mod match_protection;
|
||||||
|
pub mod create_protection_rule;
|
||||||
|
pub mod update_protection_rule;
|
||||||
|
pub mod delete_protection_rule;
|
||||||
|
pub mod check_branch_merge;
|
||||||
|
pub mod list_commit_statuses;
|
||||||
|
pub mod create_commit_status;
|
||||||
|
pub mod list_commit_comments;
|
||||||
|
pub mod create_commit_comment;
|
||||||
|
pub mod resolve_commit_comment;
|
||||||
|
pub mod get_stats;
|
||||||
|
pub mod refresh_stats;
|
||||||
|
|
||||||
|
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(
|
||||||
|
web::scope("/workspaces/{workspace_name}/repos")
|
||||||
|
.route("", web::get().to(list::list))
|
||||||
|
.route("", web::post().to(create::create))
|
||||||
|
.route("/{repo_name}", web::get().to(get::get))
|
||||||
|
.route("/{repo_name}", web::put().to(update::update))
|
||||||
|
.route("/{repo_name}", web::delete().to(delete::delete))
|
||||||
|
.route("/{repo_name}/archive", web::post().to(archive::archive))
|
||||||
|
.route("/{repo_name}/unarchive", web::post().to(unarchive::unarchive))
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/transfer-owner",
|
||||||
|
web::post().to(transfer_owner::transfer_owner),
|
||||||
|
)
|
||||||
|
.route("/{repo_name}/branches", web::get().to(list_branches::list_branches))
|
||||||
|
.route("/{repo_name}/branches", web::post().to(create_branch::create_branch))
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/branches/{branch_id}/default",
|
||||||
|
web::put().to(set_default_branch::set_default_branch),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/branches/{branch_id}/protection",
|
||||||
|
web::put().to(set_branch_protection::set_branch_protection),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/branches/{branch_id}",
|
||||||
|
web::delete().to(delete_branch::delete_branch),
|
||||||
|
)
|
||||||
|
.route("/{repo_name}/tags", web::get().to(list_tags::list_tags))
|
||||||
|
.route("/{repo_name}/tags", web::post().to(create_tag::create_tag))
|
||||||
|
.route("/{repo_name}/tags/{tag_id}", web::delete().to(delete_tag::delete_tag))
|
||||||
|
.route("/{repo_name}/releases", web::get().to(list_releases::list_releases))
|
||||||
|
.route("/{repo_name}/releases", web::post().to(create_release::create_release))
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/releases/{release_id}",
|
||||||
|
web::put().to(update_release::update_release),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/releases/{release_id}",
|
||||||
|
web::delete().to(delete_release::delete_release),
|
||||||
|
)
|
||||||
|
.route("/{repo_name}/forks", web::get().to(list_forks::list_forks))
|
||||||
|
.route("/{repo_name}/fork", web::post().to(fork_repo::fork_repo))
|
||||||
|
.route("/{repo_name}/sync", web::post().to(sync_fork::sync_fork))
|
||||||
|
.route("/{repo_name}/star", web::post().to(star_repo::star_repo))
|
||||||
|
.route("/{repo_name}/star", web::delete().to(unstar_repo::unstar_repo))
|
||||||
|
.route("/{repo_name}/stargazers", web::get().to(list_stargazers::list_stargazers))
|
||||||
|
.route("/{repo_name}/watch", web::post().to(watch_repo::watch_repo))
|
||||||
|
.route("/{repo_name}/watch", web::delete().to(unwatch_repo::unwatch_repo))
|
||||||
|
.route("/{repo_name}/watchers", web::get().to(list_watchers::list_watchers))
|
||||||
|
.route("/{repo_name}/members", web::get().to(list_members::list_members))
|
||||||
|
.route("/{repo_name}/members", web::post().to(add_member::add_member))
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/members/{member_id}/role",
|
||||||
|
web::put().to(update_member_role::update_member_role),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/members/{member_id}",
|
||||||
|
web::delete().to(remove_member::remove_member),
|
||||||
|
)
|
||||||
|
.route("/{repo_name}/leave", web::post().to(leave_repo::leave_repo))
|
||||||
|
.route("/{repo_name}/invitations", web::get().to(list_invitations::list_invitations))
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/invitations",
|
||||||
|
web::post().to(create_invitation::create_invitation),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/invitations/{invitation_id}",
|
||||||
|
web::delete().to(revoke_invitation::revoke_invitation),
|
||||||
|
)
|
||||||
|
.route("/{repo_name}/deploy-keys", web::get().to(list_deploy_keys::list_deploy_keys))
|
||||||
|
.route("/{repo_name}/deploy-keys", web::post().to(add_deploy_key::add_deploy_key))
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/deploy-keys/{key_id}",
|
||||||
|
web::delete().to(delete_deploy_key::delete_deploy_key),
|
||||||
|
)
|
||||||
|
.route("/{repo_name}/webhooks", web::get().to(list_webhooks::list_webhooks))
|
||||||
|
.route("/{repo_name}/webhooks", web::post().to(create_webhook::create_webhook))
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/webhooks/{webhook_id}",
|
||||||
|
web::put().to(update_webhook::update_webhook),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/webhooks/{webhook_id}",
|
||||||
|
web::delete().to(delete_webhook::delete_webhook),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/protection-rules",
|
||||||
|
web::get().to(list_protection_rules::list_protection_rules),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/protection-rules",
|
||||||
|
web::post().to(create_protection_rule::create_protection_rule),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/protection-rules/{rule_id}",
|
||||||
|
web::get().to(get_protection_rule::get_protection_rule),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/protection-rules/{rule_id}",
|
||||||
|
web::put().to(update_protection_rule::update_protection_rule),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/protection-rules/{rule_id}",
|
||||||
|
web::delete().to(delete_protection_rule::delete_protection_rule),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/protection/match",
|
||||||
|
web::get().to(match_protection::match_protection),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/branches/{target_branch}/merge-check",
|
||||||
|
web::get().to(check_branch_merge::check_branch_merge),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/commits/{push_commit_id}/statuses",
|
||||||
|
web::get().to(list_commit_statuses::list_commit_statuses),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/commit-statuses",
|
||||||
|
web::post().to(create_commit_status::create_commit_status),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/commits/{push_commit_id}/comments",
|
||||||
|
web::get().to(list_commit_comments::list_commit_comments),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/commit-comments",
|
||||||
|
web::post().to(create_commit_comment::create_commit_comment),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/commit-comments/{comment_id}/resolve",
|
||||||
|
web::post().to(resolve_commit_comment::resolve_commit_comment),
|
||||||
|
)
|
||||||
|
.route("/{repo_name}/stats", web::get().to(get_stats::get_stats))
|
||||||
|
.route(
|
||||||
|
"/{repo_name}/stats/refresh",
|
||||||
|
web::post().to(refresh_stats::refresh_stats),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/repos/invitations/accept",
|
||||||
|
web::post().to(accept_invitation::accept_invitation),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoStats;
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh repository statistics
|
||||||
|
///
|
||||||
|
/// Recalculates and updates repository statistics from the current state of the repository.
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Recalculates star, watcher, and fork counts
|
||||||
|
/// - Recalculates branch and tag counts
|
||||||
|
/// - Recalculates commit count from Git history
|
||||||
|
/// - Recalculates release count
|
||||||
|
/// - Recalculates open issues and pull requests count
|
||||||
|
/// - Updates storage size and bandwidth usage
|
||||||
|
/// - Updates last push timestamp
|
||||||
|
///
|
||||||
|
/// Returns the refreshed statistics with all updated values.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/stats/refresh",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoRefreshStats",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Repository statistics refreshed successfully. Returns all updated metrics.", body = ApiResponse<RepoStats>),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error or Git operation failed", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn refresh_stats(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let stats = service
|
||||||
|
.repo
|
||||||
|
.repo_refresh_stats(&session, &path.workspace_name, &path.repo_name)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(stats)))
|
||||||
|
}
|
||||||
@@ -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::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,
|
||||||
|
/// Member ID (UUID)
|
||||||
|
pub member_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a member from a repository
|
||||||
|
///
|
||||||
|
/// Revokes a user's access to the repository.
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Restrictions:
|
||||||
|
/// - Cannot remove the repository owner (use transfer_owner instead)
|
||||||
|
/// - Cannot remove members with equal or higher role than your own
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Member loses all access to the repository
|
||||||
|
/// - Member is removed from all repository activities
|
||||||
|
///
|
||||||
|
/// Returns success message on completion.
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/members/{member_id}",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoRemoveMember",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Member removed successfully.", body = ApiResponse<String>),
|
||||||
|
(status = 400, description = "Cannot remove the repository owner or member with higher role", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or member not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn remove_member(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_remove_member(&session, &path.workspace_name, &path.repo_name, path.member_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Member removed successfully".to_string())))
|
||||||
|
}
|
||||||
@@ -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::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,
|
||||||
|
/// Comment ID (UUID)
|
||||||
|
pub comment_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a commit comment
|
||||||
|
///
|
||||||
|
/// Marks a commit comment as resolved, indicating the issue has been addressed.
|
||||||
|
/// Requires Write role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Comment is marked as resolved
|
||||||
|
/// - Resolved comments are visually distinguished in code review interfaces
|
||||||
|
/// - Resolution is recorded with the resolver's user ID and timestamp
|
||||||
|
///
|
||||||
|
/// Returns success message on completion.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/comments/{comment_id}/resolve",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoResolveCommitComment",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Commit comment resolved successfully.", body = ApiResponse<String>),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Write role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or comment not found", body = ApiErrorResponse),
|
||||||
|
(status = 409, description = "Comment is already resolved", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn resolve_commit_comment(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_resolve_commit_comment(&session, &path.workspace_name, &path.repo_name, path.comment_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Commit comment resolved successfully".to_string())))
|
||||||
|
}
|
||||||
@@ -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::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,
|
||||||
|
/// Invitation ID (UUID)
|
||||||
|
pub invitation_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Revoke a repository invitation
|
||||||
|
///
|
||||||
|
/// Cancels a pending invitation, preventing the invitee from accepting it.
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Invitation is marked as revoked
|
||||||
|
/// - Invitee can no longer accept the invitation
|
||||||
|
/// - Invitation email link becomes invalid
|
||||||
|
///
|
||||||
|
/// Returns success message on completion.
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/invitations/{invitation_id}",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoRevokeInvitation",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Invitation revoked successfully.", body = ApiResponse<String>),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or invitation not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn revoke_invitation(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_revoke_invitation(&session, &path.workspace_name, &path.repo_name, path.invitation_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Invitation revoked successfully".to_string())))
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::{IntoParams, ToSchema};
|
||||||
|
|
||||||
|
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,
|
||||||
|
/// Branch ID (UUID)
|
||||||
|
pub branch_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, ToSchema)]
|
||||||
|
pub struct SetBranchProtectionParams {
|
||||||
|
/// Whether to enable branch protection
|
||||||
|
pub protected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set branch protection
|
||||||
|
///
|
||||||
|
/// Enables or disables protection for a specific branch.
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - When enabled: prevents force pushes and branch deletion
|
||||||
|
/// - When disabled: allows force pushes and branch deletion
|
||||||
|
///
|
||||||
|
/// Returns success message on completion.
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches/{branch_id}/protection",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoSetBranchProtection",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = SetBranchProtectionParams,
|
||||||
|
description = "Branch protection parameters",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Branch protection rules set successfully.", body = ApiResponse<String>),
|
||||||
|
(status = 400, description = "Invalid parameters: negative approvals count or conflicting protection settings", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or branch not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn set_branch_protection(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<SetBranchProtectionParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_set_branch_protection(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
path.branch_id,
|
||||||
|
params.protected,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Branch protection rules set successfully".to_string())))
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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,
|
||||||
|
/// Branch ID (UUID)
|
||||||
|
pub branch_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set default branch
|
||||||
|
///
|
||||||
|
/// Sets a branch as the repository's default branch. The default branch is used for:
|
||||||
|
/// - New pull requests base branch
|
||||||
|
/// - Repository cloning
|
||||||
|
/// - New branch creation base
|
||||||
|
///
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Returns success message on completion.
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches/{branch_id}/default",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoSetDefaultBranch",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Default branch set successfully. All new operations will use this branch as the default.", body = ApiResponse<String>),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or branch not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn set_default_branch(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_set_default_branch(&session, &path.workspace_name, &path.repo_name, path.branch_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Default branch set successfully".to_string())))
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Star a repository
|
||||||
|
///
|
||||||
|
/// Adds the current user to the repository's stargazers list.
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - User is added to the repository's stargazers
|
||||||
|
/// - Repository star count is incremented
|
||||||
|
/// - User can unstar later to remove themselves
|
||||||
|
///
|
||||||
|
/// Returns success message on completion. Idempotent operation (starring an already starred repository is a no-op).
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/star",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoStar",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Repository starred successfully.", body = ApiResponse<String>),
|
||||||
|
(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 star_repo(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_star(&session, &path.workspace_name, &path.repo_name)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Repository starred successfully".to_string())))
|
||||||
|
}
|
||||||
@@ -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::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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sync a fork with upstream
|
||||||
|
///
|
||||||
|
/// Synchronizes a forked repository with the latest changes from the parent repository.
|
||||||
|
/// Requires Write role or higher in the fork repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Merges changes from the parent repository's default branch into the fork
|
||||||
|
/// - Creates a merge commit if there are conflicts
|
||||||
|
/// - Updates the fork's commit history
|
||||||
|
///
|
||||||
|
/// Only works on repositories that are forks (have a parent repository).
|
||||||
|
/// Returns success message on completion.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/sync",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoSyncFork",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Fork synchronized successfully with upstream repository.", body = ApiResponse<String>),
|
||||||
|
(status = 400, description = "Repository is not a fork or has merge conflicts that require manual resolution", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Write role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or parent repository not found", body = ApiErrorResponse),
|
||||||
|
(status = 409, description = "Merge conflicts detected; manual resolution required", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error or Git operation failed", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn sync_fork(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_sync_fork(&session, &path.workspace_name, &path.repo_name)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Fork synchronized successfully".to_string())))
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::{IntoParams, ToSchema};
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::Repo;
|
||||||
|
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, ToSchema)]
|
||||||
|
pub struct TransferOwnerParams {
|
||||||
|
/// User ID of the new owner (must be a repository member)
|
||||||
|
pub new_owner_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transfer repository ownership
|
||||||
|
///
|
||||||
|
/// Transfers ownership of a repository to another user. The new owner must be an existing repository member.
|
||||||
|
/// Requires Owner role in the repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Current owner becomes an Admin
|
||||||
|
/// - New owner gains full Owner permissions
|
||||||
|
/// - Repository URL remains unchanged
|
||||||
|
/// - All forks and stars are preserved
|
||||||
|
///
|
||||||
|
/// Returns the updated repository with new owner information.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/transfer-owner",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoTransferOwner",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = TransferOwnerParams,
|
||||||
|
description = "Transfer ownership parameters",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Ownership transferred successfully. Returns the repository with updated owner information.", body = ApiResponse<Repo>),
|
||||||
|
(status = 400, description = "Invalid new owner ID or user is not a repository member", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Owner role)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or new owner not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn transfer_owner(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<TransferOwnerParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let repo = service
|
||||||
|
.repo
|
||||||
|
.repo_transfer_owner(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
params.new_owner_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(repo)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unarchive a repository
|
||||||
|
///
|
||||||
|
/// Restores an archived repository to active status, re-enabling all write operations.
|
||||||
|
/// Requires Owner role in the repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - Repository status changes from "archived" to "active"
|
||||||
|
/// - All write operations are re-enabled
|
||||||
|
/// - Previous visibility and settings are preserved
|
||||||
|
///
|
||||||
|
/// Returns success message on completion.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/unarchive",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoUnarchive",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Repository unarchived successfully. All write operations are now enabled.", body = ApiResponse<String>),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Owner role)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||||
|
(status = 409, description = "Repository is not archived", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn unarchive(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_unarchive(&session, &path.workspace_name, &path.repo_name)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Repository unarchived".to_string())))
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unstar a repository
|
||||||
|
///
|
||||||
|
/// Removes the current user from the repository's stargazers list.
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - User is removed from the repository's stargazers
|
||||||
|
/// - Repository star count is decremented
|
||||||
|
///
|
||||||
|
/// Returns success message on completion. Idempotent operation (unstarring an unstarred repository is a no-op).
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/star",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoUnstar",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Repository unstarred successfully.", body = ApiResponse<String>),
|
||||||
|
(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 unstar_repo(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_unstar(&session, &path.workspace_name, &path.repo_name)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Repository unstarred successfully".to_string())))
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unwatch a repository
|
||||||
|
///
|
||||||
|
/// Removes the current user's watch subscription from the repository.
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
///
|
||||||
|
/// Effects:
|
||||||
|
/// - User is removed from the repository's watchers
|
||||||
|
/// - User will no longer receive notifications for repository activities
|
||||||
|
///
|
||||||
|
/// Returns success message on completion. Idempotent operation (unwatching an unwatched repository is a no-op).
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/watch",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoUnwatch",
|
||||||
|
params(PathParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Repository watch subscription removed successfully.", body = ApiResponse<String>),
|
||||||
|
(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 unwatch_repo(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_unwatch(&session, &path.workspace_name, &path.repo_name)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Repository watch subscription removed successfully".to_string())))
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::Repo;
|
||||||
|
use crate::service::repo::core::UpdateRepoParams;
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a repository
|
||||||
|
///
|
||||||
|
/// Updates repository metadata such as name, description, visibility, and default branch.
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Update rules:
|
||||||
|
/// - name: Must be unique within workspace if changed (1-100 characters)
|
||||||
|
/// - description: Max 500 characters
|
||||||
|
/// - visibility: "public", "private", or "internal" (workspace owners can restrict public repos)
|
||||||
|
/// - default_branch: Must be an existing branch name
|
||||||
|
///
|
||||||
|
/// All fields are optional; only provided fields are updated.
|
||||||
|
/// Returns the updated repository with full metadata.
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoUpdate",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = UpdateRepoParams,
|
||||||
|
description = "Repository update parameters (all fields optional)",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Repository updated successfully. Returns the updated repository with full metadata.", body = ApiResponse<Repo>),
|
||||||
|
(status = 400, description = "Invalid parameters: name too long, invalid characters, default branch doesn't exist, or public repos disabled", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository or workspace not found", body = ApiErrorResponse),
|
||||||
|
(status = 409, description = "Repository name already exists in the workspace", 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<UpdateRepoParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let repo = service
|
||||||
|
.repo
|
||||||
|
.repo_update(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
params.into_inner(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(repo)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::RepoMember;
|
||||||
|
use crate::service::repo::members::UpdateRepoMemberRoleParams;
|
||||||
|
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,
|
||||||
|
/// Member ID (UUID)
|
||||||
|
pub member_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a member's role in a repository
|
||||||
|
///
|
||||||
|
/// Changes the access level of an existing repository member.
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Role restrictions:
|
||||||
|
/// - Cannot change the owner's role (use transfer_owner instead)
|
||||||
|
/// - Cannot assign "owner" role (use transfer_owner instead)
|
||||||
|
/// - Can only assign roles equal to or lower than your own
|
||||||
|
/// - Valid roles: "read", "write", "admin"
|
||||||
|
///
|
||||||
|
/// Returns the updated member record with new role information.
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/members/{member_id}/role",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoUpdateMemberRole",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = UpdateRepoMemberRoleParams,
|
||||||
|
description = "Role update parameters",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Member role updated successfully. Returns the updated member record with new role.", body = ApiResponse<RepoMember>),
|
||||||
|
(status = 400, description = "Invalid parameters: invalid role or attempting to change owner role", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or member not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn update_member_role(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<UpdateRepoMemberRoleParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let member = service
|
||||||
|
.repo
|
||||||
|
.repo_update_member_role(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
path.member_id,
|
||||||
|
params.into_inner(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(member)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::api::response::{ApiResponse, ApiErrorResponse};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::repos::BranchProtectionRule;
|
||||||
|
use crate::service::repo::protection::UpdateProtectionRuleParams;
|
||||||
|
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,
|
||||||
|
/// Protection rule ID (UUID)
|
||||||
|
pub rule_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a branch protection rule
|
||||||
|
///
|
||||||
|
/// Updates an existing branch protection rule's configuration.
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Updatable fields:
|
||||||
|
/// - required_approvals: Number of required approvals before merging (0-10)
|
||||||
|
/// - require_status_checks: Whether status checks must pass
|
||||||
|
/// - required_status_checks: List of required status check contexts
|
||||||
|
/// - restrict_pushes: Restrict who can push to matching branches
|
||||||
|
/// - allow_force_pushes: Allow force pushes (only if restrict_pushes is false)
|
||||||
|
/// - allow_deletions: Allow branch deletion (only if restrict_pushes is false)
|
||||||
|
///
|
||||||
|
/// All fields are optional; only provided fields are updated.
|
||||||
|
/// Returns the updated protection rule with full configuration.
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/protection-rules/{rule_id}",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoUpdateProtectionRule",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = UpdateProtectionRuleParams,
|
||||||
|
description = "Protection rule update parameters (all fields optional)",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Protection rule updated successfully. Returns the updated protection rule with full configuration.", body = ApiResponse<BranchProtectionRule>),
|
||||||
|
(status = 400, description = "Invalid parameters: negative approvals count or conflicting settings", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or protection rule not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn update_protection_rule(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<UpdateProtectionRuleParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let rule = service
|
||||||
|
.repo
|
||||||
|
.repo_update_protection_rule(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
path.rule_id,
|
||||||
|
params.into_inner(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(rule)))
|
||||||
|
}
|
||||||
@@ -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::repos::RepoRelease;
|
||||||
|
use crate::service::repo::releases::UpdateReleaseParams;
|
||||||
|
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,
|
||||||
|
/// Release ID (UUID)
|
||||||
|
pub release_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a release
|
||||||
|
///
|
||||||
|
/// Updates release metadata such as name, description, draft status, and prerelease flag.
|
||||||
|
/// Requires Write role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Updatable fields:
|
||||||
|
/// - name: Release name/title (max 255 characters)
|
||||||
|
/// - body: Release notes in markdown format (max 10000 characters)
|
||||||
|
/// - draft: Whether this is a draft release
|
||||||
|
/// - prerelease: Whether this is a prerelease
|
||||||
|
///
|
||||||
|
/// All fields are optional; only provided fields are updated.
|
||||||
|
/// Returns the updated release with full metadata.
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/releases/{release_id}",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoUpdateRelease",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = UpdateReleaseParams,
|
||||||
|
description = "Release update parameters (all fields optional)",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Release updated successfully. Returns the updated release with full metadata.", body = ApiResponse<RepoRelease>),
|
||||||
|
(status = 400, description = "Invalid parameters: name too long or invalid characters", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Write role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or release not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn update_release(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<UpdateReleaseParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let release = service
|
||||||
|
.repo
|
||||||
|
.repo_update_release(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
path.release_id,
|
||||||
|
params.into_inner(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(release)))
|
||||||
|
}
|
||||||
@@ -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::repos::RepoWebhook;
|
||||||
|
use crate::service::repo::webhooks::UpdateWebhookParams;
|
||||||
|
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,
|
||||||
|
/// Webhook ID (UUID)
|
||||||
|
pub webhook_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a webhook in a repository
|
||||||
|
///
|
||||||
|
/// Updates webhook configuration such as URL, events, secret, and active status.
|
||||||
|
/// Requires Admin role or higher in the repository.
|
||||||
|
///
|
||||||
|
/// Updatable fields:
|
||||||
|
/// - url: Webhook endpoint URL (must be HTTPS in production)
|
||||||
|
/// - events: List of events to subscribe to
|
||||||
|
/// - secret: Secret for webhook signature verification
|
||||||
|
/// - active: Whether the webhook is active
|
||||||
|
///
|
||||||
|
/// All fields are optional; only provided fields are updated.
|
||||||
|
/// Returns the updated webhook with full metadata.
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/webhooks/{webhook_id}",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoUpdateWebhook",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = UpdateWebhookParams,
|
||||||
|
description = "Webhook update parameters (all fields optional)",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Webhook updated successfully. Returns the updated webhook with full metadata.", body = ApiResponse<RepoWebhook>),
|
||||||
|
(status = 400, description = "Invalid parameters: invalid URL format or empty events list", body = ApiErrorResponse),
|
||||||
|
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
|
||||||
|
(status = 403, description = "Insufficient permissions (requires Admin role or higher)", body = ApiErrorResponse),
|
||||||
|
(status = 404, description = "Repository, workspace, or webhook not found", body = ApiErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
("session_cookie" = [])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn update_webhook(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<UpdateWebhookParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let webhook = service
|
||||||
|
.repo
|
||||||
|
.repo_update_webhook(
|
||||||
|
&session,
|
||||||
|
&path.workspace_name,
|
||||||
|
&path.repo_name,
|
||||||
|
path.webhook_id,
|
||||||
|
params.into_inner(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new(webhook)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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;
|
||||||
|
use crate::service::repo::watches::WatchParams;
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Watch a repository
|
||||||
|
///
|
||||||
|
/// Subscribes the current user to notifications for repository activities.
|
||||||
|
/// Requires read access to the repository.
|
||||||
|
///
|
||||||
|
/// Watch levels:
|
||||||
|
/// - "participating": Notifications for issues/PRs you're involved in
|
||||||
|
/// - "watching": All repository notifications (default)
|
||||||
|
/// - "ignoring": No notifications
|
||||||
|
///
|
||||||
|
/// Returns success message on completion. Idempotent operation.
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/watch",
|
||||||
|
tag = "Repos",
|
||||||
|
operation_id = "repoWatch",
|
||||||
|
params(PathParams),
|
||||||
|
request_body(
|
||||||
|
content = WatchParams,
|
||||||
|
description = "Watch parameters (level is optional, defaults to 'watching')",
|
||||||
|
content_type = "application/json"
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Repository watch subscription updated successfully.", body = ApiResponse<String>),
|
||||||
|
(status = 400, description = "Invalid watch level (must be 'participating', 'watching', or 'ignoring')", body = ApiErrorResponse),
|
||||||
|
(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 watch_repo(
|
||||||
|
service: web::Data<AppService>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<PathParams>,
|
||||||
|
params: web::Json<WatchParams>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
service
|
||||||
|
.repo
|
||||||
|
.repo_watch(&session, &path.workspace_name, &path.repo_name, params.into_inner())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(ApiResponse::new("Repository watch subscription updated successfully".to_string())))
|
||||||
|
}
|
||||||
+3
-1
@@ -2,12 +2,14 @@ use actix_web::web;
|
|||||||
use actix_web::web::scope;
|
use actix_web::web::scope;
|
||||||
|
|
||||||
use crate::api::auth;
|
use crate::api::auth;
|
||||||
|
use crate::api::repo;
|
||||||
use crate::api::workspace;
|
use crate::api::workspace;
|
||||||
|
|
||||||
pub fn init_routes(cfg: &mut web::ServiceConfig) {
|
pub fn init_routes(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(
|
cfg.service(
|
||||||
scope("/api/v1")
|
scope("/api/v1")
|
||||||
.configure(auth::configure)
|
.configure(auth::configure)
|
||||||
.configure(workspace::configure),
|
.configure(workspace::configure)
|
||||||
|
.configure(repo::configure),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
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 BranchProtectionRule {
|
pub struct BranchProtectionRule {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub repo_id: Uuid,
|
pub repo_id: Uuid,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
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 Repo {
|
pub struct Repo {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub workspace_id: Uuid,
|
pub workspace_id: Uuid,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
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 RepoBranch {
|
pub struct RepoBranch {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub repo_id: Uuid,
|
pub repo_id: Uuid,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
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 RepoCommitComment {
|
pub struct RepoCommitComment {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub repo_id: Uuid,
|
pub repo_id: Uuid,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
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 RepoCommitStatus {
|
pub struct RepoCommitStatus {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub repo_id: Uuid,
|
pub repo_id: Uuid,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
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 RepoDeployKey {
|
pub struct RepoDeployKey {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub repo_id: Uuid,
|
pub repo_id: Uuid,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
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 RepoFork {
|
pub struct RepoFork {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub parent_repo_id: Uuid,
|
pub parent_repo_id: Uuid,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
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 RepoInvitation {
|
pub struct RepoInvitation {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub repo_id: Uuid,
|
pub repo_id: Uuid,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
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 RepoMember {
|
pub struct RepoMember {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub repo_id: Uuid,
|
pub repo_id: Uuid,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
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 RepoRelease {
|
pub struct RepoRelease {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub repo_id: Uuid,
|
pub repo_id: Uuid,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
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 RepoStar {
|
pub struct RepoStar {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub repo_id: Uuid,
|
pub repo_id: Uuid,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
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 RepoStats {
|
pub struct RepoStats {
|
||||||
pub repo_id: Uuid,
|
pub repo_id: Uuid,
|
||||||
pub stars_count: i64,
|
pub stars_count: i64,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
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 RepoTag {
|
pub struct RepoTag {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub repo_id: Uuid,
|
pub repo_id: Uuid,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
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 RepoWatch {
|
pub struct RepoWatch {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub repo_id: Uuid,
|
pub repo_id: Uuid,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
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 RepoWebhook {
|
pub struct RepoWebhook {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub repo_id: Uuid,
|
pub repo_id: Uuid,
|
||||||
|
|||||||
Reference in New Issue
Block a user