feat(api): add comprehensive repository management API endpoints

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