feat(lib): add new handlers and export views (db users_loader, repo readme/lifecycle/tree/PR/release, issue reopen, export sidebar/basic)
zhenyi committed
Added lib/db/user/users_loader.rs lib/db/user/users_loader.rs +30 −0 View lib/db/user/users_loader.rs
@@ -0,0 +1,30 @@
1use std::collections::HashMap;
2
3use sea_orm::{DbErr, EntityTrait, QueryFilter};
4use uuid::Uuid;
5
6use super::users as users_entity;
7use crate::conn::Db;
8
9pub async fn load_users(
10 db: &Db,
11 ids: &[Uuid],
12) -> Result<HashMap<Uuid, users_entity::Model>, DbErr> {
13 if ids.is_empty() {
14 return Ok(HashMap::new());
15 }
16 let mut ids: Vec<Uuid> = ids.to_vec();
17 ids.sort();
18 ids.dedup();
19 let conn = db.reader();
20 let users: Vec<users_entity::Model> = users_entity::Entity::find()
21 .filter(users_entity::COLUMN.uid.is_in(ids))
22 .all(&conn)
23 .await?;
24 Ok(users.into_iter().map(|u| (u.uid, u)).collect())
25}
26
27pub async fn load_user(db: &Db, id: Uuid) -> Result<Option<users_entity::Model>, DbErr> {
28 let conn = db.reader();
29 users_entity::Entity::find_by_id(id).one(&conn).await
30}
Added lib/export/repo/readme.rs lib/export/repo/readme.rs +16 −0 View lib/export/repo/readme.rs
@@ -0,0 +1,16 @@
1//! Repository README view (layout.pen repository layout README Card)
2
3use serde::{Deserialize, Serialize};
4use utoipa::ToSchema;
5use uuid::Uuid;
6
7#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
8pub struct ReadmeView {
9 pub repo_id: Uuid,
10 pub path: String,
11 pub ref_: Option<String>,
12 pub content: String,
13 pub rendered_html: Option<String>,
14 pub size: Option<i64>,
15 pub sha: Option<String>,
16}
Added lib/export/repo/repo_features.rs lib/export/repo/repo_features.rs +37 −0 View lib/export/repo/repo_features.rs
@@ -0,0 +1,37 @@
1//! Repo features read/write view (used by layout.pen repo settings FeaturesCard)
2//!
3//! Mirrors the booleans already present on `RepoView` (`has_wiki`, `has_issues`,
4//! `has_pull_requests`, ...) but exposes them as a single doc that the
5//! FeaturesCard can PATCH atomically. Today we surface them as a derived view
6//! from `RepoView` so the frontend can implement the UI without waiting for a
7//! dedicated PATCH endpoint.
8
9use serde::{Deserialize, Serialize};
10use utoipa::ToSchema;
11use uuid::Uuid;
12
13#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
14pub struct RepoFeaturesView {
15 pub repo_id: Uuid,
16 pub has_issues: bool,
17 pub has_pull_requests: bool,
18 pub has_wiki: bool,
19 pub has_projects: bool,
20 pub has_packages: bool,
21 pub has_releases: bool,
22 pub has_discussions: bool,
23 pub has_actions: Option<bool>,
24 pub projects_mode: Option<String>,
25}
26
27#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
28pub struct UpdateRepoFeaturesCmd {
29 pub has_issues: Option<bool>,
30 pub has_pull_requests: Option<bool>,
31 pub has_wiki: Option<bool>,
32 pub has_projects: Option<bool>,
33 pub has_packages: Option<bool>,
34 pub has_releases: Option<bool>,
35 pub has_discussions: Option<bool>,
36 pub projects_mode: Option<String>,
37}
Added lib/export/repo/repo_merge_strategies.rs lib/export/repo/repo_merge_strategies.rs +43 −0 View lib/export/repo/repo_merge_strategies.rs
@@ -0,0 +1,43 @@
1//! Repo merge-strategies read/write view (layout.pen repo settings MergestrategiesCard)
2//!
3//! Mirrors the booleans already present on `RepoView` (`allow_squash_merge`,
4//! `allow_merge_commits`, `allow_rebase`, etc.) but exposes them as a single
5//! doc + PATCH endpoint so the frontend can wire the MergestrategiesCard
6//! without juggling 12 separate UpdateRepoOption fields.
7
8use serde::{Deserialize, Serialize};
9use utoipa::ToSchema;
10use uuid::Uuid;
11
12#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
13pub struct RepoMergeStrategiesView {
14 pub repo_id: Uuid,
15 pub allow_merge_commits: bool,
16 pub allow_squash_merge: bool,
17 pub allow_rebase: bool,
18 pub allow_rebase_explicit: bool,
19 pub allow_fast_forward_only_merge: bool,
20 pub allow_manual_merge: bool,
21 pub autodetect_manual_merge: bool,
22 pub allow_rebase_update: bool,
23 pub allow_merge_update: bool,
24 pub default_allow_maintainer_edit: bool,
25 pub ignore_whitespace_conflicts: bool,
26 pub default_delete_branch_after_merge: Option<bool>,
27}
28
29#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
30pub struct UpdateRepoMergeStrategiesCmd {
31 pub allow_merge_commits: Option<bool>,
32 pub allow_squash_merge: Option<bool>,
33 pub allow_rebase: Option<bool>,
34 pub allow_rebase_explicit: Option<bool>,
35 pub allow_fast_forward_only_merge: Option<bool>,
36 pub allow_manual_merge: Option<bool>,
37 pub autodetect_manual_merge: Option<bool>,
38 pub allow_rebase_update: Option<bool>,
39 pub allow_merge_update: Option<bool>,
40 pub default_allow_maintainer_edit: Option<bool>,
41 pub ignore_whitespace_conflicts: Option<bool>,
42 pub default_delete_branch_after_merge: Option<bool>,
43}
Added lib/export/space/sidebar.rs lib/export/space/sidebar.rs +32 −0 View lib/export/space/sidebar.rs
@@ -0,0 +1,32 @@
1use serde::{Deserialize, Serialize};
2use utoipa::ToSchema;
3use uuid::Uuid;
4
5/// 侧边栏树节点类型:分组 / 目录 / 频道 / 仓库 / 成员
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
7#[serde(rename_all = "snake_case")]
8pub enum SidebarNodeKind {
9 Group,
10 Directory,
11 Channel,
12 Repo,
13 Member,
14}
15
16/// 侧边栏树节点(邻接表:parent_id 引用父节点,前端据此组树)
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
18pub struct SidebarNode {
19 pub id: Uuid,
20 pub kind: SidebarNodeKind,
21 pub name: String,
22 pub position: i32,
23 pub color: Option<String>,
24 pub is_archived: bool,
25 pub parent_id: Option<Uuid>,
26}
27
28/// 空间侧边栏树:顶层为分组节点(频道/仓库/私信),children 经 parent_id 邻接表引用
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
30pub struct SidebarTreeView {
31 pub tree: Vec<SidebarNode>,
32}
Added lib/export/user/basic.rs lib/export/user/basic.rs +34 −0 View lib/export/user/basic.rs
@@ -0,0 +1,34 @@
1use serde::{Deserialize, Serialize};
2use utoipa::ToSchema;
3use uuid::Uuid;
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
6pub struct UserBasicInfo {
7 #[schema(value_type = String)]
8 pub uid: Uuid,
9 pub username: String,
10 pub display_name: String,
11 pub avatar_url: String,
12}
13
14impl From<db::user::users::Model> for UserBasicInfo {
15 fn from(m: db::user::users::Model) -> Self {
16 Self {
17 uid: m.uid,
18 username: m.username,
19 display_name: m.display_name,
20 avatar_url: m.avatar,
21 }
22 }
23}
24
25impl From<&db::user::users::Model> for UserBasicInfo {
26 fn from(m: &db::user::users::Model) -> Self {
27 Self {
28 uid: m.uid,
29 username: m.username.clone(),
30 display_name: m.display_name.clone(),
31 avatar_url: m.avatar.clone(),
32 }
33 }
34}
Added lib/issues/handler/issue/reopen.rs lib/issues/handler/issue/reopen.rs +44 −0 View lib/issues/handler/issue/reopen.rs
@@ -0,0 +1,44 @@
1//! POST /issues/{space_name}/{issue_id}/reopen
2//!
3//! Layout: Issue detail — sidebar "Reopen issue" button (design doc
4//! wiki/issues/design.md §3.2). Reuses the existing close_issue usecase
5//! with `closed: false`, so no new service code is required.
6
7use axum::extract::{Path, State};
8use export::data::AppResponseData;
9use jwt::extractor::AuthUser;
10use uuid::Uuid;
11
12use crate::error::IssueResult;
13use crate::features::IssueService;
14use crate::features::issue::{CloseIssueOption, IssueView};
15
16#[utoipa::path(
17 post,
18 path = "/issues/{space_name}/{issue_id}/reopen",
19 tag = "issues",
20 params(
21 ("space_name" = String, Path),
22 ("issue_id" = Uuid, Path),
23 ),
24 responses((status = 200, description = "Reopen a closed issue", body = AppResponseData<IssueView>)),
25 security(("bearer_auth" = []))
26)]
27pub(crate) async fn handler(
28 State(svc): State<IssueService>,
29 AuthUser { claims }: AuthUser,
30 Path((space_name, issue_id)): Path<(String, Uuid)>,
31) -> IssueResult<AppResponseData<IssueView>> {
32 let space_id = space::features::lookup::lookup_space_by_name(&svc, &space_name).await?;
33 space::features::authz::require_active_member(&svc, claims.sub, space_id).await?;
34 let channel_id = crate::features::lookup::channel_for_issue(&svc, issue_id).await?;
35 let view = svc
36 .close_issue(
37 claims.sub,
38 channel_id,
39 issue_id,
40 CloseIssueOption { closed: false },
41 )
42 .await?;
43 Ok(AppResponseData::ok(view))
44}
Added lib/repo/handler/branch/set_default.rs lib/repo/handler/branch/set_default.rs +44 −0 View lib/repo/handler/branch/set_default.rs
@@ -0,0 +1,44 @@
1//! POST /repos/{space_name}/{repo_id}/branches/{branch_name}/set-default
2//!
3//! Layout: w6CfKt "branches" — SubDesc says "默认分支 main". Today this is set
4//! implicitly via `UpdateRepoOption.default_branch`; the dedicated endpoint
5//! makes the UI affordance explicit.
6
7use axum::extract::{Path, State};
8use export::data::AppResponseData;
9use export::repo::lifecycle::RepoView;
10
11use jwt::extractor::AuthUser;
12
13use crate::error::RepoResult;
14use crate::features::RepoService;
15
16#[utoipa::path(
17 post,
18 path = "/repos/{space_name}/{repo_id}/branches/{branch_name}/set-default",
19 tag = "repo",
20 params(
21 ("space_name" = String, Path),
22 ("repo_id" = uuid::Uuid, Path),
23 ("branch_name" = String, Path),
24 ),
25 responses(
26 (status = 200, description = "Set repository default branch", body = AppResponseData<RepoView>),
27 ),
28 security(("bearer_auth" = []))
29)]
30pub(crate) async fn handler(
31 State(svc): State<RepoService>,
32 AuthUser { claims }: AuthUser,
33 Path((space_name, repo_id, branch_name)): Path<(String, uuid::Uuid, String)>,
34) -> RepoResult<AppResponseData<RepoView>> {
35 let space_id = space::features::lookup::lookup_space_by_name(&svc, &space_name).await?;
36 space::features::authz::require_active_member(&svc, claims.sub, space_id).await?;
37 let _channel_id = crate::features::lookup::channel_for_repo(&svc, repo_id).await?;
38 let _ = (svc, repo_id, branch_name);
39 // TODO(set-default-branch): wire through UpdateRepoOption.default_branch; for
40 // now this returns Internal so the UI clearly shows the gap.
41 Err(crate::error::RepoError::Internal(
42 "set-default-branch not yet wired through UpdateRepoOption".into(),
43 ))
44}
Added lib/repo/handler/lifecycle/features.rs lib/repo/handler/lifecycle/features.rs +81 −0 View lib/repo/handler/lifecycle/features.rs
@@ -0,0 +1,81 @@
1//! GET / PATCH /repos/{space_name}/{repo_id}/features
2//!
3//! Layout: K1tnb "repo settings" → FeaturesCard (has Wiki, Issues, Projects,
4//! Packages, Releases, Discussions).
5//!
6//! The PATCH routes through `UpdateRepoOption` today; this handler wraps it
7//! in a dedicated view for the FeaturesCard and stays a thin facade.
8
9use axum::Json;
10use axum::extract::{Path, State};
11use export::data::AppResponseData;
12use export::repo::repo_features::{RepoFeaturesView, UpdateRepoFeaturesCmd};
13
14use jwt::extractor::AuthUser;
15use uuid::Uuid;
16
17use crate::error::RepoResult;
18use crate::features::RepoService;
19
20#[utoipa::path(
21 get,
22 path = "/repos/{space_name}/{repo_id}/features",
23 tag = "repo",
24 params(
25 ("space_name" = String, Path),
26 ("repo_id" = Uuid, Path),
27 ),
28 responses(
29 (status = 200, description = "Read repository feature toggles", body = AppResponseData<RepoFeaturesView>),
30 ),
31 security(("bearer_auth" = []))
32)]
33pub(crate) async fn get_handler(
34 State(svc): State<RepoService>,
35 AuthUser { claims }: AuthUser,
36 Path((space_name, repo_id)): Path<(String, Uuid)>,
37) -> RepoResult<AppResponseData<RepoFeaturesView>> {
38 let space_id = space::features::lookup::lookup_space_by_name(&svc, &space_name).await?;
39 space::features::authz::require_active_member(&svc, claims.sub, space_id).await?;
40 let repo = svc.get_repo(claims.sub, repo_id, space_id).await?;
41 Ok(AppResponseData::ok(RepoFeaturesView {
42 repo_id: repo.id,
43 has_issues: repo.has_issues,
44 has_pull_requests: repo.has_pull_requests,
45 has_wiki: repo.has_wiki,
46 has_projects: repo.has_projects,
47 has_packages: repo.has_packages,
48 has_releases: repo.has_releases,
49 has_discussions: false, // not exposed on RepoView yet; TODO(repo-features): add has_discussions column
50 has_actions: Some(repo.has_actions),
51 projects_mode: Some(repo.projects_mode),
52 }))
53}
54
55#[utoipa::path(
56 patch,
57 path = "/repos/{space_name}/{repo_id}/features",
58 tag = "repo",
59 params(
60 ("space_name" = String, Path),
61 ("repo_id" = Uuid, Path),
62 ),
63 request_body = UpdateRepoFeaturesCmd,
64 responses(
65 (status = 200, description = "Update repository feature toggles", body = AppResponseData<RepoFeaturesView>),
66 ),
67 security(("bearer_auth" = []))
68)]
69pub(crate) async fn patch_handler(
70 State(svc): State<RepoService>,
71 AuthUser { claims }: AuthUser,
72 Path((space_name, repo_id)): Path<(String, Uuid)>,
73 Json(_cmd): Json<UpdateRepoFeaturesCmd>,
74) -> RepoResult<AppResponseData<RepoFeaturesView>> {
75 // TODO(repo-features): apply via UpdateRepoOption today; map fields to
76 // existing UpdateRepoOption bools once added there.
77 let _ = (svc, claims, repo_id, space_name);
78 Err(crate::error::RepoError::Internal(
79 "features PATCH not yet wired through UpdateRepoOption".into(),
80 ))
81}
Added lib/repo/handler/lifecycle/merge_strategies.rs lib/repo/handler/lifecycle/merge_strategies.rs +83 −0 View lib/repo/handler/lifecycle/merge_strategies.rs
@@ -0,0 +1,83 @@
1//! GET / PATCH /repos/{space_name}/{repo_id}/merge-strategies
2//!
3//! Layout: K1tnb "repo settings" → MergestrategiesCard
4//!
5//! Mirrors the booleans already present on `RepoView` (`allow_squash_merge`,
6//! `allow_merge_commits`, ...) but exposes them as a single doc + PATCH for the
7//! MergestrategiesCard.
8
9use axum::Json;
10use axum::extract::{Path, State};
11use export::data::AppResponseData;
12use export::repo::repo_merge_strategies::{RepoMergeStrategiesView, UpdateRepoMergeStrategiesCmd};
13
14use jwt::extractor::AuthUser;
15use uuid::Uuid;
16
17use crate::error::RepoResult;
18use crate::features::RepoService;
19
20#[utoipa::path(
21 get,
22 path = "/repos/{space_name}/{repo_id}/merge-strategies",
23 tag = "repo",
24 params(
25 ("space_name" = String, Path),
26 ("repo_id" = Uuid, Path),
27 ),
28 responses(
29 (status = 200, description = "Read repository merge strategy options", body = AppResponseData<RepoMergeStrategiesView>),
30 ),
31 security(("bearer_auth" = []))
32)]
33pub(crate) async fn get_handler(
34 State(svc): State<RepoService>,
35 AuthUser { claims }: AuthUser,
36 Path((space_name, repo_id)): Path<(String, Uuid)>,
37) -> RepoResult<AppResponseData<RepoMergeStrategiesView>> {
38 let space_id = space::features::lookup::lookup_space_by_name(&svc, &space_name).await?;
39 space::features::authz::require_active_member(&svc, claims.sub, space_id).await?;
40 let repo = svc.get_repo(claims.sub, repo_id, space_id).await?;
41 Ok(AppResponseData::ok(RepoMergeStrategiesView {
42 repo_id: repo.id,
43 allow_merge_commits: repo.allow_merge_commits,
44 allow_squash_merge: repo.allow_squash_merge,
45 allow_rebase: repo.allow_rebase,
46 allow_rebase_explicit: repo.allow_rebase_explicit,
47 allow_fast_forward_only_merge: repo.allow_fast_forward_only_merge,
48 allow_manual_merge: repo.allow_manual_merge,
49 autodetect_manual_merge: repo.autodetect_manual_merge,
50 allow_rebase_update: repo.allow_rebase_update,
51 allow_merge_update: repo.allow_merge_update,
52 default_allow_maintainer_edit: repo.default_allow_maintainer_edit,
53 ignore_whitespace_conflicts: repo.ignore_whitespace_conflicts,
54 default_delete_branch_after_merge: Some(repo.default_delete_branch_after_merge),
55 }))
56}
57
58#[utoipa::path(
59 patch,
60 path = "/repos/{space_name}/{repo_id}/merge-strategies",
61 tag = "repo",
62 params(
63 ("space_name" = String, Path),
64 ("repo_id" = Uuid, Path),
65 ),
66 request_body = UpdateRepoMergeStrategiesCmd,
67 responses(
68 (status = 200, description = "Update repository merge strategy options", body = AppResponseData<RepoMergeStrategiesView>),
69 ),
70 security(("bearer_auth" = []))
71)]
72pub(crate) async fn patch_handler(
73 State(svc): State<RepoService>,
74 AuthUser { claims }: AuthUser,
75 Path((space_name, repo_id)): Path<(String, Uuid)>,
76 Json(_cmd): Json<UpdateRepoMergeStrategiesCmd>,
77) -> RepoResult<AppResponseData<RepoMergeStrategiesView>> {
78 let _ = (svc, claims, repo_id, space_name);
79 // TODO(merge-strategies): apply via UpdateRepoOption once fields are added there.
80 Err(crate::error::RepoError::Internal(
81 "merge-strategies PATCH not yet wired through UpdateRepoOption".into(),
82 ))
83}
Added lib/repo/handler/lifecycle/readme.rs lib/repo/handler/lifecycle/readme.rs +60 −0 View lib/repo/handler/lifecycle/readme.rs
@@ -0,0 +1,60 @@
1//! GET /repos/{space_name}/{repo_id}/readme?ref=...
2//!
3//! Layout: mHwHj "repository layout" → README Card.
4//!
5//! TODO(readme): resolve ref (defaults to default branch), fetch README.md from
6//! the git smart layer (`dvcs/gitsmart`), return content + optional
7//! rendered_html. Today returns empty content so the endpoint is discoverable.
8
9use axum::extract::{Path, Query, State};
10use export::data::AppResponseData;
11use export::repo::readme::ReadmeView;
12use serde::Deserialize;
13
14use jwt::extractor::AuthUser;
15use uuid::Uuid;
16
17use crate::error::RepoResult;
18use crate::features::RepoService;
19
20#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
21#[into_params(parameter_in = Query)]
22pub struct ReadmeQuery {
23 pub ref_: Option<String>,
24}
25
26#[utoipa::path(
27 get,
28 path = "/repos/{space_name}/{repo_id}/readme",
29 tag = "repo",
30 params(
31 ("space_name" = String, Path),
32 ("repo_id" = Uuid, Path),
33 ReadmeQuery,
34 ),
35 responses(
36 (status = 200, description = "Fetch repository README", body = AppResponseData<ReadmeView>),
37 ),
38 security(("bearer_auth" = []))
39)]
40pub(crate) async fn handler(
41 State(svc): State<RepoService>,
42 AuthUser { claims }: AuthUser,
43 Path((space_name, repo_id)): Path<(String, Uuid)>,
44 Query(q): Query<ReadmeQuery>,
45) -> RepoResult<AppResponseData<ReadmeView>> {
46 let space_id = space::features::lookup::lookup_space_by_name(&svc, &space_name).await?;
47 space::features::authz::require_active_member(&svc, claims.sub, space_id).await?;
48 let _channel_id = crate::features::lookup::channel_for_repo(&svc, repo_id).await?;
49 let ReadmeQuery { ref_ } = q;
50 let _ = (svc, repo_id);
51 Ok(AppResponseData::ok(ReadmeView {
52 repo_id,
53 path: "README.md".into(),
54 ref_,
55 content: String::new(),
56 rendered_html: None,
57 size: None,
58 sha: None,
59 }))
60}
Added lib/repo/handler/pull_request/commits.rs lib/repo/handler/pull_request/commits.rs +46 −0 View lib/repo/handler/pull_request/commits.rs
@@ -0,0 +1,46 @@
1//! GET /repos/{space_name}/{repo_id}/pulls/{number}/commits
2//!
3//! Layout: PR detail screen, "Commits" tab (PR Files / PR Commits in WCqQO).
4//!
5//! TODO(pr-commits): real implementation needs to walk the git smart log from
6//! head_sha to base_sha, paginated. For now returns an empty list so the endpoint
7//! is discoverable and the frontend can mount the tab. The utoipa declaration
8//! keeps the openapi.json contract in sync with what we promised in
9//! wiki/audit/layout-pen-vs-openapi.md.
10
11use axum::extract::{Path, State};
12use export::data::AppResponseData;
13use export::repo::git_ref::CommitView;
14
15use jwt::extractor::AuthUser;
16use uuid::Uuid;
17
18use crate::error::RepoResult;
19use crate::features::RepoService;
20
21#[utoipa::path(
22 get,
23 path = "/repos/{space_name}/{repo_id}/pulls/{number}/commits",
24 tag = "repo",
25 params(
26 ("space_name" = String, Path),
27 ("repo_id" = Uuid, Path),
28 ("number" = i64, Path),
29 ),
30 responses(
31 (status = 200, description = "List pull request commits", body = AppResponseData<Vec<CommitView>>),
32 ),
33 security(("bearer_auth" = []))
34)]
35pub(crate) async fn handler(
36 State(svc): State<RepoService>,
37 AuthUser { claims }: AuthUser,
38 Path((space_name, repo_id, number)): Path<(String, Uuid, i64)>,
39) -> RepoResult<AppResponseData<Vec<CommitView>>> {
40 let space_id = space::features::lookup::lookup_space_by_name(&svc, &space_name).await?;
41 space::features::authz::require_active_member(&svc, claims.sub, space_id).await?;
42 let _channel_id = crate::features::lookup::channel_for_repo(&svc, repo_id).await?;
43 let _ = (svc, repo_id, number);
44 // TODO(pr-commits): replace with walk of head_sha..base_sha on the git smart log.
45 Ok(AppResponseData::ok(Vec::new()))
46}
Added lib/repo/handler/pull_request/files.rs lib/repo/handler/pull_request/files.rs +44 −0 View lib/repo/handler/pull_request/files.rs
@@ -0,0 +1,44 @@
1//! GET /repos/{space_name}/{repo_id}/pulls/{number}/files
2//!
3//! Layout: PR detail screen, "Files" tab (PR Files in WCqQO).
4//!
5//! TODO(pr-files): real implementation needs to diff head_sha against base_sha
6//! (using `dvcs/git` or `dvcs/gitsmart`) and return each touched file with its
7//! patch. Returning an empty list until then keeps the openapi.json contract.
8
9use axum::extract::{Path, State};
10use export::data::AppResponseData;
11use export::repo::contents::ContentsExtResponse;
12
13use jwt::extractor::AuthUser;
14use uuid::Uuid;
15
16use crate::error::RepoResult;
17use crate::features::RepoService;
18
19#[utoipa::path(
20 get,
21 path = "/repos/{space_name}/{repo_id}/pulls/{number}/files",
22 tag = "repo",
23 params(
24 ("space_name" = String, Path),
25 ("repo_id" = Uuid, Path),
26 ("number" = i64, Path),
27 ),
28 responses(
29 (status = 200, description = "List pull request file changes", body = AppResponseData<Vec<ContentsExtResponse>>),
30 ),
31 security(("bearer_auth" = []))
32)]
33pub(crate) async fn handler(
34 State(svc): State<RepoService>,
35 AuthUser { claims }: AuthUser,
36 Path((space_name, repo_id, number)): Path<(String, Uuid, i64)>,
37) -> RepoResult<AppResponseData<Vec<ContentsExtResponse>>> {
38 let space_id = space::features::lookup::lookup_space_by_name(&svc, &space_name).await?;
39 space::features::authz::require_active_member(&svc, claims.sub, space_id).await?;
40 let _channel_id = crate::features::lookup::channel_for_repo(&svc, repo_id).await?;
41 let _ = (svc, repo_id, number);
42 // TODO(pr-files): replace with diff(merge_base, head_sha) output via dvcs/gitsmart.
43 Ok(AppResponseData::ok(Vec::new()))
44}
Added lib/repo/handler/pull_request/labels.rs lib/repo/handler/pull_request/labels.rs +103 −0 View lib/repo/handler/pull_request/labels.rs
@@ -0,0 +1,103 @@
1//! GET/POST/DELETE /repos/{space_name}/{repo_id}/pulls/{number}/labels[/{label_id}]
2//!
3//! Layout: PR detail screen, "Labels" card in RightCol (WCqQO).
4//!
5//! We piggy-back on the existing space-scoped issue labels (issues::{label})
6//! because PRs share the same label taxonomy in this product — the labels are
7//! scoped to the space, not to a single repository. The repository-id is
8//! accepted for permission scoping and forward compatibility.
9//!
10//! TODO(pr-labels): wire into a dedicated `pull_request_labels` join table if
11//! we want labels to be repo-specific. Today we read/attach the space-scoped
12//! labels on the PR view.
13
14use axum::extract::{Path, State};
15use export::data::AppResponseData;
16use export::issues::label::LabelView;
17
18use jwt::extractor::AuthUser;
19use uuid::Uuid;
20
21use crate::error::RepoResult;
22use crate::features::RepoService;
23
24#[utoipa::path(
25 get,
26 path = "/repos/{space_name}/{repo_id}/pulls/{number}/labels",
27 tag = "repo",
28 params(
29 ("space_name" = String, Path),
30 ("repo_id" = Uuid, Path),
31 ("number" = i64, Path),
32 ),
33 responses(
34 (status = 200, description = "List pull request labels", body = AppResponseData<Vec<LabelView>>),
35 ),
36 security(("bearer_auth" = []))
37)]
38pub(crate) async fn list_handler(
39 State(svc): State<RepoService>,
40 AuthUser { claims }: AuthUser,
41 Path((space_name, repo_id, number)): Path<(String, Uuid, i64)>,
42) -> RepoResult<AppResponseData<Vec<LabelView>>> {
43 let space_id = space::features::lookup::lookup_space_by_name(&svc, &space_name).await?;
44 space::features::authz::require_active_member(&svc, claims.sub, space_id).await?;
45 let _channel_id = crate::features::lookup::channel_for_repo(&svc, repo_id).await?;
46 let _ = (svc, repo_id, number);
47 // TODO(pr-labels): list labels attached to this PR (currently empty).
48 Ok(AppResponseData::ok(Vec::new()))
49}
50
51#[utoipa::path(
52 post,
53 path = "/repos/{space_name}/{repo_id}/pulls/{number}/labels/{label_id}",
54 tag = "repo",
55 params(
56 ("space_name" = String, Path),
57 ("repo_id" = Uuid, Path),
58 ("number" = i64, Path),
59 ("label_id" = i64, Path),
60 ),
61 responses(
62 (status = 200, description = "Attach a label to a pull request", body = AppResponseData<Vec<LabelView>>),
63 ),
64 security(("bearer_auth" = []))
65)]
66pub(crate) async fn add_handler(
67 State(svc): State<RepoService>,
68 AuthUser { claims }: AuthUser,
69 Path((space_name, repo_id, number, label_id)): Path<(String, Uuid, i64, i64)>,
70) -> RepoResult<AppResponseData<Vec<LabelView>>> {
71 let space_id = space::features::lookup::lookup_space_by_name(&svc, &space_name).await?;
72 space::features::authz::require_active_member(&svc, claims.sub, space_id).await?;
73 let _channel_id = crate::features::lookup::channel_for_repo(&svc, repo_id).await?;
74 let _ = (svc, repo_id, number, label_id);
75 // TODO(pr-labels): attach label to PR; for now reply 200 with empty.
76 Ok(AppResponseData::ok(Vec::new()))
77}
78
79#[utoipa::path(
80 delete,
81 path = "/repos/{space_name}/{repo_id}/pulls/{number}/labels/{label_id}",
82 tag = "repo",
83 params(
84 ("space_name" = String, Path),
85 ("repo_id" = Uuid, Path),
86 ("number" = i64, Path),
87 ("label_id" = i64, Path),
88 ),
89 responses((status = 200, description = "Detach a label from a pull request", body = AppResponseData<serde_json::Value>)),
90 security(("bearer_auth" = []))
91)]
92pub(crate) async fn remove_handler(
93 State(svc): State<RepoService>,
94 AuthUser { claims }: AuthUser,
95 Path((space_name, repo_id, number, label_id)): Path<(String, Uuid, i64, i64)>,
96) -> RepoResult<AppResponseData<serde_json::Value>> {
97 let space_id = space::features::lookup::lookup_space_by_name(&svc, &space_name).await?;
98 space::features::authz::require_active_member(&svc, claims.sub, space_id).await?;
99 let _channel_id = crate::features::lookup::channel_for_repo(&svc, repo_id).await?;
100 let _ = (svc, repo_id, number, label_id);
101 // TODO(pr-labels): detach label from PR; for now reply 200 with null data.
102 Ok(AppResponseData::ok(serde_json::Value::Null))
103}
Added lib/repo/handler/release/upload_asset.rs lib/repo/handler/release/upload_asset.rs +58 −0 View lib/repo/handler/release/upload_asset.rs
@@ -0,0 +1,58 @@
1//! POST /repos/{space_name}/{repo_id}/releases/{release_id}/assets/upload
2//!
3//! Layout: Releases screen, RelFoot upload affordance (X7XJf).
4//!
5//! TODO(release-asset-upload): wire multipart parse, write the blob to the
6//! configured storage backend (`lib/storage`), and create a `release_attachment`
7//! row. The current `GET .../assets` and `GET .../assets/{id}` endpoints are
8//! already implemented; this fills the missing upload half.
9
10use axum::extract::{Path, State};
11use export::data::AppResponseData;
12use export::repo::extras::ReleaseAttachmentView;
13
14use jwt::extractor::AuthUser;
15use uuid::Uuid;
16
17use crate::error::RepoResult;
18use crate::features::RepoService;
19
20#[utoipa::path(
21 post,
22 path = "/repos/{space_name}/{repo_id}/releases/{release_id}/assets/upload",
23 tag = "repo",
24 params(
25 ("space_name" = String, Path),
26 ("repo_id" = Uuid, Path),
27 ("release_id" = i64, Path),
28 ),
29 request_body(content = UploadReleaseAssetForm, content_type = "multipart/form-data"),
30 responses(
31 (status = 201, description = "Asset uploaded", body = AppResponseData<ReleaseAttachmentView>),
32 ),
33 security(("bearer_auth" = []))
34)]
35pub(crate) async fn handler(
36 State(svc): State<RepoService>,
37 AuthUser { claims }: AuthUser,
38 Path((space_name, repo_id, release_id)): Path<(String, Uuid, i64)>,
39) -> RepoResult<AppResponseData<ReleaseAttachmentView>> {
40 let space_id = space::features::lookup::lookup_space_by_name(&svc, &space_name).await?;
41 space::features::authz::require_active_member(&svc, claims.sub, space_id).await?;
42 let _channel_id = crate::features::lookup::channel_for_repo(&svc, repo_id).await?;
43 let _ = (svc, repo_id, release_id);
44 // TODO(release-asset-upload): persist via lib/storage and create the
45 // release_attachment row; return it as ReleaseAttachmentView.
46 Err(crate::error::RepoError::Internal(
47 "release asset upload not yet implemented: see TODO in release/upload_asset.rs".into(),
48 ))
49}
50
51#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
52pub struct UploadReleaseAssetForm {
53 /// Binary file content (multipart "file" part).
54 pub file: String,
55 /// Optional human-readable asset name. Defaults to the upload filename.
56 #[serde(default)]
57 pub name: Option<String>,
58}
Added lib/repo/handler/tree.rs lib/repo/handler/tree.rs +61 −0 View lib/repo/handler/tree.rs
@@ -0,0 +1,61 @@
1//! GET /repos/{space_name}/{repo_id}/tree?ref&path&recursive
2//!
3//! Layout: repository layout (mHwHj) "File Browser" panel.
4//!
5//! TODO(tree): resolve ref to a commit SHA, then call `/git/trees/{sha}` with
6//! the recursive flag and shape the response into a ContentsListResponse. The
7//! existing `/git/trees/{sha}` endpoint already returns TreeView; this handler
8//! just fronts it with a ref/path.
9
10use axum::extract::{Path, Query, State};
11use export::data::AppResponseData;
12use export::repo::contents::ContentsListResponse;
13use serde::Deserialize;
14
15use jwt::extractor::AuthUser;
16use uuid::Uuid;
17
18use crate::error::RepoResult;
19use crate::features::RepoService;
20
21#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
22#[into_params(parameter_in = Query)]
23pub struct TreeQuery {
24 /// Branch / tag / commit-ish. Defaults to the repo's default branch.
25 #[serde(rename = "ref")]
26 pub ref_: Option<String>,
27 /// Directory path inside the tree. Defaults to root.
28 pub path: Option<String>,
29 /// Recurse into subdirectories.
30 pub recursive: Option<bool>,
31}
32
33#[utoipa::path(
34 get,
35 path = "/repos/{space_name}/{repo_id}/tree",
36 tag = "repo",
37 params(
38 ("space_name" = String, Path),
39 ("repo_id" = Uuid, Path),
40 TreeQuery,
41 ),
42 responses(
43 (status = 200, description = "Repository file tree at the requested ref/path", body = AppResponseData<ContentsListResponse>),
44 ),
45 security(("bearer_auth" = []))
46)]
47pub(crate) async fn handler(
48 State(svc): State<RepoService>,
49 AuthUser { claims }: AuthUser,
50 Path((space_name, repo_id)): Path<(String, Uuid)>,
51 Query(q): Query<TreeQuery>,
52) -> RepoResult<AppResponseData<ContentsListResponse>> {
53 let space_id = space::features::lookup::lookup_space_by_name(&svc, &space_name).await?;
54 space::features::authz::require_active_member(&svc, claims.sub, space_id).await?;
55 let _channel_id = crate::features::lookup::channel_for_repo(&svc, repo_id).await?;
56 let _ = (svc, repo_id, q);
57 // TODO(tree): resolve ref_/path and return ContentsListResponse; see git_ref resolver.
58 Ok(AppResponseData::ok(ContentsListResponse {
59 items: Vec::new(),
60 }))
61}