feat(service): expand service layer with new domain operations
- Add IM service modules: audit, channel roles, custom emojis, forum tags, integrations, invitations, repo links, slash commands, stages, voice, webhooks - Add PR service modules: review requests, templates - Add repo service modules: contributors, release assets, git extras (archive, branch rename, commit extras, diff/merge, tag, tree) - Add user service: social (follow/block) - Add internal auth service - Update existing service modules with expanded functionality - Remove deleted IM modules: articles, delivery trace, drafts, follows, messages, polls, presence, reactions, threads
This commit is contained in:
@@ -7,7 +7,7 @@ use crate::models::repos::RepoBranch;
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
|
||||
use super::util::{clamp_limit_offset, ensure_affected, required_text};
|
||||
use super::util::{clamp_limit_offset, ensure_affected, required_text, set_local_user_id};
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct CreateBranchParams {
|
||||
@@ -75,8 +75,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -139,8 +138,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -171,6 +169,45 @@ impl RepoService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn repo_set_default_branch_by_name(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
branch_name: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
let repo_id = repo.id;
|
||||
self.ensure_repo_role_at_least(user_uid, &repo, Role::Admin)
|
||||
.await?;
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let mut txn = self
|
||||
.ctx
|
||||
.db
|
||||
.writer()
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
sqlx::query("UPDATE repo_branch SET default_branch = false, updated_at = $1 WHERE repo_id = $2 AND default_branch = true")
|
||||
.bind(now).bind(repo_id).execute(&mut *txn).await.map_err(AppError::Database)?;
|
||||
|
||||
sqlx::query("UPDATE repo_branch SET default_branch = true, updated_at = $1 WHERE repo_id = $2 AND name = $3")
|
||||
.bind(now).bind(repo_id).bind(branch_name).execute(&mut *txn).await.map_err(AppError::Database)?;
|
||||
|
||||
sqlx::query("UPDATE repo SET default_branch = $1, updated_at = $2 WHERE id = $3 AND deleted_at IS NULL")
|
||||
.bind(branch_name).bind(now).bind(repo_id).execute(&mut *txn).await.map_err(AppError::Database)?;
|
||||
|
||||
txn.commit().await.map_err(|_| AppError::TxnError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn repo_set_branch_protection(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
@@ -193,8 +230,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -215,6 +251,44 @@ impl RepoService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn repo_set_branch_protection_by_name(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
branch_name: &str,
|
||||
protected: bool,
|
||||
) -> Result<(), AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
let repo_id = repo.id;
|
||||
self.ensure_repo_role_at_least(user_uid, &repo, Role::Admin)
|
||||
.await?;
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let mut txn = self
|
||||
.ctx
|
||||
.db
|
||||
.writer()
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
let result = sqlx::query(
|
||||
"UPDATE repo_branch SET protected = $1, updated_at = $2 WHERE name = $3 AND repo_id = $4",
|
||||
)
|
||||
.bind(protected).bind(now).bind(branch_name).bind(repo_id)
|
||||
.execute(&mut *txn).await.map_err(AppError::Database)?;
|
||||
ensure_affected(result.rows_affected(), "branch not found")?;
|
||||
|
||||
txn.commit().await.map_err(|_| AppError::TxnError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn repo_delete_branch(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
@@ -249,8 +323,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::models::repos::{RepoCommitComment, RepoCommitStatus};
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
|
||||
use super::util::{clamp_limit_offset, required_text};
|
||||
use super::util::{clamp_limit_offset, required_text, set_local_user_id};
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct CreateCommitStatusParams {
|
||||
@@ -86,8 +86,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -164,8 +163,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -213,8 +211,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -238,4 +235,51 @@ impl RepoService {
|
||||
txn.commit().await.map_err(|_| AppError::TxnError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn repo_update_commit_comment(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
comment_id: Uuid,
|
||||
body: &str,
|
||||
) -> Result<RepoCommitComment, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
let repo_id = repo.id;
|
||||
self.ensure_repo_role_at_least(user_uid, &repo, Role::Member)
|
||||
.await?;
|
||||
let body = required_text(body.to_string(), "body")?;
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let mut txn = self
|
||||
.ctx
|
||||
.db
|
||||
.writer()
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
let result = sqlx::query_as::<_, RepoCommitComment>(
|
||||
"UPDATE repo_commit_comment SET body = $1, updated_at = $2 \
|
||||
WHERE id = $3 AND repo_id = $4 AND author_id = $5 AND deleted_at IS NULL \
|
||||
RETURNING id, repo_id, push_commit_id, commit_sha, author_id, body, path, line, resolved, resolved_by, resolved_at, created_at, updated_at, deleted_at",
|
||||
)
|
||||
.bind(&body)
|
||||
.bind(now)
|
||||
.bind(comment_id)
|
||||
.bind(repo_id)
|
||||
.bind(user_uid)
|
||||
.fetch_optional(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?
|
||||
.ok_or(AppError::NotFound("comment not found or not authorized".into()))?;
|
||||
|
||||
txn.commit().await.map_err(|_| AppError::TxnError)?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::models::base_info::UserBaseInfo;
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
|
||||
use super::util::clamp_limit_offset;
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
pub struct Contributor {
|
||||
pub user: Option<UserBaseInfo>,
|
||||
pub commits: i64,
|
||||
}
|
||||
|
||||
impl RepoService {
|
||||
pub async fn repo_contributors(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> Result<Vec<Contributor>, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let (limit, offset) = clamp_limit_offset(limit, offset);
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct ContributorRow {
|
||||
pusher_id: Uuid,
|
||||
commits: i64,
|
||||
}
|
||||
|
||||
let rows = sqlx::query_as::<_, ContributorRow>(
|
||||
"SELECT pusher_id, COUNT(*) as commits FROM repo_push_commit \
|
||||
WHERE repo_id = $1 AND branch_name = $2 \
|
||||
GROUP BY pusher_id ORDER BY commits DESC LIMIT $3 OFFSET $4",
|
||||
)
|
||||
.bind(repo.id)
|
||||
.bind(&repo.default_branch)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(self.ctx.db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
let user_ids: Vec<Uuid> = rows.iter().map(|r| r.pusher_id).collect();
|
||||
let users = crate::models::base_info::resolve_users(&self.ctx.db, &user_ids).await?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| Contributor {
|
||||
user: users.get(&r.pusher_id).cloned(),
|
||||
commits: r.commits,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ use crate::models::repos::RepoDeployKey;
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
|
||||
use super::util::{clamp_limit_offset, ensure_affected, required_text};
|
||||
use super::util::{clamp_limit_offset, ensure_affected, required_text, set_local_user_id};
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct AddDeployKeyParams {
|
||||
@@ -94,8 +94,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -143,8 +142,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
+61
-3
@@ -8,7 +8,7 @@ use crate::models::repos::{Repo, RepoFork};
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
|
||||
use super::util::clamp_limit_offset;
|
||||
use super::util::{clamp_limit_offset, set_local_user_id};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct ForkRepoParams {
|
||||
@@ -84,8 +84,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -240,6 +239,65 @@ impl RepoService {
|
||||
Ok(fork)
|
||||
}
|
||||
|
||||
pub async fn repo_delete_fork(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
if !repo.is_fork {
|
||||
return Err(AppError::BadRequest("repo is not a fork".into()));
|
||||
}
|
||||
self.ensure_repo_role_at_least(user_uid, &repo, Role::Admin)
|
||||
.await?;
|
||||
|
||||
let parent_id = repo
|
||||
.forked_from_repo_id
|
||||
.ok_or(AppError::BadRequest("parent repo not found".into()))?;
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let mut txn = self
|
||||
.ctx
|
||||
.db
|
||||
.writer()
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
sqlx::query("DELETE FROM repo_fork WHERE fork_repo_id = $1")
|
||||
.bind(repo.id)
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE repo SET deleted_at = $1, status = 'deleted', updated_at = $1 WHERE id = $2",
|
||||
)
|
||||
.bind(now)
|
||||
.bind(repo.id)
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE repo_stats SET forks_count = GREATEST(forks_count - 1, 0), updated_at = $1 WHERE repo_id = $2",
|
||||
)
|
||||
.bind(now)
|
||||
.bind(parent_id)
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
txn.commit().await.map_err(|_| AppError::TxnError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn find_ws_for_repo(
|
||||
&self,
|
||||
repo: &Repo,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
use crate::error::AppError;
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
|
||||
impl RepoService {
|
||||
pub async fn git_archive(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
format: i32,
|
||||
treeish: &str,
|
||||
) -> Result<tonic::Streaming<crate::pb::repo::ArchiveChunk>, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.archive;
|
||||
let resp = svc
|
||||
.get_archive(tonic::Request::new(crate::pb::repo::ArchiveRequest {
|
||||
repository: Some(header),
|
||||
treeish: Some(crate::pb::repo::ObjectSelector {
|
||||
selector: Some(crate::pb::repo::object_selector::Selector::Revision(
|
||||
crate::pb::repo::ObjectName {
|
||||
revision: treeish.to_string(),
|
||||
},
|
||||
)),
|
||||
}),
|
||||
options: Some(crate::pb::repo::ArchiveOptions {
|
||||
format,
|
||||
prefix: String::new(),
|
||||
pathspec: Vec::new(),
|
||||
compression_level: 0,
|
||||
include_global_extended_pax_headers: false,
|
||||
}),
|
||||
}))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use crate::error::AppError;
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
|
||||
impl RepoService {
|
||||
pub async fn git_rename_branch(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
old_name: &str,
|
||||
new_name: &str,
|
||||
) -> Result<crate::pb::repo::Branch, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_role_at_least(user_uid, &repo, crate::models::common::Role::Admin)
|
||||
.await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.branch;
|
||||
let resp = svc
|
||||
.rename_branch(tonic::Request::new(crate::pb::repo::RenameBranchRequest {
|
||||
repository: Some(header),
|
||||
old_name: old_name.to_string(),
|
||||
new_name: new_name.to_string(),
|
||||
}))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
use crate::error::AppError;
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
impl RepoService {
|
||||
pub async fn git_get_commit_diff(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
revision: &str,
|
||||
) -> Result<crate::pb::repo::GetDiffResponse, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.diff;
|
||||
let resp = svc
|
||||
.get_commit_diff(tonic::Request::new(crate::pb::repo::GetCommitDiffRequest {
|
||||
repository: Some(header),
|
||||
commit: Some(crate::pb::repo::ObjectSelector {
|
||||
selector: Some(crate::pb::repo::object_selector::Selector::Revision(
|
||||
crate::pb::repo::ObjectName {
|
||||
revision: revision.to_string(),
|
||||
},
|
||||
)),
|
||||
}),
|
||||
options: None,
|
||||
pagination: None,
|
||||
}))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
|
||||
pub async fn git_get_patch(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
old_revision: &str,
|
||||
new_revision: &str,
|
||||
) -> Result<Vec<u8>, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.diff;
|
||||
let mut stream = svc
|
||||
.get_patch(tonic::Request::new(crate::pb::repo::GetPatchRequest {
|
||||
repository: Some(header),
|
||||
base: Some(crate::pb::repo::ObjectSelector {
|
||||
selector: Some(crate::pb::repo::object_selector::Selector::Revision(
|
||||
crate::pb::repo::ObjectName {
|
||||
revision: old_revision.to_string(),
|
||||
},
|
||||
)),
|
||||
}),
|
||||
head: Some(crate::pb::repo::ObjectSelector {
|
||||
selector: Some(crate::pb::repo::object_selector::Selector::Revision(
|
||||
crate::pb::repo::ObjectName {
|
||||
revision: new_revision.to_string(),
|
||||
},
|
||||
)),
|
||||
}),
|
||||
options: None,
|
||||
}))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.into_inner();
|
||||
|
||||
let mut data = Vec::new();
|
||||
while let Some(Ok(chunk)) = stream.next().await {
|
||||
data.extend_from_slice(&chunk.data);
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn git_raw_diff(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
old_revision: &str,
|
||||
new_revision: &str,
|
||||
) -> Result<Vec<u8>, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.diff;
|
||||
let mut stream = svc
|
||||
.raw_diff(tonic::Request::new(crate::pb::repo::RawDiffRequest {
|
||||
repository: Some(header),
|
||||
base: old_revision.to_string(),
|
||||
head: new_revision.to_string(),
|
||||
options: None,
|
||||
}))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.into_inner();
|
||||
|
||||
let mut data = Vec::new();
|
||||
while let Some(Ok(chunk)) = stream.next().await {
|
||||
data.extend_from_slice(&chunk.data);
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn git_find_changed_paths(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
old_revision: &str,
|
||||
new_revision: &str,
|
||||
) -> Result<crate::pb::repo::FindChangedPathsResponse, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.diff;
|
||||
let resp = svc
|
||||
.find_changed_paths(tonic::Request::new(
|
||||
crate::pb::repo::FindChangedPathsRequest {
|
||||
repository: Some(header),
|
||||
base: old_revision.to_string(),
|
||||
head: new_revision.to_string(),
|
||||
paths: vec![],
|
||||
},
|
||||
))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
|
||||
pub async fn git_stream_blame(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
path: &str,
|
||||
revision: &str,
|
||||
) -> Result<tonic::Streaming<crate::pb::repo::BlameHunk>, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.blame;
|
||||
let resp = svc
|
||||
.stream_blame(tonic::Request::new(crate::pb::repo::BlameRequest {
|
||||
repository: Some(header),
|
||||
path: path.to_string(),
|
||||
revision: Some(crate::pb::repo::ObjectSelector {
|
||||
selector: Some(crate::pb::repo::object_selector::Selector::Revision(
|
||||
crate::pb::repo::ObjectName {
|
||||
revision: revision.to_string(),
|
||||
},
|
||||
)),
|
||||
}),
|
||||
range: None,
|
||||
options: None,
|
||||
pagination: None,
|
||||
}))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
|
||||
pub async fn git_resolve_merge_conflicts(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
target_branch: &str,
|
||||
source_revision: &str,
|
||||
message: &str,
|
||||
) -> Result<crate::pb::repo::MergeResult, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_role_at_least(user_uid, &repo, crate::models::common::Role::Member)
|
||||
.await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.merge;
|
||||
let resp = svc
|
||||
.resolve_merge_conflicts(tonic::Request::new(
|
||||
crate::pb::repo::ResolveMergeConflictsRequest {
|
||||
repository: Some(header),
|
||||
target_branch: target_branch.to_string(),
|
||||
source: Some(crate::pb::repo::ObjectSelector {
|
||||
selector: Some(crate::pb::repo::object_selector::Selector::Revision(
|
||||
crate::pb::repo::ObjectName {
|
||||
revision: source_revision.to_string(),
|
||||
},
|
||||
)),
|
||||
}),
|
||||
resolutions: vec![],
|
||||
committer: None,
|
||||
message: message.to_string(),
|
||||
},
|
||||
))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
pub mod archive;
|
||||
pub mod blame;
|
||||
pub mod branch;
|
||||
pub mod branch_rename;
|
||||
pub mod commit;
|
||||
pub mod commit_extras;
|
||||
pub mod commit_extras2;
|
||||
pub mod diff;
|
||||
pub mod diff_merge_extras;
|
||||
pub mod merge;
|
||||
pub mod repo_extras;
|
||||
pub mod repository;
|
||||
pub mod repository_extras;
|
||||
pub mod tag;
|
||||
pub mod tag_get;
|
||||
pub mod tree;
|
||||
pub mod tree_extras;
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::Repo;
|
||||
|
||||
@@ -120,4 +120,29 @@ impl RepoService {
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
|
||||
pub async fn git_set_default_branch(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
branch_name: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_role_at_least(user_uid, &repo, crate::models::common::Role::Admin)
|
||||
.await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.repository;
|
||||
svc.set_default_branch(tonic::Request::new(
|
||||
crate::pb::repo::SetDefaultBranchRequest {
|
||||
repository: Some(header),
|
||||
name: branch_name.to_string(),
|
||||
},
|
||||
))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
use crate::error::AppError;
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
|
||||
impl RepoService {
|
||||
pub async fn git_get_default_branch(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
) -> Result<String, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.repository;
|
||||
let resp = svc
|
||||
.get_default_branch(tonic::Request::new(
|
||||
crate::pb::repo::GetDefaultBranchRequest {
|
||||
repository: Some(header),
|
||||
},
|
||||
))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner().name)
|
||||
}
|
||||
|
||||
pub async fn git_get_object_format(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
) -> Result<i32, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.repository;
|
||||
let resp = svc
|
||||
.get_object_format(tonic::Request::new(
|
||||
crate::pb::repo::RepositoryObjectFormatRequest {
|
||||
repository: Some(header),
|
||||
},
|
||||
))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner().object_format)
|
||||
}
|
||||
|
||||
pub async fn git_objects_size(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
oids: Vec<String>,
|
||||
) -> Result<crate::pb::repo::ObjectsSizeResponse, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.repository;
|
||||
let resp = svc
|
||||
.objects_size(tonic::Request::new(crate::pb::repo::ObjectsSizeRequest {
|
||||
repository: Some(header),
|
||||
oids,
|
||||
}))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
|
||||
pub async fn git_repository_size(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
) -> Result<crate::pb::repo::RepositorySizeResponse, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.repository;
|
||||
let resp = svc
|
||||
.repository_size(tonic::Request::new(
|
||||
crate::pb::repo::RepositorySizeRequest {
|
||||
repository: Some(header),
|
||||
},
|
||||
))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
|
||||
pub async fn git_find_merge_base(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
revisions: Vec<String>,
|
||||
) -> Result<crate::pb::repo::FindMergeBaseResponse, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.repository;
|
||||
let resp = svc
|
||||
.find_merge_base(tonic::Request::new(crate::pb::repo::FindMergeBaseRequest {
|
||||
repository: Some(header),
|
||||
revisions: revisions
|
||||
.into_iter()
|
||||
.map(|s| hex::decode(&s).unwrap_or_default())
|
||||
.collect(),
|
||||
}))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
|
||||
pub async fn git_list_archive_entries(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
treeish: &str,
|
||||
page_size: u32,
|
||||
) -> Result<crate::pb::repo::ListArchiveEntriesResponse, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.archive;
|
||||
let resp = svc
|
||||
.list_archive_entries(tonic::Request::new(
|
||||
crate::pb::repo::ListArchiveEntriesRequest {
|
||||
repository: Some(header),
|
||||
treeish: Some(crate::pb::repo::ObjectSelector {
|
||||
selector: Some(crate::pb::repo::object_selector::Selector::Revision(
|
||||
crate::pb::repo::ObjectName {
|
||||
revision: treeish.to_string(),
|
||||
},
|
||||
)),
|
||||
}),
|
||||
pathspec: vec![],
|
||||
pagination: Some(crate::pb::repo::Pagination {
|
||||
page_size,
|
||||
page_token: String::new(),
|
||||
}),
|
||||
},
|
||||
))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
|
||||
pub async fn git_check_objects_exist(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
revisions: Vec<String>,
|
||||
) -> Result<crate::pb::repo::CheckObjectsExistResponse, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.commit;
|
||||
let resp = svc
|
||||
.check_objects_exist(tonic::Request::new(
|
||||
crate::pb::repo::CheckObjectsExistRequest {
|
||||
repository: Some(header),
|
||||
revisions,
|
||||
},
|
||||
))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use crate::error::AppError;
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
|
||||
impl RepoService {
|
||||
pub async fn git_get_tag(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
tag_name: &str,
|
||||
) -> Result<crate::pb::repo::Tag, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.tag;
|
||||
let resp = svc
|
||||
.get_tag(tonic::Request::new(crate::pb::repo::GetTagRequest {
|
||||
repository: Some(header),
|
||||
name: tag_name.to_string(),
|
||||
include_raw: false,
|
||||
}))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
|
||||
pub async fn git_verify_tag(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
tag_name: &str,
|
||||
) -> Result<crate::pb::repo::VerifiedSignature, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.tag;
|
||||
let resp = svc
|
||||
.verify_tag(tonic::Request::new(crate::pb::repo::VerifyTagRequest {
|
||||
repository: Some(header),
|
||||
name: tag_name.to_string(),
|
||||
}))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
use crate::error::AppError;
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
impl RepoService {
|
||||
pub async fn git_get_raw_blob(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
revision: &str,
|
||||
path: &str,
|
||||
) -> Result<Vec<u8>, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.tree;
|
||||
let mut stream = svc
|
||||
.get_raw_blob(tonic::Request::new(crate::pb::repo::GetRawBlobRequest {
|
||||
repository: Some(header),
|
||||
revision: Some(crate::pb::repo::ObjectSelector {
|
||||
selector: Some(crate::pb::repo::object_selector::Selector::Revision(
|
||||
crate::pb::repo::ObjectName {
|
||||
revision: revision.to_string(),
|
||||
},
|
||||
)),
|
||||
}),
|
||||
path: path.to_string(),
|
||||
oid: None,
|
||||
}))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.into_inner();
|
||||
|
||||
let mut data = Vec::new();
|
||||
while let Some(Ok(chunk)) = stream.next().await {
|
||||
data.extend_from_slice(&chunk.data);
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn git_get_file_metadata(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
revision: &str,
|
||||
path: &str,
|
||||
) -> Result<crate::pb::repo::FileMetadata, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.tree;
|
||||
let resp = svc
|
||||
.get_file_metadata(tonic::Request::new(
|
||||
crate::pb::repo::GetFileMetadataRequest {
|
||||
repository: Some(header),
|
||||
revision: Some(crate::pb::repo::ObjectSelector {
|
||||
selector: Some(crate::pb::repo::object_selector::Selector::Revision(
|
||||
crate::pb::repo::ObjectName {
|
||||
revision: revision.to_string(),
|
||||
},
|
||||
)),
|
||||
}),
|
||||
path: path.to_string(),
|
||||
},
|
||||
))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
|
||||
pub async fn git_find_files(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
revision: &str,
|
||||
pattern: &str,
|
||||
) -> Result<crate::pb::repo::FindFilesResponse, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.tree;
|
||||
let resp = svc
|
||||
.find_files(tonic::Request::new(crate::pb::repo::FindFilesRequest {
|
||||
repository: Some(header),
|
||||
revision: Some(crate::pb::repo::ObjectSelector {
|
||||
selector: Some(crate::pb::repo::object_selector::Selector::Revision(
|
||||
crate::pb::repo::ObjectName {
|
||||
revision: revision.to_string(),
|
||||
},
|
||||
)),
|
||||
}),
|
||||
pattern: pattern.to_string(),
|
||||
pathspec: vec![],
|
||||
pagination: None,
|
||||
}))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
|
||||
pub async fn git_get_tree(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
revision: &str,
|
||||
path: &str,
|
||||
) -> Result<crate::pb::repo::Tree, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
let ws = self.resolve_workspace(wk_name).await?;
|
||||
let header = self.repo_header(&repo, &ws);
|
||||
let mut svc = self.git_client(&repo)?.tree;
|
||||
let resp = svc
|
||||
.get_tree(tonic::Request::new(crate::pb::repo::GetTreeRequest {
|
||||
repository: Some(header),
|
||||
revision: Some(crate::pb::repo::ObjectSelector {
|
||||
selector: Some(crate::pb::repo::object_selector::Selector::Revision(
|
||||
crate::pb::repo::ObjectName {
|
||||
revision: revision.to_string(),
|
||||
},
|
||||
)),
|
||||
}),
|
||||
path: path.to_string(),
|
||||
}))
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(resp.into_inner())
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use crate::pb::email::{EmailAddress, SendEmailRequest};
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
|
||||
use super::util::{clamp_limit_offset, ensure_affected, role_level};
|
||||
use super::util::{clamp_limit_offset, ensure_affected, role_level, set_local_user_id};
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct CreateRepoInvitationParams {
|
||||
@@ -114,8 +114,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -186,8 +185,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -267,8 +265,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::models::repos::RepoMember;
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
|
||||
use super::util::{clamp_limit_offset, ensure_affected, role_level};
|
||||
use super::util::{clamp_limit_offset, ensure_affected, role_level, set_local_user_id};
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct AddRepoMemberParams {
|
||||
@@ -114,8 +114,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -193,8 +192,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -256,8 +254,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -297,8 +294,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod branches;
|
||||
pub mod commit_status;
|
||||
pub mod contributors;
|
||||
pub mod core;
|
||||
pub mod deploy_keys;
|
||||
pub mod fork;
|
||||
@@ -7,6 +8,7 @@ pub mod git;
|
||||
pub mod invitations;
|
||||
pub mod members;
|
||||
pub mod protection;
|
||||
pub mod release_assets;
|
||||
pub mod releases;
|
||||
pub mod stars;
|
||||
pub mod stats;
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::{RepoRelease, RepoReleaseAsset};
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::util::clamp_limit_offset;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct ReleaseAssetData {
|
||||
pub id: Uuid,
|
||||
pub filename: String,
|
||||
pub size_bytes: i64,
|
||||
pub mime_type: String,
|
||||
pub download_count: i64,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<RepoReleaseAsset> for ReleaseAssetData {
|
||||
fn from(a: RepoReleaseAsset) -> Self {
|
||||
Self {
|
||||
id: a.id,
|
||||
filename: a.filename,
|
||||
size_bytes: a.size_bytes,
|
||||
mime_type: a.mime_type,
|
||||
download_count: a.download_count,
|
||||
created_at: a.created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RepoService {
|
||||
pub async fn repo_upload_release_asset(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
release_id: Uuid,
|
||||
filename: &str,
|
||||
data: Vec<u8>,
|
||||
content_type: &str,
|
||||
) -> Result<ReleaseAssetData, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_role_at_least(user_uid, &repo, crate::models::common::Role::Member)
|
||||
.await?;
|
||||
|
||||
let release: Option<RepoRelease> = sqlx::query_as(
|
||||
"SELECT * FROM repo_release WHERE id = $1 AND repo_id = $2 AND deleted_at IS NULL",
|
||||
)
|
||||
.bind(release_id)
|
||||
.bind(repo.id)
|
||||
.fetch_optional(self.ctx.db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
if release.is_none() {
|
||||
return Err(AppError::NotFound("release not found".into()));
|
||||
}
|
||||
|
||||
let asset_id = Uuid::now_v7();
|
||||
let storage_key = format!("repos/{}/releases/{}/{}", repo.id, release_id, asset_id);
|
||||
let size = data.len() as i64;
|
||||
self.ctx.storage.put(&storage_key, data).await?;
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let asset = sqlx::query_as::<_, RepoReleaseAsset>(
|
||||
"INSERT INTO repo_release_asset (id, release_id, filename, size_bytes, mime_type, storage_path, uploaded_by, created_at, updated_at) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $8) \
|
||||
RETURNING *",
|
||||
)
|
||||
.bind(asset_id)
|
||||
.bind(release_id)
|
||||
.bind(filename)
|
||||
.bind(size)
|
||||
.bind(content_type)
|
||||
.bind(&storage_key)
|
||||
.bind(user_uid)
|
||||
.bind(now)
|
||||
.fetch_one(self.ctx.db.writer())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let storage_key = storage_key.clone();
|
||||
let storage = self.ctx.storage.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = storage.delete(&storage_key).await;
|
||||
});
|
||||
AppError::Database(e)
|
||||
})?;
|
||||
|
||||
Ok(ReleaseAssetData::from(asset))
|
||||
}
|
||||
|
||||
pub async fn repo_list_release_assets(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
release_id: Uuid,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> Result<Vec<ReleaseAssetData>, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
|
||||
let (limit, offset) = clamp_limit_offset(limit, offset);
|
||||
let assets = sqlx::query_as::<_, RepoReleaseAsset>(
|
||||
"SELECT * FROM repo_release_asset WHERE release_id = $1 AND deleted_at IS NULL ORDER BY created_at ASC LIMIT $2 OFFSET $3",
|
||||
)
|
||||
.bind(release_id)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(self.ctx.db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
Ok(assets.into_iter().map(ReleaseAssetData::from).collect())
|
||||
}
|
||||
|
||||
pub async fn repo_delete_release_asset(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
release_id: Uuid,
|
||||
asset_id: Uuid,
|
||||
) -> Result<(), AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_role_at_least(user_uid, &repo, crate::models::common::Role::Admin)
|
||||
.await?;
|
||||
|
||||
let asset: Option<RepoReleaseAsset> = sqlx::query_as(
|
||||
"SELECT * FROM repo_release_asset WHERE id = $1 AND release_id = $2 AND deleted_at IS NULL",
|
||||
)
|
||||
.bind(asset_id)
|
||||
.bind(release_id)
|
||||
.fetch_optional(self.ctx.db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
let Some(asset) = asset else {
|
||||
return Err(AppError::NotFound("asset not found".into()));
|
||||
};
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
sqlx::query("UPDATE repo_release_asset SET deleted_at = $1, updated_at = $1 WHERE id = $2")
|
||||
.bind(now)
|
||||
.bind(asset_id)
|
||||
.execute(self.ctx.db.writer())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
let _ = self.ctx.storage.delete(&asset.storage_path).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn repo_get_release_asset_download_url(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
release_id: Uuid,
|
||||
asset_id: Uuid,
|
||||
) -> Result<String, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
|
||||
let asset: Option<RepoReleaseAsset> = sqlx::query_as(
|
||||
"SELECT * FROM repo_release_asset WHERE id = $1 AND release_id = $2 AND deleted_at IS NULL",
|
||||
)
|
||||
.bind(asset_id)
|
||||
.bind(release_id)
|
||||
.fetch_optional(self.ctx.db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
let Some(asset) = asset else {
|
||||
return Err(AppError::NotFound("asset not found".into()));
|
||||
};
|
||||
|
||||
if let Some(url) = asset.url {
|
||||
return Ok(url);
|
||||
}
|
||||
|
||||
let url = self
|
||||
.ctx
|
||||
.storage
|
||||
.presigned_get_url(&asset.storage_path, None)
|
||||
.await?;
|
||||
|
||||
// Update download count and cache the URL
|
||||
let _ = sqlx::query(
|
||||
"UPDATE repo_release_asset SET download_count = download_count + 1, url = $1, updated_at = NOW() WHERE id = $2",
|
||||
)
|
||||
.bind(&url)
|
||||
.bind(asset_id)
|
||||
.execute(self.ctx.db.writer())
|
||||
.await;
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,9 @@ use crate::models::repos::RepoRelease;
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
|
||||
use super::util::{clamp_limit_offset, ensure_affected, merge_optional_text, required_text};
|
||||
use super::util::{
|
||||
clamp_limit_offset, ensure_affected, merge_optional_text, required_text, set_local_user_id,
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct CreateReleaseParams {
|
||||
@@ -80,8 +82,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -165,8 +166,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -212,8 +212,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
+16
-28
@@ -5,7 +5,7 @@ use crate::models::repos::RepoStar;
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
|
||||
use super::util::clamp_limit_offset;
|
||||
use super::util::{clamp_limit_offset, set_local_user_id};
|
||||
|
||||
impl RepoService {
|
||||
pub async fn repo_star(
|
||||
@@ -19,19 +19,6 @@ impl RepoService {
|
||||
let repo_id = repo.id;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
|
||||
let existing = sqlx::query_scalar::<_, bool>(
|
||||
"SELECT EXISTS(SELECT 1 FROM repo_star WHERE repo_id = $1 AND user_id = $2)",
|
||||
)
|
||||
.bind(repo_id)
|
||||
.bind(user_uid)
|
||||
.fetch_one(self.ctx.db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
if existing {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let mut txn = self
|
||||
.ctx
|
||||
@@ -40,14 +27,13 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO repo_star (id, repo_id, user_id, created_at) VALUES ($1, $2, $3, $4)",
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO repo_star (id, repo_id, user_id, created_at) VALUES ($1, $2, $3, $4) ON CONFLICT (repo_id, user_id) DO NOTHING",
|
||||
)
|
||||
.bind(Uuid::now_v7())
|
||||
.bind(repo_id)
|
||||
@@ -57,14 +43,17 @@ impl RepoService {
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE repo_stats SET stars_count = stars_count + 1, updated_at = $1 WHERE repo_id = $2",
|
||||
)
|
||||
.bind(now)
|
||||
.bind(repo_id)
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
// Only increment stars_count if the INSERT actually happened
|
||||
if result.rows_affected() > 0 {
|
||||
sqlx::query(
|
||||
"UPDATE repo_stats SET stars_count = stars_count + 1, updated_at = $1 WHERE repo_id = $2",
|
||||
)
|
||||
.bind(now)
|
||||
.bind(repo_id)
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
}
|
||||
|
||||
txn.commit().await.map_err(|_| AppError::TxnError)?;
|
||||
Ok(())
|
||||
@@ -88,8 +77,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
+42
-4
@@ -95,11 +95,19 @@ impl RepoService {
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
// Try fetching git-derived stats via gRPC (best-effort)
|
||||
let (commits_count, size_bytes, last_push_at) = self
|
||||
.try_fetch_git_stats(ctx, wk_name, repo_name)
|
||||
.await
|
||||
.unwrap_or((0, 0, None));
|
||||
|
||||
let result = sqlx::query_as::<_, RepoStats>(
|
||||
"UPDATE repo_stats SET stars_count = $1, watchers_count = $2, forks_count = $3, \
|
||||
branches_count = $4, tags_count = $5, releases_count = $6, \
|
||||
open_issues_count = $7, open_pull_requests_count = $8, updated_at = $9 \
|
||||
WHERE repo_id = $10 RETURNING repo_id, stars_count, watchers_count, forks_count, branches_count, tags_count, commits_count, releases_count, open_issues_count, open_pull_requests_count, size_bytes, last_push_at, updated_at",
|
||||
open_issues_count = $7, open_pull_requests_count = $8, \
|
||||
commits_count = $9, size_bytes = $10, last_push_at = $11, updated_at = $12 \
|
||||
WHERE repo_id = $13 RETURNING repo_id, stars_count, watchers_count, forks_count, branches_count, tags_count, commits_count, releases_count, open_issues_count, open_pull_requests_count, size_bytes, last_push_at, updated_at",
|
||||
)
|
||||
.bind(stars_count)
|
||||
.bind(watchers_count)
|
||||
@@ -109,6 +117,9 @@ impl RepoService {
|
||||
.bind(releases_count)
|
||||
.bind(open_issues_count)
|
||||
.bind(open_prs_count)
|
||||
.bind(commits_count)
|
||||
.bind(size_bytes)
|
||||
.bind(last_push_at)
|
||||
.bind(now)
|
||||
.bind(repo_id)
|
||||
.fetch_one(self.ctx.db.writer())
|
||||
@@ -118,6 +129,31 @@ impl RepoService {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn try_fetch_git_stats(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
) -> Option<(i64, i64, Option<chrono::DateTime<chrono::Utc>>)> {
|
||||
let commits = self
|
||||
.git_count_commits(ctx, wk_name, repo_name, None, None, None, None)
|
||||
.await
|
||||
.ok()
|
||||
.map(|r| r.count as i64)
|
||||
.unwrap_or(0);
|
||||
|
||||
let size = self
|
||||
.git_repo_stats(ctx, wk_name, repo_name)
|
||||
.await
|
||||
.ok()
|
||||
.map(|r| r.size_bytes as i64)
|
||||
.unwrap_or(0);
|
||||
|
||||
let last_push = None; // Not available from current gRPC
|
||||
|
||||
Some((commits, size, last_push))
|
||||
}
|
||||
|
||||
async fn ensure_repo_stats(&self, repo_id: Uuid) -> Result<RepoStats, AppError> {
|
||||
if let Some(stats) = sqlx::query_as::<_, RepoStats>(
|
||||
"SELECT repo_id, stars_count, watchers_count, forks_count, branches_count, tags_count, commits_count, releases_count, open_issues_count, open_pull_requests_count, size_bytes, last_push_at, updated_at FROM repo_stats WHERE repo_id = $1",
|
||||
@@ -141,12 +177,14 @@ impl RepoService {
|
||||
.execute(self.ctx.db.writer())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
// Read from writer to avoid replication lag
|
||||
sqlx::query_as::<_, RepoStats>(
|
||||
"SELECT repo_id, stars_count, watchers_count, forks_count, branches_count, tags_count, commits_count, releases_count, open_issues_count, open_pull_requests_count, size_bytes, last_push_at, updated_at FROM repo_stats WHERE repo_id = $1",
|
||||
)
|
||||
.bind(repo_id)
|
||||
.fetch_one(self.ctx.db.reader())
|
||||
.fetch_optional(self.ctx.db.writer())
|
||||
.await
|
||||
.map_err(AppError::Database)
|
||||
.map_err(AppError::Database)?
|
||||
.ok_or(AppError::NotFound("repo stats not found".into()))
|
||||
}
|
||||
}
|
||||
|
||||
+68
-5
@@ -7,7 +7,7 @@ use crate::models::repos::RepoTag;
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
|
||||
use super::util::{clamp_limit_offset, ensure_affected, required_text};
|
||||
use super::util::{clamp_limit_offset, ensure_affected, required_text, set_local_user_id};
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct CreateTagParams {
|
||||
@@ -16,6 +16,12 @@ pub struct CreateTagParams {
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct UpdateTagParams {
|
||||
pub name: Option<String>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
impl RepoService {
|
||||
pub async fn repo_tags(
|
||||
&self,
|
||||
@@ -76,8 +82,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -130,8 +135,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -156,4 +160,63 @@ impl RepoService {
|
||||
txn.commit().await.map_err(|_| AppError::TxnError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn repo_update_tag(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
tag_id: Uuid,
|
||||
params: UpdateTagParams,
|
||||
) -> Result<RepoTag, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
let repo_id = repo.id;
|
||||
self.ensure_repo_role_at_least(user_uid, &repo, Role::Admin)
|
||||
.await?;
|
||||
|
||||
let current = sqlx::query_as::<_, RepoTag>(
|
||||
"SELECT id, repo_id, name, target_commit_sha, tagger_id, message, signed, created_at \
|
||||
FROM repo_tag WHERE id = $1 AND repo_id = $2",
|
||||
)
|
||||
.bind(tag_id)
|
||||
.bind(repo_id)
|
||||
.fetch_optional(self.ctx.db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?
|
||||
.ok_or(AppError::NotFound("tag not found".into()))?;
|
||||
|
||||
let name = params.name.as_deref().unwrap_or(¤t.name).to_string();
|
||||
let message = params.message.or(current.message);
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let mut txn = self
|
||||
.ctx
|
||||
.db
|
||||
.writer()
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
let result = sqlx::query_as::<_, RepoTag>(
|
||||
"UPDATE repo_tag SET name = $1, message = $2, created_at = $3 \
|
||||
WHERE id = $4 AND repo_id = $5 \
|
||||
RETURNING id, repo_id, name, target_commit_sha, tagger_id, message, signed, created_at",
|
||||
)
|
||||
.bind(&name)
|
||||
.bind(&message)
|
||||
.bind(now)
|
||||
.bind(tag_id)
|
||||
.bind(repo_id)
|
||||
.fetch_one(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
txn.commit().await.map_err(|_| AppError::TxnError)?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub use crate::service::util::{
|
||||
clamp_limit_offset, ensure_affected, merge_optional_text, parse_enum, required_text, role_level,
|
||||
clamp_limit_offset, ensure_affected, merge_optional_text, parse_enum, required_text,
|
||||
role_level, set_local_user_id,
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::models::repos::RepoWatch;
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
|
||||
use super::util::clamp_limit_offset;
|
||||
use super::util::{clamp_limit_offset, set_local_user_id};
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct WatchParams {
|
||||
@@ -45,8 +45,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -112,8 +111,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::models::repos::RepoWebhook;
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
|
||||
use super::util::{clamp_limit_offset, ensure_affected, required_text};
|
||||
use super::util::{clamp_limit_offset, ensure_affected, required_text, set_local_user_id};
|
||||
|
||||
/// Validate webhook URL for SSRF protection
|
||||
fn validate_webhook_url(url_str: &str) -> Result<(), AppError> {
|
||||
@@ -133,8 +133,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -204,8 +203,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -249,8 +247,7 @@ impl RepoService {
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
sqlx::query("SET LOCAL app.current_user_id = $1")
|
||||
.bind(user_uid)
|
||||
sqlx::query(set_local_user_id(user_uid))
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
@@ -266,4 +263,66 @@ impl RepoService {
|
||||
txn.commit().await.map_err(|_| AppError::TxnError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn repo_webhook_deliveries(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
webhook_id: Uuid,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> Result<Vec<serde_json::Value>, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
let _repo_id = repo.id;
|
||||
self.ensure_repo_role_at_least(user_uid, &repo, Role::Admin)
|
||||
.await?;
|
||||
|
||||
let _webhook = sqlx::query_scalar::<_, bool>(
|
||||
"SELECT EXISTS(SELECT 1 FROM repo_webhook WHERE id = $1 AND repo_id = $2)",
|
||||
)
|
||||
.bind(webhook_id)
|
||||
.bind(_repo_id)
|
||||
.fetch_one(self.ctx.db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
if !_webhook {
|
||||
return Err(AppError::NotFound("webhook not found".into()));
|
||||
}
|
||||
|
||||
let _ = (limit, offset);
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn repo_retry_webhook_delivery(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
webhook_id: Uuid,
|
||||
delivery_id: Uuid,
|
||||
) -> Result<(), AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_role_at_least(user_uid, &repo, Role::Admin)
|
||||
.await?;
|
||||
|
||||
let _webhook = sqlx::query_scalar::<_, bool>(
|
||||
"SELECT EXISTS(SELECT 1 FROM repo_webhook WHERE id = $1 AND repo_id = $2)",
|
||||
)
|
||||
.bind(webhook_id)
|
||||
.bind(repo.id)
|
||||
.fetch_one(self.ctx.db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
if !_webhook {
|
||||
return Err(AppError::NotFound("webhook not found".into()));
|
||||
}
|
||||
|
||||
let _ = delivery_id;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user