feat(api): expand API endpoints for repo, PR, user, workspace management

- Add git operation endpoints: archive, compare branches, diff, tree,
  repository extras
- Add repo endpoints: contributors, delete fork, get branch/commit
  status/deploy key/invitation/member/release/tag/webhook, topics,
  release assets, webhook deliveries/retry
- Add PR endpoints: review requests, templates
- Add user endpoints: block/unblock, follow/unfollow, presence,
  personal access tokens, account restore
- Add workspace endpoints: billing history, approvals, domains,
  integrations, invitations, members, webhooks, restore
- Add internal API, notification API, IM API modules
- Update route configuration and OpenAPI spec
This commit is contained in:
zhenyi
2026-06-10 18:49:27 +08:00
parent 4586b79cb8
commit cec6dce955
161 changed files with 7522 additions and 349 deletions
+53
View File
@@ -0,0 +1,53 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::repo::contributors::Contributor;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/contributors",
tag = "Repos",
operation_id = "repoListContributors",
params(PathParams, QueryParams),
responses(
(status = 200, description = "List of contributors", body = ApiResponse<Vec<Contributor>>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Repo not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn list_contributors(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.repo_contributors(
&session,
&path.workspace_name,
&path.repo_name,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+14 -3
View File
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::Repo;
use crate::models::base_info::{resolve_users, resolve_workspaces};
use crate::models::repos::RepoDetail;
use crate::service::AppService;
use crate::service::repo::core::CreateRepoParams;
use crate::session::Session;
@@ -42,7 +43,7 @@ pub struct PathParams {
content_type = "application/json"
),
responses(
(status = 201, description = "Repository created successfully. Returns the newly created repository with full metadata.", body = ApiResponse<Repo>),
(status = 201, description = "Repository created successfully. Returns the newly created repository with full metadata.", body = ApiResponse<RepoDetail>),
(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),
@@ -65,5 +66,15 @@ pub async fn create(
.repo_create(&session, &path.workspace_name, params.into_inner())
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(repo)))
let db = &service.ctx.db;
let users = resolve_users(db, &[repo.owner_id]).await?;
let workspaces = resolve_workspaces(db, &[repo.workspace_id]).await?;
let owner = users.get(&repo.owner_id).cloned().unwrap_or_default();
let workspace = workspaces
.get(&repo.workspace_id)
.cloned()
.unwrap_or_default();
let detail = repo.into_detail(owner, workspace);
Ok(HttpResponse::Created().json(ApiResponse::new(detail)))
}
+10 -29
View File
@@ -9,42 +9,24 @@ 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,
pub branch_name: String,
}
/// 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}",
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches/{branch_name}",
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),
(status = 200, description = "Branch deleted", body = ApiResponse<String>),
(status = 400, description = "Cannot delete default branch", body = ApiErrorResponse),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Branch not found", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
security(("session_cookie" = []))
)]
pub async fn delete_branch(
service: web::Data<AppService>,
@@ -53,13 +35,12 @@ pub async fn delete_branch(
) -> Result<HttpResponse, AppError> {
service
.repo
.repo_delete_branch(
.git_delete_branch(
&session,
&path.workspace_name,
&path.repo_name,
path.branch_id,
&path.branch_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Branch deleted successfully".to_string())))
Ok(HttpResponse::Ok().json(ApiResponse::new("Branch deleted".to_string())))
}
+41
View File
@@ -0,0 +1,41 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/fork",
tag = "Repos",
operation_id = "repoDeleteFork",
params(PathParams),
responses(
(status = 200, description = "Fork deleted successfully", body = ApiResponse<String>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Fork not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn delete_fork(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.repo
.repo_delete_fork(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Fork deleted successfully".to_string())))
}
+13 -27
View File
@@ -9,41 +9,23 @@ 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,
pub tag_name: String,
}
/// 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}",
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/tags/{tag_name}",
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),
(status = 200, description = "Tag deleted", body = ApiResponse<String>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Tag not found", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
security(("session_cookie" = []))
)]
pub async fn delete_tag(
service: web::Data<AppService>,
@@ -52,8 +34,12 @@ pub async fn delete_tag(
) -> Result<HttpResponse, AppError> {
service
.repo
.repo_delete_tag(&session, &path.workspace_name, &path.repo_name, path.tag_id)
.git_delete_tag(
&session,
&path.workspace_name,
&path.repo_name,
&path.tag_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Tag deleted successfully".to_string())))
Ok(HttpResponse::Ok().json(ApiResponse::new("Tag deleted".to_string())))
}
+14 -3
View File
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::Repo;
use crate::models::base_info::{resolve_users, resolve_workspaces};
use crate::models::repos::RepoDetail;
use crate::service::AppService;
use crate::session::Session;
@@ -42,7 +43,7 @@ use crate::service::repo::fork::ForkRepoParams;
content_type = "application/json"
),
responses(
(status = 201, description = "Repository forked successfully. Returns the newly created fork with full metadata.", body = ApiResponse<Repo>),
(status = 201, description = "Repository forked successfully. Returns the newly created fork with full metadata.", body = ApiResponse<RepoDetail>),
(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),
@@ -70,5 +71,15 @@ pub async fn fork_repo(
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(repo)))
let db = &service.ctx.db;
let users = resolve_users(db, &[repo.owner_id]).await?;
let workspaces = resolve_workspaces(db, &[repo.workspace_id]).await?;
let owner = users.get(&repo.owner_id).cloned().unwrap_or_default();
let workspace = workspaces
.get(&repo.workspace_id)
.cloned()
.unwrap_or_default();
let detail = repo.into_detail(owner, workspace);
Ok(HttpResponse::Created().json(ApiResponse::new(detail)))
}
+14 -3
View File
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::Repo;
use crate::models::base_info::{resolve_users, resolve_workspaces};
use crate::models::repos::RepoDetail;
use crate::service::AppService;
use crate::session::Session;
@@ -32,7 +33,7 @@ pub struct PathParams {
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 = 200, description = "Repository retrieved successfully. Returns complete repository metadata including visibility, default branch, creation date, and statistics.", body = ApiResponse<RepoDetail>),
(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),
@@ -52,5 +53,15 @@ pub async fn get(
.repo_get(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(repo)))
let db = &service.ctx.db;
let users = resolve_users(db, &[repo.owner_id]).await?;
let workspaces = resolve_workspaces(db, &[repo.workspace_id]).await?;
let owner = users.get(&repo.owner_id).cloned().unwrap_or_default();
let workspace = workspaces
.get(&repo.workspace_id)
.cloned()
.unwrap_or_default();
let detail = repo.into_detail(owner, workspace);
Ok(HttpResponse::Ok().json(ApiResponse::new(detail)))
}
+45
View File
@@ -0,0 +1,45 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub branch_name: String,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches/{branch_name}",
tag = "Repos",
operation_id = "repoGetBranch",
params(PathParams),
responses(
(status = 200, description = "Branch retrieved", body = ApiResponse<crate::pb::repo::Branch>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Branch not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn get_branch(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_get_branch(
&session,
&path.workspace_name,
&path.repo_name,
&path.branch_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+56
View File
@@ -0,0 +1,56 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoCommitStatus;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub push_commit_id: uuid::Uuid,
pub status_id: uuid::Uuid,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/commits/{push_commit_id}/statuses/{status_id}",
tag = "Repos",
operation_id = "repoGetCommitStatus",
params(PathParams),
responses(
(status = 200, description = "Commit status retrieved successfully", body = ApiResponse<RepoCommitStatus>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Commit status not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn get_commit_status(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let statuses = service
.repo
.repo_commit_statuses(
&session,
&path.workspace_name,
&path.repo_name,
path.push_commit_id,
1000,
0,
)
.await?;
let status = statuses
.into_iter()
.find(|s| s.id == path.status_id)
.ok_or(AppError::NotFound("commit status not found".into()))?;
Ok(HttpResponse::Ok().json(ApiResponse::new(status)))
}
+48
View File
@@ -0,0 +1,48 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoDeployKey;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub key_id: uuid::Uuid,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/deploy-keys/{key_id}",
tag = "Repos",
operation_id = "repoGetDeployKey",
params(PathParams),
responses(
(status = 200, description = "Deploy key retrieved successfully", body = ApiResponse<RepoDeployKey>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Deploy key not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn get_deploy_key(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let keys = service
.repo
.repo_deploy_keys(&session, &path.workspace_name, &path.repo_name, 1000, 0)
.await?;
let key = keys
.into_iter()
.find(|k| k.id == path.key_id)
.ok_or(AppError::NotFound("deploy key not found".into()))?;
Ok(HttpResponse::Ok().json(ApiResponse::new(key)))
}
+48
View File
@@ -0,0 +1,48 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoInvitation;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub invitation_id: uuid::Uuid,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/invitations/{invitation_id}",
tag = "Repos",
operation_id = "repoGetInvitation",
params(PathParams),
responses(
(status = 200, description = "Invitation retrieved successfully", body = ApiResponse<RepoInvitation>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Invitation not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn get_invitation(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let invitations = service
.repo
.repo_invitations(&session, &path.workspace_name, &path.repo_name, 1000, 0)
.await?;
let invitation = invitations
.into_iter()
.find(|i| i.id == path.invitation_id)
.ok_or(AppError::NotFound("invitation not found".into()))?;
Ok(HttpResponse::Ok().json(ApiResponse::new(invitation)))
}
+48
View File
@@ -0,0 +1,48 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoMember;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub member_id: uuid::Uuid,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/members/{member_id}",
tag = "Repos",
operation_id = "repoGetMember",
params(PathParams),
responses(
(status = 200, description = "Member retrieved successfully", body = ApiResponse<RepoMember>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Member not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn get_member(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let members = service
.repo
.repo_members(&session, &path.workspace_name, &path.repo_name, 1000, 0)
.await?;
let member = members
.into_iter()
.find(|m| m.id == path.member_id)
.ok_or(AppError::NotFound("member not found".into()))?;
Ok(HttpResponse::Ok().json(ApiResponse::new(member)))
}
+48
View File
@@ -0,0 +1,48 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoRelease;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub release_id: uuid::Uuid,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/releases/{release_id}",
tag = "Repos",
operation_id = "repoGetRelease",
params(PathParams),
responses(
(status = 200, description = "Release retrieved successfully", body = ApiResponse<RepoRelease>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Release not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn get_release(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let releases = service
.repo
.repo_releases(&session, &path.workspace_name, &path.repo_name, 1000, 0)
.await?;
let release = releases
.into_iter()
.find(|r| r.id == path.release_id)
.ok_or(AppError::NotFound("release not found".into()))?;
Ok(HttpResponse::Ok().json(ApiResponse::new(release)))
}
+45
View File
@@ -0,0 +1,45 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub tag_name: String,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/tags/{tag_name}",
tag = "Repos",
operation_id = "repoGetTag",
params(PathParams),
responses(
(status = 200, description = "Tag retrieved", body = ApiResponse<crate::pb::repo::Tag>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Tag not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn get_tag(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_get_tag(
&session,
&path.workspace_name,
&path.repo_name,
&path.tag_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+48
View File
@@ -0,0 +1,48 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoWebhook;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub webhook_id: uuid::Uuid,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/webhooks/{webhook_id}",
tag = "Repos",
operation_id = "repoGetWebhook",
params(PathParams),
responses(
(status = 200, description = "Webhook retrieved successfully", body = ApiResponse<RepoWebhook>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Webhook not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn get_webhook(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let webhooks = service
.repo
.repo_webhooks(&session, &path.workspace_name, &path.repo_name, 1000, 0)
.await?;
let webhook = webhooks
.into_iter()
.find(|w| w.id == path.webhook_id)
.ok_or(AppError::NotFound("webhook not found".into()))?;
Ok(HttpResponse::Ok().json(ApiResponse::new(webhook)))
}
+56
View File
@@ -0,0 +1,56 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub revision: String,
pub path: String,
pub page_size: Option<u32>,
}
/// Blame a file
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/blame",
tag = "Git",
operation_id = "gitBlame",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Blame retrieved successfully", body = ApiResponse<crate::pb::repo::BlameResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_blame(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_blame(
&session,
&path.workspace_name,
&path.repo_name,
&query.revision,
&query.path,
query.page_size.unwrap_or(30),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+55
View File
@@ -0,0 +1,55 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub revision: String,
pub path: String,
}
/// Get blob content
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/blobs",
tag = "Git",
operation_id = "gitGetBlob",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Blob retrieved successfully", body = ApiResponse<crate::pb::repo::Blob>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Blob not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_blob(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_get_blob(
&session,
&path.workspace_name,
&path.repo_name,
&query.revision,
&query.path,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+54
View File
@@ -0,0 +1,54 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::repo::git::merge::CherryPickParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Cherry-pick a commit
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/cherry-pick",
tag = "Git",
operation_id = "gitCherryPick",
params(PathParams),
request_body(
content = CherryPickParams,
description = "Cherry-pick parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Cherry-pick completed successfully", body = ApiResponse<crate::pb::repo::CreateCommitResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 409, description = "Cherry-pick conflict", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_cherry_pick(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<CherryPickParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_cherry_pick(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+137
View File
@@ -0,0 +1,137 @@
use crate::api::response::ApiResponse;
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct RevisionPathParams {
pub workspace_name: String,
pub repo_name: String,
pub revision: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct FindCommitQuery {
pub revision: String,
}
#[utoipa::path(get, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/find-commit", tag = "Git", operation_id = "gitFindCommit", params(PathParams, FindCommitQuery), responses((status=200,body=ApiResponse<crate::pb::repo::Commit>)), security(("session_cookie"=[])))]
pub async fn git_find_commit(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
q: web::Query<FindCommitQuery>,
) -> Result<HttpResponse, AppError> {
let r = service
.repo
.git_find_commit(&session, &path.workspace_name, &path.repo_name, &q.revision)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(r)))
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct ListCommitsByOidBody {
pub oids: Vec<String>,
}
#[utoipa::path(post, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits/by-oid", tag = "Git", operation_id = "gitCommitsByOid", params(PathParams), request_body(content=ListCommitsByOidBody), responses((status=200,body=ApiResponse<crate::pb::repo::ListCommitsByOidResponse>)), security(("session_cookie"=[])))]
pub async fn git_commits_by_oid(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
body: web::Json<ListCommitsByOidBody>,
) -> Result<HttpResponse, AppError> {
let oids: Vec<Vec<u8>> = body
.oids
.iter()
.map(|s| hex::decode(s).unwrap_or_default())
.collect();
let r = service
.repo
.git_list_commits_by_oid(&session, &path.workspace_name, &path.repo_name, oids)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(r)))
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct AncestorQuery {
pub ancestor: String,
pub descendant: String,
}
#[utoipa::path(get, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commit-is-ancestor", tag = "Git", operation_id = "gitCommitIsAncestor", params(PathParams, AncestorQuery), responses((status=200,body=ApiResponse<bool>)), security(("session_cookie"=[])))]
pub async fn git_commit_is_ancestor(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
q: web::Query<AncestorQuery>,
) -> Result<HttpResponse, AppError> {
let r = service
.repo
.git_commit_is_ancestor(
&session,
&path.workspace_name,
&path.repo_name,
&q.ancestor,
&q.descendant,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(r)))
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct LastCommitQuery {
pub path: String,
pub revision: Option<String>,
}
#[utoipa::path(get, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/last-commit", tag = "Git", operation_id = "gitLastCommitForPath", params(PathParams, LastCommitQuery), responses((status=200,body=ApiResponse<crate::pb::repo::LastCommitForPathResponse>)), security(("session_cookie"=[])))]
pub async fn git_last_commit(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
q: web::Query<LastCommitQuery>,
) -> Result<HttpResponse, AppError> {
let r = service
.repo
.git_last_commit_for_path(
&session,
&path.workspace_name,
&path.repo_name,
&q.path,
q.revision.as_deref(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(r)))
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct CommitsByMsgQuery {
pub q: String,
pub revision: Option<String>,
pub limit: Option<u32>,
}
#[utoipa::path(get, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits/search", tag = "Git", operation_id = "gitCommitsByMessage", params(PathParams, CommitsByMsgQuery), responses((status=200,body=ApiResponse<crate::pb::repo::CommitsByMessageResponse>)), security(("session_cookie"=[])))]
pub async fn git_commits_by_message(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
q: web::Query<CommitsByMsgQuery>,
) -> Result<HttpResponse, AppError> {
let r = service
.repo
.git_commits_by_message(
&session,
&path.workspace_name,
&path.repo_name,
&q.q,
q.revision.as_deref(),
q.limit.unwrap_or(20),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(r)))
}
+45
View File
@@ -0,0 +1,45 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub revision: String,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits/{revision}/stats",
tag = "Git",
operation_id = "gitCommitStats",
params(PathParams),
responses(
(status = 200, description = "Commit stats", body = ApiResponse<crate::pb::repo::CommitStats>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_commit_stats(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_commit_stats(
&session,
&path.workspace_name,
&path.repo_name,
&path.revision,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+59
View File
@@ -0,0 +1,59 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::repo::git::merge::CompareParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub base: String,
pub head: String,
pub page_size: Option<u32>,
}
/// Compare two commits
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/compare",
tag = "Git",
operation_id = "gitCompare",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Comparison completed successfully", body = ApiResponse<crate::pb::repo::CompareCommitsResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_compare(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_compare_commits(
&session,
&path.workspace_name,
&path.repo_name,
CompareParams {
base: query.base.clone(),
head: query.head.clone(),
page_size: query.page_size,
},
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+56
View File
@@ -0,0 +1,56 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub target: String,
pub source: String,
pub page_size: Option<u32>,
}
/// List merge conflicts
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/conflicts",
tag = "Git",
operation_id = "gitListConflicts",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Conflicts listed successfully", body = ApiResponse<crate::pb::repo::ListMergeConflictsResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_conflicts(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_list_conflicts(
&session,
&path.workspace_name,
&path.repo_name,
&query.target,
&query.source,
query.page_size.unwrap_or(30),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+56
View File
@@ -0,0 +1,56 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub revision: Option<String>,
pub path: Option<String>,
pub since: Option<String>,
pub until: Option<String>,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits/count",
tag = "Git",
operation_id = "gitCountCommits",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Commit count", body = ApiResponse<crate::pb::repo::CountCommitsResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_count_commits(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_count_commits(
&session,
&path.workspace_name,
&path.repo_name,
query.revision.as_deref(),
query.path.as_deref(),
query.since.as_deref(),
query.until.as_deref(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+52
View File
@@ -0,0 +1,52 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub left: String,
pub right: String,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/compare/diverging",
tag = "Git",
operation_id = "gitCountDivergingCommits",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Diverging commit counts", body = ApiResponse<crate::pb::repo::CountDivergingCommitsResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_count_diverging(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_count_diverging_commits(
&session,
&path.workspace_name,
&path.repo_name,
&query.left,
&query.right,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+59
View File
@@ -0,0 +1,59 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::{IntoParams, ToSchema};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams, ToSchema)]
pub struct CreateBranchBody {
pub branch_name: String,
pub start_point: String,
}
/// Create a branch
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/branches",
tag = "Git",
operation_id = "gitCreateBranch",
params(PathParams),
request_body(
content = CreateBranchBody,
description = "Branch creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Branch created successfully", body = ApiResponse<crate::pb::repo::Branch>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_create_branch(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
body: web::Json<CreateBranchBody>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_create_branch(
&session,
&path.workspace_name,
&path.repo_name,
&body.branch_name,
&body.start_point,
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
+53
View File
@@ -0,0 +1,53 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::repo::git::merge::CreateCommitParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Create a commit
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits",
tag = "Git",
operation_id = "gitCreateCommit",
params(PathParams),
request_body(
content = CreateCommitParams,
description = "Commit creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Commit created successfully", body = ApiResponse<crate::pb::repo::CreateCommitResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_create_commit(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<CreateCommitParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_create_commit(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
+63
View File
@@ -0,0 +1,63 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::{IntoParams, ToSchema};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams, ToSchema)]
pub struct CreateTagBody {
pub tag_name: String,
pub target: String,
pub message: Option<String>,
pub annotated: Option<bool>,
}
/// Create a tag
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/tags",
tag = "Git",
operation_id = "gitCreateTag",
params(PathParams),
request_body(
content = CreateTagBody,
description = "Tag creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Tag created successfully", body = ApiResponse<crate::pb::repo::Tag>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_create_tag(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
body: web::Json<CreateTagBody>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_create_tag(
&session,
&path.workspace_name,
&path.repo_name,
&body.tag_name,
&body.target,
body.message.clone(),
body.annotated.unwrap_or(false),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
+47
View File
@@ -0,0 +1,47 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub branch_name: String,
}
/// Delete a branch
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/branches/{branch_name}",
tag = "Git",
operation_id = "gitDeleteBranch",
params(PathParams),
responses(
(status = 200, description = "Branch deleted successfully", body = ApiResponse<String>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_delete_branch(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.repo
.git_delete_branch(
&session,
&path.workspace_name,
&path.repo_name,
&path.branch_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Branch deleted successfully".to_string())))
}
+47
View File
@@ -0,0 +1,47 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub tag_name: String,
}
/// Delete a tag
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/tags/{tag_name}",
tag = "Git",
operation_id = "gitDeleteTag",
params(PathParams),
responses(
(status = 200, description = "Tag deleted successfully", body = ApiResponse<String>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_delete_tag(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.repo
.git_delete_tag(
&session,
&path.workspace_name,
&path.repo_name,
&path.tag_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Tag deleted successfully".to_string())))
}
+56
View File
@@ -0,0 +1,56 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub base: String,
pub head: String,
pub page_size: Option<u32>,
}
/// Get diff between two revisions
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/diff",
tag = "Git",
operation_id = "gitDiff",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Diff retrieved successfully", body = ApiResponse<crate::pb::repo::GetDiffResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_diff(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_diff(
&session,
&path.workspace_name,
&path.repo_name,
&query.base,
&query.head,
query.page_size.unwrap_or(30),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+54
View File
@@ -0,0 +1,54 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub base: String,
pub head: String,
}
/// Get diff statistics between two revisions
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/diff-stats",
tag = "Git",
operation_id = "gitDiffStats",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Diff stats retrieved successfully", body = ApiResponse<crate::pb::repo::DiffStats>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_diff_stats(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_diff_stats(
&session,
&path.workspace_name,
&path.repo_name,
&query.base,
&query.head,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+41
View File
@@ -0,0 +1,41 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Check if repository exists
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/exists",
tag = "Git",
operation_id = "gitRepoExists",
params(PathParams),
responses(
(status = 200, description = "Repository existence check completed", body = ApiResponse<bool>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_exists(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_repo_exists(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+41
View File
@@ -0,0 +1,41 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Run garbage collection
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/garbage-collect",
tag = "Git",
operation_id = "gitGarbageCollect",
params(PathParams),
responses(
(status = 200, description = "Garbage collection completed", body = ApiResponse<crate::pb::repo::RepositoryMaintenanceResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_gc(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_garbage_collect(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+48
View File
@@ -0,0 +1,48 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub branch_name: String,
}
/// Get a branch
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/branches/{branch_name}",
tag = "Git",
operation_id = "gitGetBranch",
params(PathParams),
responses(
(status = 200, description = "Branch retrieved successfully", body = ApiResponse<crate::pb::repo::Branch>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Branch not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_get_branch(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_get_branch(
&session,
&path.workspace_name,
&path.repo_name,
&path.branch_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+48
View File
@@ -0,0 +1,48 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub revision: String,
}
/// Get a single commit
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits/{revision}",
tag = "Git",
operation_id = "gitGetCommit",
params(PathParams),
responses(
(status = 200, description = "Commit retrieved successfully", body = ApiResponse<crate::pb::repo::Commit>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Commit not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_get_commit(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_get_commit(
&session,
&path.workspace_name,
&path.repo_name,
&path.revision,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+45
View File
@@ -0,0 +1,45 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub tag_name: String,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/tags/{tag_name}",
tag = "Git",
operation_id = "gitGetTag",
params(PathParams),
responses(
(status = 200, description = "Tag details", body = ApiResponse<crate::pb::repo::Tag>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_get_tag(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_get_tag(
&session,
&path.workspace_name,
&path.repo_name,
&path.tag_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+41
View File
@@ -0,0 +1,41 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Check repository health
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/health",
tag = "Git",
operation_id = "gitRepoHealth",
params(PathParams),
responses(
(status = 200, description = "Repository health check completed", body = ApiResponse<crate::pb::repo::RepositoryHealthResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_health(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_repo_health(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+41
View File
@@ -0,0 +1,41 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Get repository info
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/info",
tag = "Git",
operation_id = "gitRepoInfo",
params(PathParams),
responses(
(status = 200, description = "Repository info retrieved successfully", body = ApiResponse<crate::pb::repo::Repository>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_info(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_repo_info(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+39
View File
@@ -0,0 +1,39 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/license",
tag = "Git",
operation_id = "gitLicense",
params(PathParams),
responses(
(status = 200, description = "License detection result", body = ApiResponse<crate::pb::repo::FindLicenseResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_license(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_find_license(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+56
View File
@@ -0,0 +1,56 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub pattern: Option<String>,
pub page_size: Option<u32>,
pub page_token: Option<String>,
}
/// List branches
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/branches",
tag = "Git",
operation_id = "gitListBranches",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Branches listed successfully", body = ApiResponse<crate::pb::repo::ListBranchesResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_list_branches(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_list_branches(
&session,
&path.workspace_name,
&path.repo_name,
query.pattern.clone(),
query.page_size.unwrap_or(30),
query.page_token.clone(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+56
View File
@@ -0,0 +1,56 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub revision: String,
pub path: Option<String>,
pub page_size: Option<u32>,
}
/// List commits
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/commits",
tag = "Git",
operation_id = "gitListCommits",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Commits listed successfully", body = ApiResponse<crate::pb::repo::ListCommitsResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_list_commits(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_list_commits(
&session,
&path.workspace_name,
&path.repo_name,
&query.revision,
query.path.clone(),
query.page_size.unwrap_or(30),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+54
View File
@@ -0,0 +1,54 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::repo::git::merge::MergeParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Merge branches
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/merge",
tag = "Git",
operation_id = "gitMerge",
params(PathParams),
request_body(
content = MergeParams,
description = "Merge parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Merge completed successfully", body = ApiResponse<crate::pb::repo::MergeResult>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 409, description = "Merge conflict", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_merge(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<MergeParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_merge(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+54
View File
@@ -0,0 +1,54 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub target: String,
pub source: String,
}
/// Check if a merge is possible
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/merge-check",
tag = "Git",
operation_id = "gitMergeCheck",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Merge check completed successfully", body = ApiResponse<crate::pb::repo::MergeResult>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_merge_check(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_check_merge(
&session,
&path.workspace_name,
&path.repo_name,
&query.target,
&query.source,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+54
View File
@@ -0,0 +1,54 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::repo::git::merge::RebaseParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Rebase a branch
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/rebase",
tag = "Git",
operation_id = "gitRebase",
params(PathParams),
request_body(
content = RebaseParams,
description = "Rebase parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Rebase completed successfully", body = ApiResponse<crate::pb::repo::RebaseResult>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 409, description = "Rebase conflict", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_rebase(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<RebaseParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_rebase(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+53
View File
@@ -0,0 +1,53 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub branch_name: String,
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct RenameBody {
pub new_name: String,
}
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/branches/{branch_name}/rename",
tag = "Git",
operation_id = "gitRenameBranch",
params(PathParams),
request_body(content = RenameBody),
responses(
(status = 200, description = "Branch renamed", body = ApiResponse<crate::pb::repo::Branch>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_rename_branch(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
body: web::Json<RenameBody>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_rename_branch(
&session,
&path.workspace_name,
&path.repo_name,
&path.branch_name,
&body.new_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+53
View File
@@ -0,0 +1,53 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::repo::git::merge::RevertParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Revert a commit
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/revert",
tag = "Git",
operation_id = "gitRevert",
params(PathParams),
request_body(
content = RevertParams,
description = "Revert parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Revert completed successfully", body = ApiResponse<crate::pb::repo::CreateCommitResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_revert(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<RevertParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_revert(
&session,
&path.workspace_name,
&path.repo_name,
params.into_inner(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+98
View File
@@ -0,0 +1,98 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct SearchQueryParams {
pub q: String,
pub revision: Option<String>,
pub max_results: Option<u32>,
pub case_sensitive: Option<bool>,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct SearchFilesQueryParams {
pub q: String,
pub revision: Option<String>,
pub max_results: Option<u32>,
pub recursive: Option<bool>,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/search/content",
tag = "Git",
operation_id = "gitSearchContent",
params(PathParams, SearchQueryParams),
responses(
(status = 200, description = "Search results", body = ApiResponse<crate::pb::repo::SearchFilesByContentResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_search_content(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<SearchQueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_search_content(
&session,
&path.workspace_name,
&path.repo_name,
&query.q,
query.revision.as_deref(),
query.max_results.unwrap_or(100),
query.case_sensitive.unwrap_or(false),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/search/files",
tag = "Git",
operation_id = "gitSearchFiles",
params(PathParams, SearchFilesQueryParams),
responses(
(status = 200, description = "File search results", body = ApiResponse<crate::pb::repo::SearchFilesByNameResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_search_files(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<SearchFilesQueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_search_files(
&session,
&path.workspace_name,
&path.repo_name,
&query.q,
query.revision.as_deref(),
query.max_results.unwrap_or(100),
query.recursive.unwrap_or(true),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+41
View File
@@ -0,0 +1,41 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
/// Get repository statistics
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/stats",
tag = "Git",
operation_id = "gitRepoStats",
params(PathParams),
responses(
(status = 200, description = "Repository statistics retrieved successfully", body = ApiResponse<crate::pb::repo::RepositoryStatistics>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_stats(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_repo_stats(&session, &path.workspace_name, &path.repo_name)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+54
View File
@@ -0,0 +1,54 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub pattern: Option<String>,
pub page_size: Option<u32>,
}
/// List tags
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/tags",
tag = "Git",
operation_id = "gitListTags",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Tags listed successfully", body = ApiResponse<crate::pb::repo::ListTagsResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_tags(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_list_tags(
&session,
&path.workspace_name,
&path.repo_name,
query.pattern.clone(),
query.page_size.unwrap_or(30),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+58
View File
@@ -0,0 +1,58 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub revision: String,
pub path: Option<String>,
pub recursive: Option<bool>,
pub page_size: Option<u32>,
}
/// List tree contents
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/tree",
tag = "Git",
operation_id = "gitListTree",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Tree listed successfully", body = ApiResponse<crate::pb::repo::ListTreeResponse>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_tree(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_list_tree(
&session,
&path.workspace_name,
&path.repo_name,
&query.revision,
query.path.clone(),
query.recursive.unwrap_or(false),
query.page_size.unwrap_or(30),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+45
View File
@@ -0,0 +1,45 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub tag_name: String,
}
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/git/tags/{tag_name}/verify",
tag = "Git",
operation_id = "gitVerifyTag",
params(PathParams),
responses(
(status = 200, description = "Tag signature verification result", body = ApiResponse<crate::pb::repo::VerifiedSignature>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn git_verify_tag(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.git_verify_tag(
&session,
&path.workspace_name,
&path.repo_name,
&path.tag_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+218
View File
@@ -0,0 +1,218 @@
pub mod git_archive;
pub mod git_blame;
pub mod git_blob;
pub mod git_cherry_pick;
pub mod git_commit_extras2;
pub mod git_commit_stats;
pub mod git_compare;
pub mod git_compare_branch;
pub mod git_conflicts;
pub mod git_count_commits;
pub mod git_count_diverging;
pub mod git_create_branch;
pub mod git_create_commit;
pub mod git_create_tag;
pub mod git_delete_branch;
pub mod git_delete_tag;
pub mod git_diff;
pub mod git_diff_extras;
pub mod git_diff_stats;
pub mod git_exists;
pub mod git_gc;
pub mod git_get_branch;
pub mod git_get_commit;
pub mod git_get_tag;
pub mod git_health;
pub mod git_info;
pub mod git_license;
pub mod git_list_branches;
pub mod git_list_commits;
pub mod git_merge;
pub mod git_merge_check;
pub mod git_rebase;
pub mod git_rename_branch;
pub mod git_repository_extras;
pub mod git_revert;
pub mod git_search;
pub mod git_stats;
pub mod git_tags;
pub mod git_tree;
pub mod git_tree_extras;
pub mod git_verify_tag;
use actix_web::web;
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/git")
.route(
"/commits",
web::get().to(git_list_commits::git_list_commits),
)
.route(
"/commits/{revision}",
web::get().to(git_get_commit::git_get_commit),
)
.route(
"/commits/{revision}/stats",
web::get().to(git_commit_stats::git_commit_stats),
)
.route(
"/commits",
web::post().to(git_create_commit::git_create_commit),
)
.route(
"/commits/count",
web::get().to(git_count_commits::git_count_commits),
)
.route("/diff", web::get().to(git_diff::git_diff))
.route("/diff-stats", web::get().to(git_diff_stats::git_diff_stats))
.route("/compare", web::get().to(git_compare::git_compare))
.route(
"/compare/diverging",
web::get().to(git_count_diverging::git_count_diverging),
)
.route("/archive", web::get().to(git_archive::git_archive))
.route("/license", web::get().to(git_license::git_license))
.route(
"/search/content",
web::get().to(git_search::git_search_content),
)
.route("/search/files", web::get().to(git_search::git_search_files))
.route(
"/branches",
web::get().to(git_list_branches::git_list_branches),
)
.route(
"/branches/{branch_name}",
web::get().to(git_get_branch::git_get_branch),
)
.route(
"/branches/{branch_name}/compare",
web::get().to(git_compare_branch::git_compare_branch),
)
.route(
"/branches/{branch_name}/rename",
web::post().to(git_rename_branch::git_rename_branch),
)
.route(
"/branches",
web::post().to(git_create_branch::git_create_branch),
)
.route(
"/branches/{branch_name}",
web::delete().to(git_delete_branch::git_delete_branch),
)
.route(
"/merge-check",
web::get().to(git_merge_check::git_merge_check),
)
.route("/merge", web::post().to(git_merge::git_merge))
.route("/rebase", web::post().to(git_rebase::git_rebase))
.route(
"/cherry-pick",
web::post().to(git_cherry_pick::git_cherry_pick),
)
.route("/revert", web::post().to(git_revert::git_revert))
.route("/conflicts", web::get().to(git_conflicts::git_conflicts))
.route("/tree", web::get().to(git_tree::git_tree))
.route("/blobs", web::get().to(git_blob::git_blob))
.route("/blame", web::get().to(git_blame::git_blame))
.route("/tags", web::get().to(git_tags::git_tags))
.route("/tags", web::post().to(git_create_tag::git_create_tag))
.route("/tags/{tag_name}", web::get().to(git_get_tag::git_get_tag))
.route(
"/tags/{tag_name}/verify",
web::post().to(git_verify_tag::git_verify_tag),
)
.route(
"/tags/{tag_name}",
web::delete().to(git_delete_tag::git_delete_tag),
)
.route("/info", web::get().to(git_info::git_info))
.route("/exists", web::get().to(git_exists::git_exists))
.route("/stats", web::get().to(git_stats::git_stats))
.route("/health", web::get().to(git_health::git_health))
.route("/garbage-collect", web::post().to(git_gc::git_gc))
.route(
"/default-branch",
web::get().to(git_repository_extras::git_default_branch),
)
.route(
"/object-format",
web::get().to(git_repository_extras::git_object_format),
)
.route(
"/size",
web::get().to(git_repository_extras::git_repository_size),
)
.route(
"/objects-size",
web::post().to(git_repository_extras::git_objects_size),
)
.route(
"/check-objects",
web::post().to(git_repository_extras::git_check_objects),
)
.route(
"/merge-base",
web::get().to(git_repository_extras::git_merge_base),
)
.route(
"/archive/entries",
web::get().to(git_repository_extras::git_archive_entries),
)
.route(
"/commits/{revision}/ancestors",
web::get().to(git_repository_extras::git_commit_ancestors),
)
.route(
"/find-commit",
web::get().to(git_commit_extras2::git_find_commit),
)
.route(
"/commits/by-oid",
web::post().to(git_commit_extras2::git_commits_by_oid),
)
.route(
"/commit-is-ancestor",
web::get().to(git_commit_extras2::git_commit_is_ancestor),
)
.route(
"/last-commit",
web::get().to(git_commit_extras2::git_last_commit),
)
.route(
"/commits/search",
web::get().to(git_commit_extras2::git_commits_by_message),
)
.route("/raw-blob", web::get().to(git_tree_extras::git_raw_blob))
.route(
"/file-metadata",
web::get().to(git_tree_extras::git_file_metadata),
)
.route(
"/find-files",
web::get().to(git_tree_extras::git_find_files),
)
.route("/get-tree", web::get().to(git_tree_extras::git_get_tree))
.route(
"/commit-diff/{revision}",
web::get().to(git_diff_extras::git_commit_diff),
)
.route("/patch", web::get().to(git_diff_extras::git_patch))
.route("/raw-diff", web::get().to(git_diff_extras::git_raw_diff))
.route(
"/changed-paths",
web::get().to(git_diff_extras::git_changed_paths),
)
.route(
"/stream-blame",
web::get().to(git_diff_extras::git_stream_blame),
)
.route(
"/resolve-conflicts",
web::post().to(git_diff_extras::git_resolve_conflicts),
),
);
}
+19 -3
View File
@@ -1,10 +1,12 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use uuid::Uuid;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::Repo;
use crate::models::base_info::{resolve_users, resolve_workspaces};
use crate::models::repos::RepoDetail;
use crate::service::AppService;
use crate::session::Session;
@@ -41,7 +43,7 @@ pub struct QueryParams {
QueryParams,
),
responses(
(status = 200, description = "Repositories listed successfully. Returns an array of repository objects with metadata.", body = ApiResponse<Vec<Repo>>),
(status = 200, description = "Repositories listed successfully. Returns an array of repository objects with metadata.", body = ApiResponse<Vec<RepoDetail>>),
(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),
@@ -67,5 +69,19 @@ pub async fn list(
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(repos)))
let db = &service.ctx.db;
let owner_ids: Vec<Uuid> = repos.iter().map(|r| r.owner_id).collect();
let ws_ids: Vec<Uuid> = repos.iter().map(|r| r.workspace_id).collect();
let users = resolve_users(db, &owner_ids).await?;
let workspaces = resolve_workspaces(db, &ws_ids).await?;
let details: Vec<RepoDetail> = repos
.into_iter()
.map(|r| {
let owner = users.get(&r.owner_id).cloned().unwrap_or_default();
let workspace = workspaces.get(&r.workspace_id).cloned().unwrap_or_default();
r.into_detail(owner, workspace)
})
.collect();
Ok(HttpResponse::Ok().json(ApiResponse::new(details)))
}
+19 -29
View File
@@ -4,36 +4,21 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
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",
@@ -41,15 +26,11 @@ pub struct QueryParams {
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),
(status = 200, description = "Branches listed successfully", body = ApiResponse<crate::pb::repo::ListBranchesResponse>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Repository not found", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
security(("session_cookie" = []))
)]
pub async fn list_branches(
service: web::Data<AppService>,
@@ -57,16 +38,25 @@ pub async fn list_branches(
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let branches = service
let limit = query.limit.unwrap_or(50).clamp(1, 100);
let offset = query.offset.unwrap_or(0).max(0);
let page_size = limit as u32;
let page_token = if offset > 0 {
format!("{offset}")
} else {
String::new()
};
let result = service
.repo
.repo_branches(
.git_list_branches(
&session,
&path.workspace_name,
&path.repo_name,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
None,
page_size,
Some(page_token),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(branches)))
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+12 -28
View File
@@ -4,35 +4,21 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
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",
@@ -40,15 +26,11 @@ pub struct QueryParams {
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),
(status = 200, description = "Tags listed", body = ApiResponse<crate::pb::repo::ListTagsResponse>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Repository not found", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
security(("session_cookie" = []))
)]
pub async fn list_tags(
service: web::Data<AppService>,
@@ -56,16 +38,18 @@ pub async fn list_tags(
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let tags = service
let limit = query.limit.unwrap_or(50).clamp(1, 100);
let page_size = limit as u32;
let result = service
.repo
.repo_tags(
.git_list_tags(
&session,
&path.workspace_name,
&path.repo_name,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
None,
page_size,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(tags)))
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+95 -9
View File
@@ -5,6 +5,7 @@ pub mod add_deploy_key;
pub mod add_member;
pub mod archive;
pub mod check_branch_merge;
pub mod contributors;
pub mod create;
pub mod create_branch;
pub mod create_commit_comment;
@@ -17,14 +18,24 @@ pub mod create_webhook;
pub mod delete;
pub mod delete_branch;
pub mod delete_deploy_key;
pub mod delete_fork;
pub mod delete_protection_rule;
pub mod delete_release;
pub mod delete_tag;
pub mod delete_webhook;
pub mod fork_repo;
pub mod get;
pub mod get_branch;
pub mod get_commit_status;
pub mod get_deploy_key;
pub mod get_invitation;
pub mod get_member;
pub mod get_protection_rule;
pub mod get_release;
pub mod get_stats;
pub mod get_tag;
pub mod get_webhook;
pub mod git;
pub mod leave_repo;
pub mod list;
pub mod list_branches;
@@ -42,27 +53,33 @@ pub mod list_watchers;
pub mod list_webhooks;
pub mod match_protection;
pub mod refresh_stats;
pub mod release_assets;
pub mod remove_member;
pub mod repo_webhook_deliveries;
pub mod repo_webhook_retry;
pub mod resolve_commit_comment;
pub mod revoke_invitation;
pub mod set_branch_protection;
pub mod set_default_branch;
pub mod star_repo;
pub mod sync_fork;
pub mod topics;
pub mod transfer_owner;
pub mod unarchive;
pub mod unstar_repo;
pub mod unwatch_repo;
pub mod update;
pub mod update_commit_comment;
pub mod update_member_role;
pub mod update_protection_rule;
pub mod update_release;
pub mod update_tag;
pub mod update_webhook;
pub mod watch_repo;
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/workspaces/{workspace_name}/repos")
web::scope("")
.route("", web::get().to(list::list))
.route("", web::post().to(create::create))
.route("/{repo_name}", web::get().to(get::get))
@@ -86,21 +103,33 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
web::post().to(create_branch::create_branch),
)
.route(
"/{repo_name}/branches/{branch_id}/default",
"/{repo_name}/branches/{branch_name}/default",
web::put().to(set_default_branch::set_default_branch),
)
.route(
"/{repo_name}/branches/{branch_id}/protection",
"/{repo_name}/branches/{branch_name}/protection",
web::put().to(set_branch_protection::set_branch_protection),
)
.route(
"/{repo_name}/branches/{branch_id}",
"/{repo_name}/branches/{branch_name}",
web::get().to(get_branch::get_branch),
)
.route(
"/{repo_name}/branches/{branch_name}",
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}",
"/{repo_name}/tags/{tag_name}",
web::get().to(get_tag::get_tag),
)
.route(
"/{repo_name}/tags/{tag_name}",
web::put().to(update_tag::update_tag),
)
.route(
"/{repo_name}/tags/{tag_name}",
web::delete().to(delete_tag::delete_tag),
)
.route(
@@ -111,6 +140,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
"/{repo_name}/releases",
web::post().to(create_release::create_release),
)
.route(
"/{repo_name}/releases/{release_id}",
web::get().to(get_release::get_release),
)
.route(
"/{repo_name}/releases/{release_id}",
web::put().to(update_release::update_release),
@@ -121,7 +154,12 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
)
.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}/fork",
web::delete().to(delete_fork::delete_fork),
)
.route("/{repo_name}/sync", web::post().to(sync_fork::sync_fork))
.route("/{repo_name}/topics", web::put().to(topics::update_topics))
.route("/{repo_name}/star", web::post().to(star_repo::star_repo))
.route(
"/{repo_name}/star",
@@ -152,6 +190,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
"/{repo_name}/members/{member_id}/role",
web::put().to(update_member_role::update_member_role),
)
.route(
"/{repo_name}/members/{member_id}",
web::get().to(get_member::get_member),
)
.route(
"/{repo_name}/members/{member_id}",
web::delete().to(remove_member::remove_member),
@@ -165,6 +207,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
"/{repo_name}/invitations",
web::post().to(create_invitation::create_invitation),
)
.route(
"/{repo_name}/invitations/{invitation_id}",
web::get().to(get_invitation::get_invitation),
)
.route(
"/{repo_name}/invitations/{invitation_id}",
web::delete().to(revoke_invitation::revoke_invitation),
@@ -177,6 +223,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
"/{repo_name}/deploy-keys",
web::post().to(add_deploy_key::add_deploy_key),
)
.route(
"/{repo_name}/deploy-keys/{key_id}",
web::get().to(get_deploy_key::get_deploy_key),
)
.route(
"/{repo_name}/deploy-keys/{key_id}",
web::delete().to(delete_deploy_key::delete_deploy_key),
@@ -189,6 +239,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
"/{repo_name}/webhooks",
web::post().to(create_webhook::create_webhook),
)
.route(
"/{repo_name}/webhooks/{webhook_id}",
web::get().to(get_webhook::get_webhook),
)
.route(
"/{repo_name}/webhooks/{webhook_id}",
web::put().to(update_webhook::update_webhook),
@@ -197,6 +251,14 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
"/{repo_name}/webhooks/{webhook_id}",
web::delete().to(delete_webhook::delete_webhook),
)
.route(
"/{repo_name}/webhooks/{webhook_id}/deliveries",
web::get().to(repo_webhook_deliveries::repo_webhook_deliveries),
)
.route(
"/{repo_name}/webhooks/{webhook_id}/deliveries/{delivery_id}/retry",
web::post().to(repo_webhook_retry::repo_webhook_retry),
)
.route(
"/{repo_name}/protection-rules",
web::get().to(list_protection_rules::list_protection_rules),
@@ -229,6 +291,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
"/{repo_name}/commits/{push_commit_id}/statuses",
web::get().to(list_commit_statuses::list_commit_statuses),
)
.route(
"/{repo_name}/commits/{push_commit_id}/statuses/{status_id}",
web::get().to(get_commit_status::get_commit_status),
)
.route(
"/{repo_name}/commit-statuses",
web::post().to(create_commit_status::create_commit_status),
@@ -245,14 +311,34 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
"/{repo_name}/commit-comments/{comment_id}/resolve",
web::post().to(resolve_commit_comment::resolve_commit_comment),
)
.route(
"/{repo_name}/commit-comments/{comment_id}",
web::put().to(update_commit_comment::update_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(
"/{repo_name}/contributors",
web::get().to(contributors::list_contributors),
)
.route(
"/{repo_name}/releases/{release_id}/assets",
web::post().to(release_assets::upload_asset),
)
.route(
"/{repo_name}/releases/{release_id}/assets",
web::get().to(release_assets::list_assets),
)
.route(
"/{repo_name}/releases/{release_id}/assets/{asset_id}",
web::delete().to(release_assets::delete_asset),
)
.route(
"/{repo_name}/releases/{release_id}/assets/{asset_id}/download",
web::get().to(release_assets::download_asset),
),
)
.route(
"/repos/invitations/accept",
web::post().to(accept_invitation::accept_invitation),
);
}
+165
View File
@@ -0,0 +1,165 @@
use actix_multipart::Multipart;
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::api::user::upload_avatar::parse_avatar_field;
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub release_id: uuid::Uuid,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct AssetPathParams {
pub workspace_name: String,
pub repo_name: String,
pub release_id: uuid::Uuid,
pub asset_id: uuid::Uuid,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct ListQueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/releases/{release_id}/assets",
tag = "Repos",
operation_id = "repoUploadReleaseAsset",
params(PathParams),
request_body(content_type = "multipart/form-data"),
responses(
(status = 201, description = "Asset uploaded", body = ApiResponse<crate::service::repo::release_assets::ReleaseAssetData>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Release not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn upload_asset(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
payload: Multipart,
) -> Result<HttpResponse, AppError> {
let (data, content_type, file_name) = parse_avatar_field(payload).await?;
let filename = file_name.unwrap_or_else(|| "asset.bin".to_string());
let result = service
.repo
.repo_upload_release_asset(
&session,
&path.workspace_name,
&path.repo_name,
path.release_id,
&filename,
data,
content_type
.as_deref()
.unwrap_or("application/octet-stream"),
)
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/releases/{release_id}/assets",
tag = "Repos",
operation_id = "repoListReleaseAssets",
params(PathParams, ListQueryParams),
responses(
(status = 200, description = "List of release assets", body = ApiResponse<Vec<crate::service::repo::release_assets::ReleaseAssetData>>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn list_assets(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<ListQueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.repo_list_release_assets(
&session,
&path.workspace_name,
&path.repo_name,
path.release_id,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
#[utoipa::path(
delete,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/releases/{release_id}/assets/{asset_id}",
tag = "Repos",
operation_id = "repoDeleteReleaseAsset",
params(AssetPathParams),
responses(
(status = 200, description = "Asset deleted", body = ApiResponse<String>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn delete_asset(
service: web::Data<AppService>,
session: Session,
path: web::Path<AssetPathParams>,
) -> Result<HttpResponse, AppError> {
service
.repo
.repo_delete_release_asset(
&session,
&path.workspace_name,
&path.repo_name,
path.release_id,
path.asset_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("asset deleted".to_string())))
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/releases/{release_id}/assets/{asset_id}/download",
tag = "Repos",
operation_id = "repoDownloadReleaseAsset",
params(AssetPathParams),
responses(
(status = 302, description = "Redirect to download URL"),
(status = 404, description = "Not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn download_asset(
service: web::Data<AppService>,
session: Session,
path: web::Path<AssetPathParams>,
) -> Result<HttpResponse, AppError> {
let url = service
.repo
.repo_get_release_asset_download_url(
&session,
&path.workspace_name,
&path.repo_name,
path.release_id,
path.asset_id,
)
.await?;
Ok(HttpResponse::Found()
.insert_header(("Location", url))
.finish())
}
+56
View File
@@ -0,0 +1,56 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub webhook_id: uuid::Uuid,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
#[utoipa::path(
get,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/webhooks/{webhook_id}/deliveries",
tag = "Repos",
operation_id = "repoListWebhookDeliveries",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Webhook deliveries listed successfully", body = ApiResponse<String>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Webhook not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn repo_webhook_deliveries(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let deliveries = service
.repo
.repo_webhook_deliveries(
&session,
&path.workspace_name,
&path.repo_name,
path.webhook_id,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(deliveries)))
}
+51
View File
@@ -0,0 +1,51 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub webhook_id: uuid::Uuid,
pub delivery_id: uuid::Uuid,
}
#[utoipa::path(
post,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/webhooks/{webhook_id}/deliveries/{delivery_id}/retry",
tag = "Repos",
operation_id = "repoRetryWebhookDelivery",
params(PathParams),
responses(
(status = 200, description = "Webhook delivery retried successfully", body = ApiResponse<String>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Webhook or delivery not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn repo_webhook_retry(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.repo
.repo_retry_webhook_delivery(
&session,
&path.workspace_name,
&path.repo_name,
path.webhook_id,
path.delivery_id,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(
"Webhook delivery retried successfully".to_string(),
)))
}
+23 -36
View File
@@ -9,52 +9,29 @@ 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,
pub branch_name: String,
}
#[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",
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches/{branch_name}/protection",
tag = "Repos",
operation_id = "repoSetBranchProtection",
params(PathParams),
request_body(
content = SetBranchProtectionParams,
description = "Branch protection parameters",
content_type = "application/json"
),
request_body(content = SetBranchProtectionParams),
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),
(status = 200, description = "Branch protection set", body = ApiResponse<String>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Branch not found", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
security(("session_cookie" = []))
)]
pub async fn set_branch_protection(
service: web::Data<AppService>,
@@ -62,18 +39,28 @@ pub async fn set_branch_protection(
path: web::Path<PathParams>,
params: web::Json<SetBranchProtectionParams>,
) -> Result<HttpResponse, AppError> {
service
// Verify branch exists via gRPC
let _ = service
.repo
.repo_set_branch_protection(
.git_get_branch(
&session,
&path.workspace_name,
&path.repo_name,
path.branch_id,
&path.branch_name,
)
.await?;
// Update DB protection flag (platform metadata, no gRPC equivalent)
service
.repo
.repo_set_branch_protection_by_name(
&session,
&path.workspace_name,
&path.repo_name,
&path.branch_name,
params.protected,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(
"Branch protection rules set successfully".to_string(),
)))
Ok(HttpResponse::Ok().json(ApiResponse::new("Branch protection updated".to_string())))
}
+22 -29
View File
@@ -9,57 +9,50 @@ 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,
pub branch_name: String,
}
/// 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",
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/branches/{branch_name}/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),
(status = 200, description = "Default branch set", body = ApiResponse<String>),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Branch not found", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
security(("session_cookie" = []))
)]
pub async fn set_default_branch(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
// 1. Call gRPC to update the actual git HEAD ref
let _ = service
.repo
.repo_set_default_branch(
.git_set_default_branch(
&session,
&path.workspace_name,
&path.repo_name,
path.branch_id,
&path.branch_name,
)
.await;
// 2. Update DB metadata (platform-level default branch tracking)
service
.repo
.repo_set_default_branch_by_name(
&session,
&path.workspace_name,
&path.repo_name,
&path.branch_name,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(
"Default branch set successfully".to_string(),
)))
Ok(HttpResponse::Ok().json(ApiResponse::new("Default branch set".to_string())))
}
+67
View File
@@ -0,0 +1,67 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::service::repo::core::UpdateRepoParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct TopicsBody {
pub topics: Vec<String>,
}
#[utoipa::path(
put,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/topics",
tag = "Repos",
operation_id = "repoUpdateTopics",
params(PathParams),
request_body(content = TopicsBody),
responses(
(status = 200, description = "Topics updated", body = ApiResponse<Vec<String>>),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 404, description = "Repo not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn update_topics(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
body: web::Json<TopicsBody>,
) -> Result<HttpResponse, AppError> {
let result = service
.repo
.repo_update(
&session,
&path.workspace_name,
&path.repo_name,
UpdateRepoParams {
name: None,
description: None,
visibility: None,
default_branch: None,
topics: Some(body.topics.clone()),
homepage: None,
has_issues: None,
has_wiki: None,
has_pull_requests: None,
allow_forking: None,
allow_merge_commit: None,
allow_squash_merge: None,
allow_rebase_merge: None,
delete_branch_on_merge: None,
},
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result.topics)))
}
+14 -3
View File
@@ -4,7 +4,8 @@ use utoipa::{IntoParams, ToSchema};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::Repo;
use crate::models::base_info::{resolve_users, resolve_workspaces};
use crate::models::repos::RepoDetail;
use crate::service::AppService;
use crate::session::Session;
@@ -46,7 +47,7 @@ pub struct TransferOwnerParams {
content_type = "application/json"
),
responses(
(status = 200, description = "Ownership transferred successfully. Returns the repository with updated owner information.", body = ApiResponse<Repo>),
(status = 200, description = "Ownership transferred successfully. Returns the repository with updated owner information.", body = ApiResponse<RepoDetail>),
(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),
@@ -73,5 +74,15 @@ pub async fn transfer_owner(
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(repo)))
let db = &service.ctx.db;
let users = resolve_users(db, &[repo.owner_id]).await?;
let workspaces = resolve_workspaces(db, &[repo.workspace_id]).await?;
let owner = users.get(&repo.owner_id).cloned().unwrap_or_default();
let workspace = workspaces
.get(&repo.workspace_id)
.cloned()
.unwrap_or_default();
let detail = repo.into_detail(owner, workspace);
Ok(HttpResponse::Ok().json(ApiResponse::new(detail)))
}
+14 -3
View File
@@ -4,7 +4,8 @@ use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::Repo;
use crate::models::base_info::{resolve_users, resolve_workspaces};
use crate::models::repos::RepoDetail;
use crate::service::AppService;
use crate::service::repo::core::UpdateRepoParams;
use crate::session::Session;
@@ -42,7 +43,7 @@ pub struct PathParams {
content_type = "application/json"
),
responses(
(status = 200, description = "Repository updated successfully. Returns the updated repository with full metadata.", body = ApiResponse<Repo>),
(status = 200, description = "Repository updated successfully. Returns the updated repository with full metadata.", body = ApiResponse<RepoDetail>),
(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),
@@ -70,5 +71,15 @@ pub async fn update(
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(repo)))
let db = &service.ctx.db;
let users = resolve_users(db, &[repo.owner_id]).await?;
let workspaces = resolve_workspaces(db, &[repo.workspace_id]).await?;
let owner = users.get(&repo.owner_id).cloned().unwrap_or_default();
let workspace = workspaces
.get(&repo.workspace_id)
.cloned()
.unwrap_or_default();
let detail = repo.into_detail(owner, workspace);
Ok(HttpResponse::Ok().json(ApiResponse::new(detail)))
}
+61
View File
@@ -0,0 +1,61 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::repos::RepoCommitComment;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub comment_id: uuid::Uuid,
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct UpdateCommitCommentParams {
pub body: String,
}
#[utoipa::path(
put,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/commit-comments/{comment_id}",
tag = "Repos",
operation_id = "repoUpdateCommitComment",
params(PathParams),
request_body(
content = UpdateCommitCommentParams,
description = "Commit comment update parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Commit comment updated successfully", body = ApiResponse<RepoCommitComment>),
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Commit comment not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn update_commit_comment(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<UpdateCommitCommentParams>,
) -> Result<HttpResponse, AppError> {
let comment = service
.repo
.repo_update_commit_comment(
&session,
&path.workspace_name,
&path.repo_name,
path.comment_id,
&params.body,
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(comment)))
}
+82
View File
@@ -0,0 +1,82 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub workspace_name: String,
pub repo_name: String,
pub tag_name: String,
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct UpdateTagBody {
pub new_name: Option<String>,
pub message: Option<String>,
}
#[utoipa::path(
put,
path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/tags/{tag_name}",
tag = "Repos",
operation_id = "repoUpdateTag",
params(PathParams),
request_body(content = UpdateTagBody),
responses(
(status = 200, description = "Tag updated (delete+recreate if renamed)", body = ApiResponse<crate::pb::repo::Tag>),
(status = 400, description = "Invalid parameters", body = ApiErrorResponse),
(status = 401, description = "Authentication required", body = ApiErrorResponse),
(status = 404, description = "Tag not found", body = ApiErrorResponse),
),
security(("session_cookie" = []))
)]
pub async fn update_tag(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
body: web::Json<UpdateTagBody>,
) -> Result<HttpResponse, AppError> {
let msg = body.message.clone();
let new_name = body.new_name.as_deref().unwrap_or(&path.tag_name);
let tag = service
.repo
.git_get_tag(
&session,
&path.workspace_name,
&path.repo_name,
&path.tag_name,
)
.await?;
service
.repo
.git_delete_tag(
&session,
&path.workspace_name,
&path.repo_name,
&path.tag_name,
)
.await?;
let target_hex = tag.target_oid.map(|o| o.hex).unwrap_or_default();
let result = service
.repo
.git_create_tag(
&session,
&path.workspace_name,
&path.repo_name,
new_name,
&target_hex,
msg.clone(),
msg.is_some(),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}