diff --git a/api/mod.rs b/api/mod.rs index 313b7e7..ffc70e5 100644 --- a/api/mod.rs +++ b/api/mod.rs @@ -1,5 +1,6 @@ pub mod auth; pub mod openapi; +pub mod repo; pub mod response; pub mod routes; pub mod workspace; diff --git a/api/openapi.rs b/api/openapi.rs index c63cea1..983caa5 100644 --- a/api/openapi.rs +++ b/api/openapi.rs @@ -5,9 +5,18 @@ use crate::api::auth::regenerate_2fa_backup_codes::{ }; use crate::api::auth::register::RegisterResponse; 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::review_approval::ReviewApprovalRequest; 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::{ Workspace, WorkspaceAuditLog, WorkspaceBilling, WorkspaceCustomBranding, WorkspaceDomain, WorkspaceIntegration, WorkspaceInvitation, WorkspaceMember, WorkspacePendingApproval, @@ -25,6 +34,18 @@ use crate::service::auth::rsa::RsaResponse; use crate::service::auth::totp::{ 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::billing::UpdateBillingParams; use crate::service::workspace::branding::UpdateBrandingParams; @@ -46,6 +67,7 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara tags( (name = "Auth", description = "Authentication, registration, session and email security endpoints."), (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( // Auth @@ -110,6 +132,66 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara crate::api::workspace::request_approval::handle, crate::api::workspace::review_approval::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( ApiEmptyResponse, @@ -193,6 +275,76 @@ use crate::service::workspace::webhooks::{CreateWebhookParams, UpdateWebhookPara RequestApprovalParams, ReviewApprovalRequest, WorkspaceAuditLog, + // Repos + ApiResponse, + ApiResponse>, + ApiResponse, + ApiResponse>, + ApiResponse, + ApiResponse>, + ApiResponse, + ApiResponse>, + ApiResponse, + ApiResponse>, + ApiResponse, + ApiResponse>, + ApiResponse, + ApiResponse>, + ApiResponse, + ApiResponse>, + ApiResponse, + ApiResponse>, + ApiResponse, + ApiResponse>, + ApiResponse, + ApiResponse>, + ApiResponse, + ApiResponse>, + ApiResponse>, + ApiResponse, + ApiResponse, + ApiResponse>, + ApiResponse, + ApiResponse>, + ApiResponse, + ApiResponse, + 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; diff --git a/api/repo/accept_invitation.rs b/api/repo/accept_invitation.rs new file mode 100644 index 0000000..765072b --- /dev/null +++ b/api/repo/accept_invitation.rs @@ -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), + (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, + session: Session, + params: web::Json, +) -> Result { + let invitation = service + .repo + .repo_accept_invitation(&session, ¶ms.token) + .await?; + + Ok(HttpResponse::Ok().json(ApiResponse::new(invitation))) +} diff --git a/api/repo/add_deploy_key.rs b/api/repo/add_deploy_key.rs new file mode 100644 index 0000000..697eeee --- /dev/null +++ b/api/repo/add_deploy_key.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/repo/add_member.rs b/api/repo/add_member.rs new file mode 100644 index 0000000..2d2f3e7 --- /dev/null +++ b/api/repo/add_member.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/repo/archive.rs b/api/repo/archive.rs new file mode 100644 index 0000000..4d10e3d --- /dev/null +++ b/api/repo/archive.rs @@ -0,0 +1,59 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::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), + (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, + session: Session, + path: web::Path, +) -> Result { + service + .repo + .repo_archive(&session, &path.workspace_name, &path.repo_name) + .await?; + + Ok(HttpResponse::Ok().json(ApiResponse::new("Repository archived".to_string()))) +} diff --git a/api/repo/check_branch_merge.rs b/api/repo/check_branch_merge.rs new file mode 100644 index 0000000..df009a8 --- /dev/null +++ b/api/repo/check_branch_merge.rs @@ -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), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/repo/create.rs b/api/repo/create.rs new file mode 100644 index 0000000..5e9fb7d --- /dev/null +++ b/api/repo/create.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + let repo = service + .repo + .repo_create(&session, &path.workspace_name, params.into_inner()) + .await?; + + Ok(HttpResponse::Created().json(ApiResponse::new(repo))) +} diff --git a/api/repo/create_branch.rs b/api/repo/create_branch.rs new file mode 100644 index 0000000..fa9ac3a --- /dev/null +++ b/api/repo/create_branch.rs @@ -0,0 +1,66 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/repo/create_commit_comment.rs b/api/repo/create_commit_comment.rs new file mode 100644 index 0000000..6ddb5b9 --- /dev/null +++ b/api/repo/create_commit_comment.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/repo/create_commit_status.rs b/api/repo/create_commit_status.rs new file mode 100644 index 0000000..74a62a7 --- /dev/null +++ b/api/repo/create_commit_status.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/repo/create_invitation.rs b/api/repo/create_invitation.rs new file mode 100644 index 0000000..5663e7e --- /dev/null +++ b/api/repo/create_invitation.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/repo/create_protection_rule.rs b/api/repo/create_protection_rule.rs new file mode 100644 index 0000000..9136241 --- /dev/null +++ b/api/repo/create_protection_rule.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/repo/create_release.rs b/api/repo/create_release.rs new file mode 100644 index 0000000..743297e --- /dev/null +++ b/api/repo/create_release.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/repo/create_tag.rs b/api/repo/create_tag.rs new file mode 100644 index 0000000..b46a7bb --- /dev/null +++ b/api/repo/create_tag.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/repo/create_webhook.rs b/api/repo/create_webhook.rs new file mode 100644 index 0000000..034f201 --- /dev/null +++ b/api/repo/create_webhook.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/repo/delete.rs b/api/repo/delete.rs new file mode 100644 index 0000000..686d675 --- /dev/null +++ b/api/repo/delete.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + service + .repo + .repo_delete(&session, &path.workspace_name, &path.repo_name) + .await?; + + Ok(HttpResponse::Ok().json(ApiResponse::new("Repository deleted".to_string()))) +} diff --git a/api/repo/delete_branch.rs b/api/repo/delete_branch.rs new file mode 100644 index 0000000..36dd99c --- /dev/null +++ b/api/repo/delete_branch.rs @@ -0,0 +1,60 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/repo/delete_deploy_key.rs b/api/repo/delete_deploy_key.rs new file mode 100644 index 0000000..06e2f12 --- /dev/null +++ b/api/repo/delete_deploy_key.rs @@ -0,0 +1,59 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/repo/delete_protection_rule.rs b/api/repo/delete_protection_rule.rs new file mode 100644 index 0000000..108313b --- /dev/null +++ b/api/repo/delete_protection_rule.rs @@ -0,0 +1,59 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/repo/delete_release.rs b/api/repo/delete_release.rs new file mode 100644 index 0000000..56c3727 --- /dev/null +++ b/api/repo/delete_release.rs @@ -0,0 +1,60 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/repo/delete_tag.rs b/api/repo/delete_tag.rs new file mode 100644 index 0000000..075bb3e --- /dev/null +++ b/api/repo/delete_tag.rs @@ -0,0 +1,59 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/repo/delete_webhook.rs b/api/repo/delete_webhook.rs new file mode 100644 index 0000000..04082fd --- /dev/null +++ b/api/repo/delete_webhook.rs @@ -0,0 +1,59 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/repo/fork_repo.rs b/api/repo/fork_repo.rs new file mode 100644 index 0000000..47ac0f5 --- /dev/null +++ b/api/repo/fork_repo.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + let repo = service + .repo + .repo_fork(&session, &path.workspace_name, &path.repo_name, params.into_inner()) + .await?; + + Ok(HttpResponse::Created().json(ApiResponse::new(repo))) +} diff --git a/api/repo/get.rs b/api/repo/get.rs new file mode 100644 index 0000000..f1900f4 --- /dev/null +++ b/api/repo/get.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + let repo = service + .repo + .repo_get(&session, &path.workspace_name, &path.repo_name) + .await?; + + Ok(HttpResponse::Ok().json(ApiResponse::new(repo))) +} diff --git a/api/repo/get_protection_rule.rs b/api/repo/get_protection_rule.rs new file mode 100644 index 0000000..08f2376 --- /dev/null +++ b/api/repo/get_protection_rule.rs @@ -0,0 +1,59 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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))) +} diff --git a/api/repo/get_stats.rs b/api/repo/get_stats.rs new file mode 100644 index 0000000..56a2026 --- /dev/null +++ b/api/repo/get_stats.rs @@ -0,0 +1,60 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::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), + (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, + session: Session, + path: web::Path, +) -> Result { + let stats = service + .repo + .repo_stats(&session, &path.workspace_name, &path.repo_name) + .await?; + + Ok(HttpResponse::Ok().json(ApiResponse::new(stats))) +} diff --git a/api/repo/leave_repo.rs b/api/repo/leave_repo.rs new file mode 100644 index 0000000..6fbc4bb --- /dev/null +++ b/api/repo/leave_repo.rs @@ -0,0 +1,59 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::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), + (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, + session: Session, + path: web::Path, +) -> Result { + service + .repo + .repo_leave(&session, &path.workspace_name, &path.repo_name) + .await?; + + Ok(HttpResponse::Ok().json(ApiResponse::new("Left repository successfully".to_string()))) +} diff --git a/api/repo/list.rs b/api/repo/list.rs new file mode 100644 index 0000000..f00e175 --- /dev/null +++ b/api/repo/list.rs @@ -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, + /// Number of repositories to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/repo/list_branches.rs b/api/repo/list_branches.rs new file mode 100644 index 0000000..ec46cf5 --- /dev/null +++ b/api/repo/list_branches.rs @@ -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, + /// Number of branches to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/repo/list_commit_comments.rs b/api/repo/list_commit_comments.rs new file mode 100644 index 0000000..d7a0b87 --- /dev/null +++ b/api/repo/list_commit_comments.rs @@ -0,0 +1,76 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::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, + /// Number of comments to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/repo/list_commit_statuses.rs b/api/repo/list_commit_statuses.rs new file mode 100644 index 0000000..5874c30 --- /dev/null +++ b/api/repo/list_commit_statuses.rs @@ -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, + /// Number of statuses to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/repo/list_deploy_keys.rs b/api/repo/list_deploy_keys.rs new file mode 100644 index 0000000..ecf622e --- /dev/null +++ b/api/repo/list_deploy_keys.rs @@ -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, + /// Number of deploy keys to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/repo/list_forks.rs b/api/repo/list_forks.rs new file mode 100644 index 0000000..f10d709 --- /dev/null +++ b/api/repo/list_forks.rs @@ -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, + /// Number of forks to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/repo/list_invitations.rs b/api/repo/list_invitations.rs new file mode 100644 index 0000000..cbd136d --- /dev/null +++ b/api/repo/list_invitations.rs @@ -0,0 +1,73 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::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, + /// Number of invitations to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/repo/list_members.rs b/api/repo/list_members.rs new file mode 100644 index 0000000..216dc2a --- /dev/null +++ b/api/repo/list_members.rs @@ -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, + /// Number of members to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/repo/list_protection_rules.rs b/api/repo/list_protection_rules.rs new file mode 100644 index 0000000..34657a2 --- /dev/null +++ b/api/repo/list_protection_rules.rs @@ -0,0 +1,73 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::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, + /// Number of protection rules to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/repo/list_releases.rs b/api/repo/list_releases.rs new file mode 100644 index 0000000..3b842db --- /dev/null +++ b/api/repo/list_releases.rs @@ -0,0 +1,73 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::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, + /// Number of releases to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/repo/list_stargazers.rs b/api/repo/list_stargazers.rs new file mode 100644 index 0000000..c9e47d2 --- /dev/null +++ b/api/repo/list_stargazers.rs @@ -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, + /// Number of stargazers to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/repo/list_tags.rs b/api/repo/list_tags.rs new file mode 100644 index 0000000..76666c7 --- /dev/null +++ b/api/repo/list_tags.rs @@ -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, + /// Number of tags to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/repo/list_watchers.rs b/api/repo/list_watchers.rs new file mode 100644 index 0000000..f9a89b2 --- /dev/null +++ b/api/repo/list_watchers.rs @@ -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, + /// Number of watchers to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/repo/list_webhooks.rs b/api/repo/list_webhooks.rs new file mode 100644 index 0000000..28e163a --- /dev/null +++ b/api/repo/list_webhooks.rs @@ -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, + /// Number of webhooks to skip for pagination (default: 0) + pub offset: Option, +} + +/// 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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/repo/match_protection.rs b/api/repo/match_protection.rs new file mode 100644 index 0000000..ecdd6db --- /dev/null +++ b/api/repo/match_protection.rs @@ -0,0 +1,66 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::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>), + (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, + session: Session, + path: web::Path, + query: web::Query, +) -> Result { + 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))) +} diff --git a/api/repo/mod.rs b/api/repo/mod.rs new file mode 100644 index 0000000..0b58765 --- /dev/null +++ b/api/repo/mod.rs @@ -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), + ); +} diff --git a/api/repo/refresh_stats.rs b/api/repo/refresh_stats.rs new file mode 100644 index 0000000..f0606b3 --- /dev/null +++ b/api/repo/refresh_stats.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + let stats = service + .repo + .repo_refresh_stats(&session, &path.workspace_name, &path.repo_name) + .await?; + + Ok(HttpResponse::Ok().json(ApiResponse::new(stats))) +} diff --git a/api/repo/remove_member.rs b/api/repo/remove_member.rs new file mode 100644 index 0000000..e0a6331 --- /dev/null +++ b/api/repo/remove_member.rs @@ -0,0 +1,63 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/repo/resolve_commit_comment.rs b/api/repo/resolve_commit_comment.rs new file mode 100644 index 0000000..217ebb3 --- /dev/null +++ b/api/repo/resolve_commit_comment.rs @@ -0,0 +1,60 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/repo/revoke_invitation.rs b/api/repo/revoke_invitation.rs new file mode 100644 index 0000000..8865ac8 --- /dev/null +++ b/api/repo/revoke_invitation.rs @@ -0,0 +1,59 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/repo/set_branch_protection.rs b/api/repo/set_branch_protection.rs new file mode 100644 index 0000000..2253763 --- /dev/null +++ b/api/repo/set_branch_protection.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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()))) +} diff --git a/api/repo/set_default_branch.rs b/api/repo/set_default_branch.rs new file mode 100644 index 0000000..cec6a98 --- /dev/null +++ b/api/repo/set_default_branch.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/repo/star_repo.rs b/api/repo/star_repo.rs new file mode 100644 index 0000000..eb69697 --- /dev/null +++ b/api/repo/star_repo.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + service + .repo + .repo_star(&session, &path.workspace_name, &path.repo_name) + .await?; + + Ok(HttpResponse::Ok().json(ApiResponse::new("Repository starred successfully".to_string()))) +} diff --git a/api/repo/sync_fork.rs b/api/repo/sync_fork.rs new file mode 100644 index 0000000..b9bafd9 --- /dev/null +++ b/api/repo/sync_fork.rs @@ -0,0 +1,60 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::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), + (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, + session: Session, + path: web::Path, +) -> Result { + service + .repo + .repo_sync_fork(&session, &path.workspace_name, &path.repo_name) + .await?; + + Ok(HttpResponse::Ok().json(ApiResponse::new("Fork synchronized successfully".to_string()))) +} diff --git a/api/repo/transfer_owner.rs b/api/repo/transfer_owner.rs new file mode 100644 index 0000000..d66d1bc --- /dev/null +++ b/api/repo/transfer_owner.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/repo/unarchive.rs b/api/repo/unarchive.rs new file mode 100644 index 0000000..b225096 --- /dev/null +++ b/api/repo/unarchive.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + service + .repo + .repo_unarchive(&session, &path.workspace_name, &path.repo_name) + .await?; + + Ok(HttpResponse::Ok().json(ApiResponse::new("Repository unarchived".to_string()))) +} diff --git a/api/repo/unstar_repo.rs b/api/repo/unstar_repo.rs new file mode 100644 index 0000000..679bfa4 --- /dev/null +++ b/api/repo/unstar_repo.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + service + .repo + .repo_unstar(&session, &path.workspace_name, &path.repo_name) + .await?; + + Ok(HttpResponse::Ok().json(ApiResponse::new("Repository unstarred successfully".to_string()))) +} diff --git a/api/repo/unwatch_repo.rs b/api/repo/unwatch_repo.rs new file mode 100644 index 0000000..ab8014f --- /dev/null +++ b/api/repo/unwatch_repo.rs @@ -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), + (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, + session: Session, + path: web::Path, +) -> Result { + 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()))) +} diff --git a/api/repo/update.rs b/api/repo/update.rs new file mode 100644 index 0000000..9399bb3 --- /dev/null +++ b/api/repo/update.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + let repo = service + .repo + .repo_update( + &session, + &path.workspace_name, + &path.repo_name, + params.into_inner(), + ) + .await?; + + Ok(HttpResponse::Ok().json(ApiResponse::new(repo))) +} diff --git a/api/repo/update_member_role.rs b/api/repo/update_member_role.rs new file mode 100644 index 0000000..08c02f7 --- /dev/null +++ b/api/repo/update_member_role.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/repo/update_protection_rule.rs b/api/repo/update_protection_rule.rs new file mode 100644 index 0000000..d6214fe --- /dev/null +++ b/api/repo/update_protection_rule.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/repo/update_release.rs b/api/repo/update_release.rs new file mode 100644 index 0000000..dde4324 --- /dev/null +++ b/api/repo/update_release.rs @@ -0,0 +1,76 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/repo/update_webhook.rs b/api/repo/update_webhook.rs new file mode 100644 index 0000000..55ebd28 --- /dev/null +++ b/api/repo/update_webhook.rs @@ -0,0 +1,76 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::api::response::{ApiResponse, ApiErrorResponse}; +use crate::error::AppError; +use crate::models::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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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))) +} diff --git a/api/repo/watch_repo.rs b/api/repo/watch_repo.rs new file mode 100644 index 0000000..f6271d5 --- /dev/null +++ b/api/repo/watch_repo.rs @@ -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), + (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, + session: Session, + path: web::Path, + params: web::Json, +) -> Result { + 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()))) +} diff --git a/api/routes.rs b/api/routes.rs index 363f162..f2c9971 100644 --- a/api/routes.rs +++ b/api/routes.rs @@ -2,12 +2,14 @@ use actix_web::web; use actix_web::web::scope; use crate::api::auth; +use crate::api::repo; use crate::api::workspace; pub fn init_routes(cfg: &mut web::ServiceConfig) { cfg.service( scope("/api/v1") .configure(auth::configure) - .configure(workspace::configure), + .configure(workspace::configure) + .configure(repo::configure), ); } diff --git a/models/repos/branch_protection_rule.rs b/models/repos/branch_protection_rule.rs index 9df3320..c8bb84e 100644 --- a/models/repos/branch_protection_rule.rs +++ b/models/repos/branch_protection_rule.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct BranchProtectionRule { pub id: Uuid, pub repo_id: Uuid, diff --git a/models/repos/repo.rs b/models/repos/repo.rs index 4123f0e..7b6c0ad 100644 --- a/models/repos/repo.rs +++ b/models/repos/repo.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct Repo { pub id: Uuid, pub workspace_id: Uuid, diff --git a/models/repos/repo_branches.rs b/models/repos/repo_branches.rs index 7dff5b4..4f9a391 100644 --- a/models/repos/repo_branches.rs +++ b/models/repos/repo_branches.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct RepoBranch { pub id: Uuid, pub repo_id: Uuid, diff --git a/models/repos/repo_commit_comments.rs b/models/repos/repo_commit_comments.rs index 6739af6..047db19 100644 --- a/models/repos/repo_commit_comments.rs +++ b/models/repos/repo_commit_comments.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct RepoCommitComment { pub id: Uuid, pub repo_id: Uuid, diff --git a/models/repos/repo_commit_statuses.rs b/models/repos/repo_commit_statuses.rs index d21bfa7..93e4038 100644 --- a/models/repos/repo_commit_statuses.rs +++ b/models/repos/repo_commit_statuses.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct RepoCommitStatus { pub id: Uuid, pub repo_id: Uuid, diff --git a/models/repos/repo_deploy_keys.rs b/models/repos/repo_deploy_keys.rs index 6e8c5e2..296a153 100644 --- a/models/repos/repo_deploy_keys.rs +++ b/models/repos/repo_deploy_keys.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct RepoDeployKey { pub id: Uuid, pub repo_id: Uuid, diff --git a/models/repos/repo_fork.rs b/models/repos/repo_fork.rs index 56ce122..4ed99bc 100644 --- a/models/repos/repo_fork.rs +++ b/models/repos/repo_fork.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct RepoFork { pub id: Uuid, pub parent_repo_id: Uuid, diff --git a/models/repos/repo_invitations.rs b/models/repos/repo_invitations.rs index 52c49db..eeb3ee5 100644 --- a/models/repos/repo_invitations.rs +++ b/models/repos/repo_invitations.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct RepoInvitation { pub id: Uuid, pub repo_id: Uuid, diff --git a/models/repos/repo_members.rs b/models/repos/repo_members.rs index 3b51543..c92e31b 100644 --- a/models/repos/repo_members.rs +++ b/models/repos/repo_members.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct RepoMember { pub id: Uuid, pub repo_id: Uuid, diff --git a/models/repos/repo_releases.rs b/models/repos/repo_releases.rs index e770a04..efc83d7 100644 --- a/models/repos/repo_releases.rs +++ b/models/repos/repo_releases.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct RepoRelease { pub id: Uuid, pub repo_id: Uuid, diff --git a/models/repos/repo_stars.rs b/models/repos/repo_stars.rs index e7f9f78..ac543b8 100644 --- a/models/repos/repo_stars.rs +++ b/models/repos/repo_stars.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct RepoStar { pub id: Uuid, pub repo_id: Uuid, diff --git a/models/repos/repo_stats.rs b/models/repos/repo_stats.rs index f5f218b..f442fb8 100644 --- a/models/repos/repo_stats.rs +++ b/models/repos/repo_stats.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct RepoStats { pub repo_id: Uuid, pub stars_count: i64, diff --git a/models/repos/repo_tags.rs b/models/repos/repo_tags.rs index ea8dae3..2a92fab 100644 --- a/models/repos/repo_tags.rs +++ b/models/repos/repo_tags.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct RepoTag { pub id: Uuid, pub repo_id: Uuid, diff --git a/models/repos/repo_watches.rs b/models/repos/repo_watches.rs index 7c72ce8..05b7491 100644 --- a/models/repos/repo_watches.rs +++ b/models/repos/repo_watches.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct RepoWatch { pub id: Uuid, pub repo_id: Uuid, diff --git a/models/repos/repo_webhooks.rs b/models/repos/repo_webhooks.rs index 9a2101e..29c1aa9 100644 --- a/models/repos/repo_webhooks.rs +++ b/models/repos/repo_webhooks.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow, utoipa::ToSchema)] pub struct RepoWebhook { pub id: Uuid, pub repo_id: Uuid,