refactor(repo): refresh all feature usecase implementations
zhenyi committed
Modified lib/repo/features/archive/usecase.rs lib/repo/features/archive/usecase.rs +21 −14 View lib/repo/features/archive/usecase.rs
@@ -1,5 +1,6 @@
11use crate::error::{RepoError, RepoResult};
22use crate::features::RepoService;
3use crate::metrics;
34
45use super::ArchiveFormatQuery;
56
@@ -11,19 +12,25 @@
1112 ref_name: &str,
1213 _q: ArchiveFormatQuery,
1314) -> RepoResult<Vec<u8>> {
14 let _ = actor;
15 match archive_format {
16 "tarball" | "zipball" | "bundle" => {}
17 other => {
18 return Err(RepoError::BadRequest(format!(
19 "unsupported archive format: {other}"
20 )));
15 let _timer = metrics::Timer::start(metrics::FEATURE_ARCHIVES_GET);
16 let result: RepoResult<Vec<u8>> = async {
17 let _ = actor;
18 match archive_format {
19 "tarball" | "zipball" | "bundle" => {}
20 other => {
21 return Err(RepoError::BadRequest(format!(
22 "unsupported archive format: {other}"
23 )));
24 }
2125 }
22 }
23 if ref_name.is_empty() {
24 return Err(RepoError::BadRequest("ref is required".into()));
26 if ref_name.is_empty() {
27 return Err(RepoError::BadRequest("ref is required".into()));
28 }
29 Err(RepoError::Internal(
30 "archive generation requires lib/git rewire (in progress)".into(),
31 ))
2532 }
26 Err(RepoError::Internal(
27 "archive generation requires lib/git rewire (in progress)".into(),
28 ))
29}
33 .await;
34 metrics::record_outcome(metrics::FEATURE_ARCHIVES_GET, &result);
35 result
36}
No newline at end of file
Modified lib/repo/features/branch/usecase.rs lib/repo/features/branch/usecase.rs +142 −99 View lib/repo/features/branch/usecase.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{RepoError, RepoResult};
77use crate::features::RepoService;
8use crate::metrics;
89use crate::path::validate_branch_name;
910
1011use super::{
@@ -40,33 +41,39 @@
4041 repo_id: Uuid,
4142 q: ListBranchQuery,
4243) -> RepoResult<BranchPage> {
43 let _ = actor;
44 let limit = q.limit.unwrap_or(svc.config.default_pagination);
45 let offset = q.page.unwrap_or(0).saturating_mul(limit);
46 let limit = std::cmp::min(limit, svc.config.max_pagination);
47 let db = svc.db.reader();
48 let mut query = branch_entity::Entity::find()
49 .filter(branch_entity::Column::RepoId.eq(repo_id))
50 .filter(branch_entity::Column::IsDeleted.eq(false));
51 if let Some(kw) = q.keyword.as_deref() {
52 if !kw.is_empty() {
53 query = query.filter(branch_entity::Column::Name.contains(kw));
44 let _timer = metrics::Timer::start(metrics::FEATURE_BRANCHES_LIST);
45 let result: RepoResult<BranchPage> = async {
46 let _ = actor;
47 let limit = q.limit.unwrap_or(svc.config.default_pagination);
48 let offset = q.page.unwrap_or(0).saturating_mul(limit);
49 let limit = std::cmp::min(limit, svc.config.max_pagination);
50 let db = svc.db.reader();
51 let mut query = branch_entity::Entity::find()
52 .filter(branch_entity::Column::RepoId.eq(repo_id))
53 .filter(branch_entity::Column::IsDeleted.eq(false));
54 if let Some(kw) = q.keyword.as_deref() {
55 if !kw.is_empty() {
56 query = query.filter(branch_entity::Column::Name.contains(kw));
57 }
5458 }
59 let total = query.clone().count(&db).await?;
60 let items = query
61 .order_by_asc(branch_entity::Column::Name)
62 .limit(Some(limit))
63 .offset(Some(offset))
64 .all(&db)
65 .await?
66 .into_iter()
67 .map(BranchView::from)
68 .collect();
69 Ok(BranchPage {
70 items,
71 total: total as i64,
72 })
5573 }
56 let total = query.clone().count(&db).await?;
57 let items = query
58 .order_by_asc(branch_entity::Column::Name)
59 .limit(Some(limit))
60 .offset(Some(offset))
61 .all(&db)
62 .await?
63 .into_iter()
64 .map(BranchView::from)
65 .collect();
66 Ok(BranchPage {
67 items,
68 total: total as i64,
69 })
74 .await;
75 metrics::record_outcome(metrics::FEATURE_BRANCHES_LIST, &result);
76 result
7077}
7178
7279pub async fn get_branch(
@@ -75,9 +82,15 @@
7582 repo_id: Uuid,
7683 name: &str,
7784) -> RepoResult<BranchView> {
78 let _ = actor;
79 let branch = load_branch(svc, repo_id, name).await?;
80 Ok(BranchView::from(branch))
85 let _timer = metrics::Timer::start(metrics::FEATURE_BRANCHES_GET);
86 let result: RepoResult<BranchView> = async {
87 let _ = actor;
88 let branch = load_branch(svc, repo_id, name).await?;
89 Ok(BranchView::from(branch))
90 }
91 .await;
92 metrics::record_outcome(metrics::FEATURE_BRANCHES_GET, &result);
93 result
8194}
8295
8396pub async fn create_branch(
@@ -86,29 +99,35 @@
8699 repo_id: Uuid,
87100 input: CreateBranchOption,
88101) -> RepoResult<BranchView> {
89 require_authenticated(actor)?;
90 let new_name = validate_branch_name(&input.new_branch_name, svc.config.max_branch_name_len)?;
91 let source_name = input
92 .old_ref_name
93 .as_deref()
94 .or(input.old_branch_name.as_deref())
95 .unwrap_or("HEAD");
96 let source = load_branch(svc, repo_id, source_name).await?;
97 let db = svc.db.writer();
98 let now = OffsetDateTime::now_utc();
99 let active: branch_entity::ActiveModel = branch_entity::ActiveModel {
100 id: Set(Uuid::new_v4()),
101 repo_id: Set(repo_id),
102 name: Set(new_name),
103 commit_sha: Set(source.commit_sha),
104 is_deleted: Set(false),
105 is_protected: Set(false),
106 deleted_at: Set(None),
107 created_at: Set(now),
108 updated_at: Set(now),
109 };
110 let saved = active.insert(&db).await?;
111 Ok(BranchView::from(saved))
102 let _timer = metrics::Timer::start(metrics::FEATURE_BRANCHES_CREATE);
103 let result: RepoResult<BranchView> = async {
104 require_authenticated(actor)?;
105 let new_name = validate_branch_name(&input.new_branch_name, svc.config.max_branch_name_len)?;
106 let source_name = input
107 .old_ref_name
108 .as_deref()
109 .or(input.old_branch_name.as_deref())
110 .unwrap_or("HEAD");
111 let source = load_branch(svc, repo_id, source_name).await?;
112 let db = svc.db.writer();
113 let now = OffsetDateTime::now_utc();
114 let active: branch_entity::ActiveModel = branch_entity::ActiveModel {
115 id: Set(Uuid::new_v4()),
116 repo_id: Set(repo_id),
117 name: Set(new_name),
118 commit_sha: Set(source.commit_sha),
119 is_deleted: Set(false),
120 is_protected: Set(false),
121 deleted_at: Set(None),
122 created_at: Set(now),
123 updated_at: Set(now),
124 };
125 let saved = active.insert(&db).await?;
126 Ok(BranchView::from(saved))
127 }
128 .await;
129 metrics::record_outcome(metrics::FEATURE_BRANCHES_CREATE, &result);
130 result
112131}
113132
114133pub async fn update_branch(
@@ -118,21 +137,27 @@
118137 name: &str,
119138 input: UpdateBranchOption,
120139) -> RepoResult<BranchView> {
121 require_authenticated(actor)?;
122 let branch = load_branch(svc, repo_id, name).await?;
123 if let Some(ref old) = input.old_commit_id {
124 if !old.is_empty() && branch.commit_sha != *old {
125 return Err(RepoError::Conflict(
126 "branch tip has moved; refresh and retry".into(),
127 ));
140 let _timer = metrics::Timer::start(metrics::FEATURE_BRANCHES_UPDATE);
141 let result: RepoResult<BranchView> = async {
142 require_authenticated(actor)?;
143 let branch = load_branch(svc, repo_id, name).await?;
144 if let Some(ref old) = input.old_commit_id {
145 if !old.is_empty() && branch.commit_sha != *old {
146 return Err(RepoError::Conflict(
147 "branch tip has moved; refresh and retry".into(),
148 ));
149 }
128150 }
151 let db = svc.db.writer();
152 let mut active: branch_entity::ActiveModel = branch.into();
153 active.commit_sha = Set(input.new_commit_id);
154 active.updated_at = Set(OffsetDateTime::now_utc());
155 let saved = active.update(&db).await?;
156 Ok(BranchView::from(saved))
129157 }
130 let db = svc.db.writer();
131 let mut active: branch_entity::ActiveModel = branch.into();
132 active.commit_sha = Set(input.new_commit_id);
133 active.updated_at = Set(OffsetDateTime::now_utc());
134 let saved = active.update(&db).await?;
135 Ok(BranchView::from(saved))
158 .await;
159 metrics::record_outcome(metrics::FEATURE_BRANCHES_UPDATE, &result);
160 result
136161}
137162
138163pub async fn delete_branch(
@@ -141,19 +166,25 @@
141166 repo_id: Uuid,
142167 name: &str,
143168) -> RepoResult<()> {
144 require_authenticated(actor)?;
145 let branch = load_branch(svc, repo_id, name).await?;
146 if branch.is_protected {
147 return Err(RepoError::Forbidden(format!("branch {name} is protected")));
169 let _timer = metrics::Timer::start(metrics::FEATURE_BRANCHES_DELETE);
170 let result: RepoResult<()> = async {
171 require_authenticated(actor)?;
172 let branch = load_branch(svc, repo_id, name).await?;
173 if branch.is_protected {
174 return Err(RepoError::Forbidden(format!("branch {name} is protected")));
175 }
176 let db = svc.db.writer();
177 let now = OffsetDateTime::now_utc();
178 let mut active: branch_entity::ActiveModel = branch.into();
179 active.is_deleted = Set(true);
180 active.deleted_at = Set(Some(now));
181 active.updated_at = Set(now);
182 let _ = active.update(&db).await?;
183 Ok(())
148184 }
149 let db = svc.db.writer();
150 let now = OffsetDateTime::now_utc();
151 let mut active: branch_entity::ActiveModel = branch.into();
152 active.is_deleted = Set(true);
153 active.deleted_at = Set(Some(now));
154 active.updated_at = Set(now);
155 let _ = active.update(&db).await?;
156 Ok(())
185 .await;
186 metrics::record_outcome(metrics::FEATURE_BRANCHES_DELETE, &result);
187 result
157188}
158189
159190pub async fn rename_branch(
@@ -163,20 +194,26 @@
163194 old_name: &str,
164195 input: RenameBranchOption,
165196) -> RepoResult<BranchView> {
166 require_authenticated(actor)?;
167 let new_name = validate_branch_name(&input.new_name, svc.config.max_branch_name_len)?;
168 let branch = load_branch(svc, repo_id, old_name).await?;
169 if branch.is_protected {
170 return Err(RepoError::Forbidden(format!(
171 "branch {old_name} is protected"
172 )));
197 let _timer = metrics::Timer::start(metrics::FEATURE_BRANCHES_RENAME);
198 let result: RepoResult<BranchView> = async {
199 require_authenticated(actor)?;
200 let new_name = validate_branch_name(&input.new_name, svc.config.max_branch_name_len)?;
201 let branch = load_branch(svc, repo_id, old_name).await?;
202 if branch.is_protected {
203 return Err(RepoError::Forbidden(format!(
204 "branch {old_name} is protected"
205 )));
206 }
207 let db = svc.db.writer();
208 let mut active: branch_entity::ActiveModel = branch.into();
209 active.name = Set(new_name);
210 active.updated_at = Set(OffsetDateTime::now_utc());
211 let saved = active.update(&db).await?;
212 Ok(BranchView::from(saved))
173213 }
174 let db = svc.db.writer();
175 let mut active: branch_entity::ActiveModel = branch.into();
176 active.name = Set(new_name);
177 active.updated_at = Set(OffsetDateTime::now_utc());
178 let saved = active.update(&db).await?;
179 Ok(BranchView::from(saved))
214 .await;
215 metrics::record_outcome(metrics::FEATURE_BRANCHES_RENAME, &result);
216 result
180217}
181218
182219pub async fn merge_upstream(
@@ -185,14 +222,20 @@
185222 repo_id: Uuid,
186223 input: MergeUpstreamRequest,
187224) -> RepoResult<MergeUpstreamResponse> {
188 require_authenticated(actor)?;
189 let _ = repo_id;
190 let _ = input.branch;
191 let merge_type = if input.ff_only.unwrap_or(false) {
192 "fast-forward".to_string()
193 } else {
194 "merge".to_string()
195 };
196 let _ = actor;
197 Ok(MergeUpstreamResponse { merge_type })
225 let _timer = metrics::Timer::start(metrics::FEATURE_BRANCHES_MERGE_UPSTREAM);
226 let result: RepoResult<MergeUpstreamResponse> = async {
227 require_authenticated(actor)?;
228 let _ = repo_id;
229 let _ = input.branch;
230 let merge_type = if input.ff_only.unwrap_or(false) {
231 "fast-forward".to_string()
232 } else {
233 "merge".to_string()
234 };
235 let _ = actor;
236 Ok(MergeUpstreamResponse { merge_type })
237 }
238 .await;
239 metrics::record_outcome(metrics::FEATURE_BRANCHES_MERGE_UPSTREAM, &result);
240 result
198241}
Modified lib/repo/features/branch_protection/usecase.rs lib/repo/features/branch_protection/usecase.rs +154 −117 View lib/repo/features/branch_protection/usecase.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{RepoError, RepoResult};
77use crate::features::RepoService;
8use crate::metrics;
89use crate::path::validate_branch_name;
910
1011use super::{
@@ -41,26 +42,32 @@
4142 repo_id: Uuid,
4243 q: ListBranchProtectionQuery,
4344) -> RepoResult<BranchProtectionPage> {
44 let _ = actor;
45 let limit = q.limit.unwrap_or(svc.config.default_pagination);
46 let offset = q.page.unwrap_or(0).saturating_mul(limit);
47 let limit = std::cmp::min(limit, svc.config.max_pagination);
48 let db = svc.db.reader();
49 let query = policy_entity::Entity::find().filter(policy_entity::Column::RepoId.eq(repo_id));
50 let total = query.clone().count(&db).await?;
51 let items = query
52 .order_by_asc(policy_entity::Column::BranchName)
53 .limit(Some(limit))
54 .offset(Some(offset))
55 .all(&db)
56 .await?
57 .into_iter()
58 .map(BranchProtectionView::from)
59 .collect();
60 Ok(BranchProtectionPage {
61 items,
62 total: total as i64,
63 })
45 let _timer = metrics::Timer::start(metrics::FEATURE_BRANCH_PROTECTIONS_LIST);
46 let result: RepoResult<BranchProtectionPage> = async {
47 let _ = actor;
48 let limit = q.limit.unwrap_or(svc.config.default_pagination);
49 let offset = q.page.unwrap_or(0).saturating_mul(limit);
50 let limit = std::cmp::min(limit, svc.config.max_pagination);
51 let db = svc.db.reader();
52 let query = policy_entity::Entity::find().filter(policy_entity::Column::RepoId.eq(repo_id));
53 let total = query.clone().count(&db).await?;
54 let items = query
55 .order_by_asc(policy_entity::Column::BranchName)
56 .limit(Some(limit))
57 .offset(Some(offset))
58 .all(&db)
59 .await?
60 .into_iter()
61 .map(BranchProtectionView::from)
62 .collect();
63 Ok(BranchProtectionPage {
64 items,
65 total: total as i64,
66 })
67 }
68 .await;
69 metrics::record_outcome(metrics::FEATURE_BRANCH_PROTECTIONS_LIST, &result);
70 result
6471}
6572
6673pub async fn create_branch_protection(
@@ -69,34 +76,40 @@
6976 repo_id: Uuid,
7077 input: CreateBranchProtectionOption,
7178) -> RepoResult<BranchProtectionView> {
72 require_admin(actor)?;
73 let branch_name = validate_branch_name(&input.branch_name, svc.config.max_branch_name_len)?;
74 let db = svc.db.writer();
75 let now = OffsetDateTime::now_utc();
76 let active: policy_entity::ActiveModel = policy_entity::ActiveModel {
77 id: Set(Uuid::new_v4()),
78 repo_id: Set(repo_id),
79 branch_name: Set(branch_name),
80 enable_push: Set(input.enable_push.unwrap_or(true)),
81 enable_push_whitelist: Set(input.enable_push_whitelist.unwrap_or(false)),
82 push_whitelist_deploy_keys: Set(input.push_whitelist_deploy_keys.unwrap_or(false)),
83 enable_merge_whitelist: Set(input.enable_merge_whitelist.unwrap_or(false)),
84 required_approvals: Set(input.required_approvals.unwrap_or(0)),
85 enable_approvals_whitelist: Set(input.enable_approvals_whitelist.unwrap_or(false)),
86 block_on_rejected_reviews: Set(input.block_on_rejected_reviews.unwrap_or(false)),
87 block_on_change_requested_reviews: Set(input
88 .block_on_change_requested_reviews
89 .unwrap_or(false)),
90 block_on_outdated_branch: Set(input.block_on_outdated_branch.unwrap_or(false)),
91 dismiss_stale_approvals: Set(input.dismiss_stale_approvals.unwrap_or(false)),
92 require_signed_commits: Set(input.require_signed_commits.unwrap_or(false)),
93 protected_file_patterns: Set(input.protected_file_patterns.unwrap_or_default()),
94 unprotected_file_patterns: Set(input.unprotected_file_patterns.unwrap_or_default()),
95 created_at: Set(now),
96 updated_at: Set(now),
97 };
98 let saved = active.insert(&db).await?;
99 Ok(BranchProtectionView::from(saved))
79 let _timer = metrics::Timer::start(metrics::FEATURE_BRANCH_PROTECTIONS_CREATE);
80 let result: RepoResult<BranchProtectionView> = async {
81 require_admin(actor)?;
82 let branch_name = validate_branch_name(&input.branch_name, svc.config.max_branch_name_len)?;
83 let db = svc.db.writer();
84 let now = OffsetDateTime::now_utc();
85 let active: policy_entity::ActiveModel = policy_entity::ActiveModel {
86 id: Set(Uuid::new_v4()),
87 repo_id: Set(repo_id),
88 branch_name: Set(branch_name),
89 enable_push: Set(input.enable_push.unwrap_or(true)),
90 enable_push_whitelist: Set(input.enable_push_whitelist.unwrap_or(false)),
91 push_whitelist_deploy_keys: Set(input.push_whitelist_deploy_keys.unwrap_or(false)),
92 enable_merge_whitelist: Set(input.enable_merge_whitelist.unwrap_or(false)),
93 required_approvals: Set(input.required_approvals.unwrap_or(0)),
94 enable_approvals_whitelist: Set(input.enable_approvals_whitelist.unwrap_or(false)),
95 block_on_rejected_reviews: Set(input.block_on_rejected_reviews.unwrap_or(false)),
96 block_on_change_requested_reviews: Set(input
97 .block_on_change_requested_reviews
98 .unwrap_or(false)),
99 block_on_outdated_branch: Set(input.block_on_outdated_branch.unwrap_or(false)),
100 dismiss_stale_approvals: Set(input.dismiss_stale_approvals.unwrap_or(false)),
101 require_signed_commits: Set(input.require_signed_commits.unwrap_or(false)),
102 protected_file_patterns: Set(input.protected_file_patterns.unwrap_or_default()),
103 unprotected_file_patterns: Set(input.unprotected_file_patterns.unwrap_or_default()),
104 created_at: Set(now),
105 updated_at: Set(now),
106 };
107 let saved = active.insert(&db).await?;
108 Ok(BranchProtectionView::from(saved))
109 }
110 .await;
111 metrics::record_outcome(metrics::FEATURE_BRANCH_PROTECTIONS_CREATE, &result);
112 result
100113}
101114
102115pub async fn get_branch_protection(
@@ -105,9 +118,15 @@
105118 repo_id: Uuid,
106119 policy_id: Uuid,
107120) -> RepoResult<BranchProtectionView> {
108 let _ = actor;
109 let policy = load_policy(svc, repo_id, policy_id).await?;
110 Ok(BranchProtectionView::from(policy))
121 let _timer = metrics::Timer::start(metrics::FEATURE_BRANCH_PROTECTIONS_GET);
122 let result: RepoResult<BranchProtectionView> = async {
123 let _ = actor;
124 let policy = load_policy(svc, repo_id, policy_id).await?;
125 Ok(BranchProtectionView::from(policy))
126 }
127 .await;
128 metrics::record_outcome(metrics::FEATURE_BRANCH_PROTECTIONS_GET, &result);
129 result
111130}
112131
113132pub async fn edit_branch_protection(
@@ -117,52 +136,58 @@
117136 policy_id: Uuid,
118137 input: EditBranchProtectionOption,
119138) -> RepoResult<BranchProtectionView> {
120 require_admin(actor)?;
121 let policy = load_policy(svc, repo_id, policy_id).await?;
122 let db = svc.db.writer();
123 let mut active: policy_entity::ActiveModel = policy.into();
124 if let Some(v) = input.enable_push {
125 active.enable_push = Set(v);
126 }
127 if let Some(v) = input.enable_push_whitelist {
128 active.enable_push_whitelist = Set(v);
129 }
130 if let Some(v) = input.push_whitelist_deploy_keys {
131 active.push_whitelist_deploy_keys = Set(v);
132 }
133 if let Some(v) = input.enable_merge_whitelist {
134 active.enable_merge_whitelist = Set(v);
135 }
136 if let Some(v) = input.required_approvals {
137 active.required_approvals = Set(v);
138 }
139 if let Some(v) = input.enable_approvals_whitelist {
140 active.enable_approvals_whitelist = Set(v);
141 }
142 if let Some(v) = input.block_on_rejected_reviews {
143 active.block_on_rejected_reviews = Set(v);
144 }
145 if let Some(v) = input.block_on_change_requested_reviews {
146 active.block_on_change_requested_reviews = Set(v);
147 }
148 if let Some(v) = input.block_on_outdated_branch {
149 active.block_on_outdated_branch = Set(v);
150 }
151 if let Some(v) = input.dismiss_stale_approvals {
152 active.dismiss_stale_approvals = Set(v);
153 }
154 if let Some(v) = input.require_signed_commits {
155 active.require_signed_commits = Set(v);
156 }
157 if let Some(v) = input.protected_file_patterns {
158 active.protected_file_patterns = Set(v);
159 }
160 if let Some(v) = input.unprotected_file_patterns {
161 active.unprotected_file_patterns = Set(v);
139 let _timer = metrics::Timer::start(metrics::FEATURE_BRANCH_PROTECTIONS_EDIT);
140 let result: RepoResult<BranchProtectionView> = async {
141 require_admin(actor)?;
142 let policy = load_policy(svc, repo_id, policy_id).await?;
143 let db = svc.db.writer();
144 let mut active: policy_entity::ActiveModel = policy.into();
145 if let Some(v) = input.enable_push {
146 active.enable_push = Set(v);
147 }
148 if let Some(v) = input.enable_push_whitelist {
149 active.enable_push_whitelist = Set(v);
150 }
151 if let Some(v) = input.push_whitelist_deploy_keys {
152 active.push_whitelist_deploy_keys = Set(v);
153 }
154 if let Some(v) = input.enable_merge_whitelist {
155 active.enable_merge_whitelist = Set(v);
156 }
157 if let Some(v) = input.required_approvals {
158 active.required_approvals = Set(v);
159 }
160 if let Some(v) = input.enable_approvals_whitelist {
161 active.enable_approvals_whitelist = Set(v);
162 }
163 if let Some(v) = input.block_on_rejected_reviews {
164 active.block_on_rejected_reviews = Set(v);
165 }
166 if let Some(v) = input.block_on_change_requested_reviews {
167 active.block_on_change_requested_reviews = Set(v);
168 }
169 if let Some(v) = input.block_on_outdated_branch {
170 active.block_on_outdated_branch = Set(v);
171 }
172 if let Some(v) = input.dismiss_stale_approvals {
173 active.dismiss_stale_approvals = Set(v);
174 }
175 if let Some(v) = input.require_signed_commits {
176 active.require_signed_commits = Set(v);
177 }
178 if let Some(v) = input.protected_file_patterns {
179 active.protected_file_patterns = Set(v);
180 }
181 if let Some(v) = input.unprotected_file_patterns {
182 active.unprotected_file_patterns = Set(v);
183 }
184 active.updated_at = Set(OffsetDateTime::now_utc());
185 let saved = active.update(&db).await?;
186 Ok(BranchProtectionView::from(saved))
162187 }
163 active.updated_at = Set(OffsetDateTime::now_utc());
164 let saved = active.update(&db).await?;
165 Ok(BranchProtectionView::from(saved))
188 .await;
189 metrics::record_outcome(metrics::FEATURE_BRANCH_PROTECTIONS_EDIT, &result);
190 result
166191}
167192
168193pub async fn delete_branch_protection(
@@ -171,13 +196,19 @@
171196 repo_id: Uuid,
172197 policy_id: Uuid,
173198) -> RepoResult<()> {
174 require_admin(actor)?;
175 let _ = load_policy(svc, repo_id, policy_id).await?;
176 let db = svc.db.writer();
177 policy_entity::Entity::delete_by_id(policy_id)
178 .exec(&db)
179 .await?;
180 Ok(())
199 let _timer = metrics::Timer::start(metrics::FEATURE_BRANCH_PROTECTIONS_DELETE);
200 let result: RepoResult<()> = async {
201 require_admin(actor)?;
202 let _ = load_policy(svc, repo_id, policy_id).await?;
203 let db = svc.db.writer();
204 policy_entity::Entity::delete_by_id(policy_id)
205 .exec(&db)
206 .await?;
207 Ok(())
208 }
209 .await;
210 metrics::record_outcome(metrics::FEATURE_BRANCH_PROTECTIONS_DELETE, &result);
211 result
181212}
182213
183214pub async fn update_branch_protection_priorities(
@@ -186,19 +217,25 @@
186217 repo_id: Uuid,
187218 input: UpdateBranchProtectionPriorities,
188219) -> RepoResult<()> {
189 require_admin(actor)?;
190 if input.ids.len() as i64 > svc.config.branch_protection_priority_max {
191 return Err(RepoError::BadRequest(format!(
192 "too many priority entries (max {})",
193 svc.config.branch_protection_priority_max
194 )));
195 }
196 let db = svc.db.writer();
197 for id in input.ids.iter() {
198 let policy = load_policy(svc, repo_id, *id).await?;
199 let mut active: policy_entity::ActiveModel = policy.into();
200 active.updated_at = Set(OffsetDateTime::now_utc());
201 let _ = active.update(&db).await?;
220 let _timer = metrics::Timer::start(metrics::FEATURE_BRANCH_PROTECTIONS_UPDATE_PRIORITIES);
221 let result: RepoResult<()> = async {
222 require_admin(actor)?;
223 if input.ids.len() as i64 > svc.config.branch_protection_priority_max {
224 return Err(RepoError::BadRequest(format!(
225 "too many priority entries (max {})",
226 svc.config.branch_protection_priority_max
227 )));
228 }
229 let db = svc.db.writer();
230 for id in input.ids.iter() {
231 let policy = load_policy(svc, repo_id, *id).await?;
232 let mut active: policy_entity::ActiveModel = policy.into();
233 active.updated_at = Set(OffsetDateTime::now_utc());
234 let _ = active.update(&db).await?;
235 }
236 Ok(())
202237 }
203 Ok(())
238 .await;
239 metrics::record_outcome(metrics::FEATURE_BRANCH_PROTECTIONS_UPDATE_PRIORITIES, &result);
240 result
204241}
Modified lib/repo/features/collab/usecase.rs lib/repo/features/collab/usecase.rs +113 −81 View lib/repo/features/collab/usecase.rs
@@ -6,6 +6,7 @@
66
77use crate::error::{RepoError, RepoResult};
88use crate::features::RepoService;
9use crate::metrics;
910
1011use super::{
1112 AddCollaboratorOption, CollaboratorPage, CollaboratorView, RepoCollaboratorPermission,
@@ -43,25 +44,32 @@
4344 page: Option<u64>,
4445 limit: Option<u64>,
4546) -> RepoResult<CollaboratorPage> {
46 let _ = actor;
47 let limit = limit.unwrap_or(svc.config.default_pagination);
48 let offset = page.unwrap_or(0).saturating_mul(limit);
49 let limit = std::cmp::min(limit, svc.config.max_pagination);
50 let db = svc.db.reader();
51 let query = collab_entity::Entity::find().filter(collab_entity::Column::RepoId.eq(repo_id));
52 let total = query.clone().count(&db).await?;
53 let items = query
54 .limit(Some(limit))
55 .offset(Some(offset))
56 .all(&db)
57 .await?
58 .into_iter()
59 .map(CollaboratorView::from)
60 .collect();
61 Ok(CollaboratorPage {
62 items,
63 total: total as i64,
64 })
47 let _timer = metrics::Timer::start(metrics::FEATURE_COLLAB_LIST);
48 let result: RepoResult<CollaboratorPage> = async {
49 let _ = actor;
50 let limit = limit.unwrap_or(svc.config.default_pagination);
51 let offset = page.unwrap_or(0).saturating_mul(limit);
52 let limit = std::cmp::min(limit, svc.config.max_pagination);
53 let db = svc.db.reader();
54 let query =
55 collab_entity::Entity::find().filter(collab_entity::Column::RepoId.eq(repo_id));
56 let total = query.clone().count(&db).await?;
57 let items = query
58 .limit(Some(limit))
59 .offset(Some(offset))
60 .all(&db)
61 .await?
62 .into_iter()
63 .map(CollaboratorView::from)
64 .collect();
65 Ok(CollaboratorPage {
66 items,
67 total: total as i64,
68 })
69 }
70 .await;
71 metrics::record_outcome(metrics::FEATURE_COLLAB_LIST, &result);
72 result
6573}
6674
6775pub async fn is_collaborator(
@@ -70,14 +78,20 @@
7078 repo_id: Uuid,
7179 user_id: Uuid,
7280) -> RepoResult<bool> {
73 let _ = actor;
74 let db = svc.db.reader();
75 let hit = collab_entity::Entity::find()
76 .filter(collab_entity::Column::RepoId.eq(repo_id))
77 .filter(collab_entity::Column::UserId.eq(user_id))
78 .one(&db)
79 .await?;
80 Ok(hit.is_some())
81 let _timer = metrics::Timer::start(metrics::FEATURE_COLLAB_IS_COLLABORATOR);
82 let result: RepoResult<bool> = async {
83 let _ = actor;
84 let db = svc.db.reader();
85 let hit = collab_entity::Entity::find()
86 .filter(collab_entity::Column::RepoId.eq(repo_id))
87 .filter(collab_entity::Column::UserId.eq(user_id))
88 .one(&db)
89 .await?;
90 Ok(hit.is_some())
91 }
92 .await;
93 metrics::record_outcome(metrics::FEATURE_COLLAB_IS_COLLABORATOR, &result);
94 result
8195}
8296
8397pub async fn add_collaborator(
@@ -87,32 +101,38 @@
87101 user_id: Uuid,
88102 input: AddCollaboratorOption,
89103) -> RepoResult<()> {
90 require_admin(actor)?;
91 let mode = parse_mode(&input.permission)?;
92 let db = svc.db.writer();
93 let now = OffsetDateTime::now_utc();
94 let existing = collab_entity::Entity::find()
95 .filter(collab_entity::Column::RepoId.eq(repo_id))
96 .filter(collab_entity::Column::UserId.eq(user_id))
97 .one(&db)
98 .await?;
99 if let Some(current) = existing {
100 let mut active: collab_entity::ActiveModel = current.into();
101 active.mode = Set(mode);
102 active.updated_at = Set(now);
103 active.update(&db).await?;
104 } else {
105 let active: collab_entity::ActiveModel = collab_entity::ActiveModel {
106 id: Set(Uuid::new_v4()),
107 repo_id: Set(repo_id),
108 user_id: Set(user_id),
109 mode: Set(mode),
110 created_at: Set(now),
111 updated_at: Set(now),
112 };
113 active.insert(&db).await?;
104 let _timer = metrics::Timer::start(metrics::FEATURE_COLLAB_ADD);
105 let result: RepoResult<()> = async {
106 require_admin(actor)?;
107 let mode = parse_mode(&input.permission)?;
108 let db = svc.db.writer();
109 let now = OffsetDateTime::now_utc();
110 let existing = collab_entity::Entity::find()
111 .filter(collab_entity::Column::RepoId.eq(repo_id))
112 .filter(collab_entity::Column::UserId.eq(user_id))
113 .one(&db)
114 .await?;
115 if let Some(current) = existing {
116 let mut active: collab_entity::ActiveModel = current.into();
117 active.mode = Set(mode);
118 active.updated_at = Set(now);
119 active.update(&db).await?;
120 } else {
121 let active: collab_entity::ActiveModel = collab_entity::ActiveModel {
122 id: Set(Uuid::new_v4()),
123 repo_id: Set(repo_id),
124 user_id: Set(user_id),
125 mode: Set(mode),
126 created_at: Set(now),
127 updated_at: Set(now),
128 };
129 active.insert(&db).await?;
130 }
131 Ok(())
114132 }
115 Ok(())
133 .await;
134 metrics::record_outcome(metrics::FEATURE_COLLAB_ADD, &result);
135 result
116136}
117137
118138pub async fn delete_collaborator(
@@ -121,14 +141,20 @@
121141 repo_id: Uuid,
122142 user_id: Uuid,
123143) -> RepoResult<()> {
124 require_admin(actor)?;
125 let db = svc.db.writer();
126 collab_entity::Entity::delete_many()
127 .filter(collab_entity::Column::RepoId.eq(repo_id))
128 .filter(collab_entity::Column::UserId.eq(user_id))
129 .exec(&db)
130 .await?;
131 Ok(())
144 let _timer = metrics::Timer::start(metrics::FEATURE_COLLAB_DELETE);
145 let result: RepoResult<()> = async {
146 require_admin(actor)?;
147 let db = svc.db.writer();
148 collab_entity::Entity::delete_many()
149 .filter(collab_entity::Column::RepoId.eq(repo_id))
150 .filter(collab_entity::Column::UserId.eq(user_id))
151 .exec(&db)
152 .await?;
153 Ok(())
154 }
155 .await;
156 metrics::record_outcome(metrics::FEATURE_COLLAB_DELETE, &result);
157 result
132158}
133159
134160pub async fn get_repo_permissions(
@@ -137,25 +163,31 @@
137163 repo_id: Uuid,
138164 user_id: Uuid,
139165) -> RepoResult<RepoCollaboratorPermission> {
140 let _ = actor;
141 let db = svc.db.reader();
142 let collab = collab_entity::Entity::find()
143 .filter(collab_entity::Column::RepoId.eq(repo_id))
144 .filter(collab_entity::Column::UserId.eq(user_id))
145 .one(&db)
146 .await?;
147 let permission = match collab {
148 Some(c) => mode_to_str(c.mode),
149 None => "none".to_string(),
150 };
151 Ok(RepoCollaboratorPermission {
152 permission,
153 role: "collaborator".to_string(),
154 user: UserSummary {
155 id: user_id,
156 name: String::new(),
157 login: String::new(),
158 email: None,
159 },
160 })
166 let _timer = metrics::Timer::start(metrics::FEATURE_COLLAB_GET_REPO_PERMISSIONS);
167 let result: RepoResult<RepoCollaboratorPermission> = async {
168 let _ = actor;
169 let db = svc.db.reader();
170 let collab = collab_entity::Entity::find()
171 .filter(collab_entity::Column::RepoId.eq(repo_id))
172 .filter(collab_entity::Column::UserId.eq(user_id))
173 .one(&db)
174 .await?;
175 let permission = match collab {
176 Some(c) => mode_to_str(c.mode),
177 None => "none".to_string(),
178 };
179 Ok(RepoCollaboratorPermission {
180 permission,
181 role: "collaborator".to_string(),
182 user: UserSummary {
183 id: user_id,
184 name: String::new(),
185 login: String::new(),
186 email: None,
187 },
188 })
189 }
190 .await;
191 metrics::record_outcome(metrics::FEATURE_COLLAB_GET_REPO_PERMISSIONS, &result);
192 result
161193}
Modified lib/repo/features/commit_status/usecase.rs lib/repo/features/commit_status/usecase.rs +89 −63 View lib/repo/features/commit_status/usecase.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{RepoError, RepoResult};
77use crate::features::RepoService;
8use crate::metrics;
89
910use super::types::{
1011 CombinedStatusView, CommitStatusPage, CommitStatusView, CreateStatusOption, ListStatusQuery,
@@ -49,32 +50,38 @@
4950 sha: &str,
5051 q: ListStatusQuery,
5152) -> RepoResult<CommitStatusPage> {
52 let _ = actor;
53 let limit = q.limit.unwrap_or(svc.config.default_pagination);
54 let offset = q.page.unwrap_or(0).saturating_mul(limit);
55 let limit = std::cmp::min(limit, svc.config.max_pagination);
56 let db = svc.db.reader();
57 let mut query = status_entity::Entity::find()
58 .filter(status_entity::Column::RepoId.eq(repo_id))
59 .filter(status_entity::Column::Sha.eq(sha));
60 if let Some(s) = q.state.as_deref() {
61 let parsed = parse_state_pub(s)?;
62 query = query.filter(status_entity::Column::State.eq(parsed));
53 let _timer = metrics::Timer::start(metrics::FEATURE_COMMIT_STATUSES_LIST);
54 let result: RepoResult<CommitStatusPage> = async {
55 let _ = actor;
56 let limit = q.limit.unwrap_or(svc.config.default_pagination);
57 let offset = q.page.unwrap_or(0).saturating_mul(limit);
58 let limit = std::cmp::min(limit, svc.config.max_pagination);
59 let db = svc.db.reader();
60 let mut query = status_entity::Entity::find()
61 .filter(status_entity::Column::RepoId.eq(repo_id))
62 .filter(status_entity::Column::Sha.eq(sha));
63 if let Some(s) = q.state.as_deref() {
64 let parsed = parse_state_pub(s)?;
65 query = query.filter(status_entity::Column::State.eq(parsed));
66 }
67 let total = query.clone().count(&db).await?;
68 let items = query
69 .order_by_desc(status_entity::Column::CreatedAt)
70 .limit(Some(limit))
71 .offset(Some(offset))
72 .all(&db)
73 .await?
74 .into_iter()
75 .map(CommitStatusView::from)
76 .collect();
77 Ok(CommitStatusPage {
78 items,
79 total: total as i64,
80 })
6381 }
64 let total = query.clone().count(&db).await?;
65 let items = query
66 .order_by_desc(status_entity::Column::CreatedAt)
67 .limit(Some(limit))
68 .offset(Some(offset))
69 .all(&db)
70 .await?
71 .into_iter()
72 .map(CommitStatusView::from)
73 .collect();
74 Ok(CommitStatusPage {
75 items,
76 total: total as i64,
77 })
82 .await;
83 metrics::record_outcome(metrics::FEATURE_COMMIT_STATUSES_LIST, &result);
84 result
7885}
7986
8087pub async fn create_commit_status(
@@ -84,24 +91,30 @@
8491 sha: &str,
8592 input: CreateStatusOption,
8693) -> RepoResult<CommitStatusView> {
87 require_authenticated(actor)?;
88 let state = parse_state_pub(&input.state)?;
89 let db = svc.db.writer();
90 let now = OffsetDateTime::now_utc();
91 let active: status_entity::ActiveModel = status_entity::ActiveModel {
92 id: Set(Uuid::new_v4()),
93 repo_id: Set(repo_id),
94 sha: Set(sha.to_string()),
95 state: Set(state),
96 context: Set(input.context),
97 description: Set(input.description),
98 target_url: Set(input.target_url),
99 creator_id: Set(actor),
100 created_at: Set(now),
101 updated_at: Set(Some(now)),
102 };
103 let saved = active.insert(&db).await?;
104 Ok(CommitStatusView::from(saved))
94 let _timer = metrics::Timer::start(metrics::FEATURE_COMMIT_STATUSES_CREATE);
95 let result: RepoResult<CommitStatusView> = async {
96 require_authenticated(actor)?;
97 let state = parse_state_pub(&input.state)?;
98 let db = svc.db.writer();
99 let now = OffsetDateTime::now_utc();
100 let active: status_entity::ActiveModel = status_entity::ActiveModel {
101 id: Set(Uuid::new_v4()),
102 repo_id: Set(repo_id),
103 sha: Set(sha.to_string()),
104 state: Set(state),
105 context: Set(input.context),
106 description: Set(input.description),
107 target_url: Set(input.target_url),
108 creator_id: Set(actor),
109 created_at: Set(now),
110 updated_at: Set(Some(now)),
111 };
112 let saved = active.insert(&db).await?;
113 Ok(CommitStatusView::from(saved))
114 }
115 .await;
116 metrics::record_outcome(metrics::FEATURE_COMMIT_STATUSES_CREATE, &result);
117 result
105118}
106119
107120pub async fn list_commit_statuses_by_ref(
@@ -111,9 +124,15 @@
111124 git_ref: &str,
112125 q: ListStatusQuery,
113126) -> RepoResult<CommitStatusPage> {
114 let _ = actor;
115 let sha = resolve_ref(svc, repo_id, git_ref).await?;
116 list_commit_statuses(svc, actor, repo_id, &sha, q).await
127 let _timer = metrics::Timer::start(metrics::FEATURE_COMMIT_STATUSES_LIST_BY_REF);
128 let result: RepoResult<CommitStatusPage> = async {
129 let _ = actor;
130 let sha = resolve_ref(svc, repo_id, git_ref).await?;
131 list_commit_statuses(svc, actor, repo_id, &sha, q).await
132 }
133 .await;
134 metrics::record_outcome(metrics::FEATURE_COMMIT_STATUSES_LIST_BY_REF, &result);
135 result
117136}
118137
119138pub async fn combined_status_by_ref(
@@ -122,23 +141,30 @@
122141 repo_id: Uuid,
123142 git_ref: &str,
124143) -> RepoResult<CombinedStatusView> {
125 let _ = actor;
126 let sha = resolve_ref(svc, repo_id, git_ref).await?;
127 let db = svc.db.reader();
128 let items = status_entity::Entity::find()
129 .filter(status_entity::Column::RepoId.eq(repo_id))
130 .filter(status_entity::Column::Sha.eq(&sha))
131 .all(&db)
132 .await?;
133 let total = items.len() as i64;
134 let state = aggregate_state(&items).to_string();
135 let statuses: Vec<CommitStatusView> = items.into_iter().map(CommitStatusView::from).collect();
136 Ok(CombinedStatusView {
137 sha,
138 state,
139 statuses,
140 total_count: total,
141 })
144 let _timer = metrics::Timer::start(metrics::FEATURE_COMMIT_STATUSES_COMBINED_BY_REF);
145 let result: RepoResult<CombinedStatusView> = async {
146 let _ = actor;
147 let sha = resolve_ref(svc, repo_id, git_ref).await?;
148 let db = svc.db.reader();
149 let items = status_entity::Entity::find()
150 .filter(status_entity::Column::RepoId.eq(repo_id))
151 .filter(status_entity::Column::Sha.eq(&sha))
152 .all(&db)
153 .await?;
154 let total = items.len() as i64;
155 let state = aggregate_state(&items).to_string();
156 let statuses: Vec<CommitStatusView> =
157 items.into_iter().map(CommitStatusView::from).collect();
158 Ok(CombinedStatusView {
159 sha,
160 state,
161 statuses,
162 total_count: total,
163 })
164 }
165 .await;
166 metrics::record_outcome(metrics::FEATURE_COMMIT_STATUSES_COMBINED_BY_REF, &result);
167 result
142168}
143169
144170async fn resolve_ref(_svc: &RepoService, _repo_id: Uuid, git_ref: &str) -> RepoResult<String> {
Modified lib/repo/features/compare/usecase.rs lib/repo/features/compare/usecase.rs +14 −7 View lib/repo/features/compare/usecase.rs
@@ -1,5 +1,6 @@
11use crate::error::{RepoError, RepoResult};
22use crate::features::RepoService;
3use crate::metrics;
34
45use super::CompareView;
56
@@ -10,11 +11,17 @@
1011 base: &str,
1112 head: &str,
1213) -> RepoResult<CompareView> {
13 let _ = actor;
14 if base.is_empty() || head.is_empty() {
15 return Err(RepoError::BadRequest("base and head are required".into()));
14 let _timer = metrics::Timer::start(metrics::FEATURE_COMPARE);
15 let result: RepoResult<CompareView> = async {
16 let _ = actor;
17 if base.is_empty() || head.is_empty() {
18 return Err(RepoError::BadRequest("base and head are required".into()));
19 }
20 Err(RepoError::Internal(
21 "compare requires lib/git rewire (in progress)".into(),
22 ))
1623 }
17 Err(RepoError::Internal(
18 "compare requires lib/git rewire (in progress)".into(),
19 ))
20}
24 .await;
25 metrics::record_outcome(metrics::FEATURE_COMPARE, &result);
26 result
27}
No newline at end of file
Modified lib/repo/features/contents/usecase.rs lib/repo/features/contents/usecase.rs +105 −62 View lib/repo/features/contents/usecase.rs
@@ -2,6 +2,7 @@
22
33use crate::error::{RepoError, RepoResult};
44use crate::features::RepoService;
5use crate::metrics;
56
67use super::{
78 ApplyDiffPatchFileOptions, ChangeFilesOptions, ContentsListResponse, ContentsQuery,
@@ -19,11 +20,17 @@
1920 path: &str,
2021 _q: ContentsQuery,
2122) -> RepoResult<ContentsListResponse> {
22 let _ = actor;
23 let _ = path;
24 Err(RepoError::Internal(
25 "contents listing requires lib/git rewire (in progress)".into(),
26 ))
23 let _timer = metrics::Timer::start(metrics::FEATURE_CONTENTS_LIST);
24 let result: RepoResult<ContentsListResponse> = async {
25 let _ = actor;
26 let _ = path;
27 Err(RepoError::Internal(
28 "contents listing requires lib/git rewire (in progress)".into(),
29 ))
30 }
31 .await;
32 metrics::record_outcome(metrics::FEATURE_CONTENTS_LIST, &result);
33 result
2734}
2835
2936pub async fn get_contents(
@@ -33,11 +40,17 @@
3340 path: &str,
3441 _q: ContentsQuery,
3542) -> RepoResult<ContentsResponse> {
36 let _ = actor;
37 let _ = path;
38 Err(RepoError::Internal(
39 "contents read requires lib/git rewire (in progress)".into(),
40 ))
43 let _timer = metrics::Timer::start(metrics::FEATURE_CONTENTS_GET);
44 let result: RepoResult<ContentsResponse> = async {
45 let _ = actor;
46 let _ = path;
47 Err(RepoError::Internal(
48 "contents read requires lib/git rewire (in progress)".into(),
49 ))
50 }
51 .await;
52 metrics::record_outcome(metrics::FEATURE_CONTENTS_GET, &result);
53 result
4154}
4255
4356pub async fn create_file(
@@ -47,23 +60,29 @@
4760 path: &str,
4861 input: CreateFileOption,
4962) -> RepoResult<ContentsResponse> {
50 require_write(actor)?;
51 if path.is_empty() {
52 return Err(RepoError::BadRequest("path is required".into()));
63 let _timer = metrics::Timer::start(metrics::FEATURE_CONTENTS_CREATE_FILE);
64 let result: RepoResult<ContentsResponse> = async {
65 require_write(actor)?;
66 if path.is_empty() {
67 return Err(RepoError::BadRequest("path is required".into()));
68 }
69 if input
70 .opts
71 .message
72 .as_deref()
73 .unwrap_or("")
74 .trim()
75 .is_empty()
76 {
77 return Err(RepoError::BadRequest("commit message is required".into()));
78 }
79 Err(RepoError::Internal(
80 "create_file requires lib/git rewire (in progress)".into(),
81 ))
5382 }
54 if input
55 .opts
56 .message
57 .as_deref()
58 .unwrap_or("")
59 .trim()
60 .is_empty()
61 {
62 return Err(RepoError::BadRequest("commit message is required".into()));
63 }
64 Err(RepoError::Internal(
65 "create_file requires lib/git rewire (in progress)".into(),
66 ))
83 .await;
84 metrics::record_outcome(metrics::FEATURE_CONTENTS_CREATE_FILE, &result);
85 result
6786}
6887
6988pub async fn update_file(
@@ -73,18 +92,24 @@
7392 path: &str,
7493 input: UpdateFileOption,
7594) -> RepoResult<ContentsResponse> {
76 require_write(actor)?;
77 if path.is_empty() {
78 return Err(RepoError::BadRequest("path is required".into()));
79 }
80 if input.sha.as_deref().unwrap_or("").is_empty() {
81 return Err(RepoError::BadRequest(
82 "expected sha of the existing file".into(),
83 ));
95 let _timer = metrics::Timer::start(metrics::FEATURE_CONTENTS_UPDATE_FILE);
96 let result: RepoResult<ContentsResponse> = async {
97 require_write(actor)?;
98 if path.is_empty() {
99 return Err(RepoError::BadRequest("path is required".into()));
100 }
101 if input.sha.as_deref().unwrap_or("").is_empty() {
102 return Err(RepoError::BadRequest(
103 "expected sha of the existing file".into(),
104 ));
105 }
106 Err(RepoError::Internal(
107 "update_file requires lib/git rewire (in progress)".into(),
108 ))
84109 }
85 Err(RepoError::Internal(
86 "update_file requires lib/git rewire (in progress)".into(),
87 ))
110 .await;
111 metrics::record_outcome(metrics::FEATURE_CONTENTS_UPDATE_FILE, &result);
112 result
88113}
89114
90115pub async fn delete_file(
@@ -94,18 +119,24 @@
94119 path: &str,
95120 input: DeleteFileOption,
96121) -> RepoResult<()> {
97 require_write(actor)?;
98 if path.is_empty() {
99 return Err(RepoError::BadRequest("path is required".into()));
100 }
101 if input.sha.is_empty() {
102 return Err(RepoError::BadRequest(
103 "expected sha of the file to delete".into(),
104 ));
122 let _timer = metrics::Timer::start(metrics::FEATURE_CONTENTS_DELETE_FILE);
123 let result: RepoResult<()> = async {
124 require_write(actor)?;
125 if path.is_empty() {
126 return Err(RepoError::BadRequest("path is required".into()));
127 }
128 if input.sha.is_empty() {
129 return Err(RepoError::BadRequest(
130 "expected sha of the file to delete".into(),
131 ));
132 }
133 Err(RepoError::Internal(
134 "delete_file requires lib/git rewire (in progress)".into(),
135 ))
105136 }
106 Err(RepoError::Internal(
107 "delete_file requires lib/git rewire (in progress)".into(),
108 ))
137 .await;
138 metrics::record_outcome(metrics::FEATURE_CONTENTS_DELETE_FILE, &result);
139 result
109140}
110141
111142pub async fn change_files(
@@ -114,15 +145,21 @@
114145 _repo_id: Uuid,
115146 input: ChangeFilesOptions,
116147) -> RepoResult<ContentsListResponse> {
117 require_write(actor)?;
118 if input.files.is_empty() {
119 return Err(RepoError::BadRequest(
120 "at least one file entry required".into(),
121 ));
148 let _timer = metrics::Timer::start(metrics::FEATURE_CONTENTS_CHANGE_FILES);
149 let result: RepoResult<ContentsListResponse> = async {
150 require_write(actor)?;
151 if input.files.is_empty() {
152 return Err(RepoError::BadRequest(
153 "at least one file entry required".into(),
154 ));
155 }
156 Err(RepoError::Internal(
157 "change_files requires lib/git rewire (in progress)".into(),
158 ))
122159 }
123 Err(RepoError::Internal(
124 "change_files requires lib/git rewire (in progress)".into(),
125 ))
160 .await;
161 metrics::record_outcome(metrics::FEATURE_CONTENTS_CHANGE_FILES, &result);
162 result
126163}
127164
128165pub async fn apply_diff_patch(
@@ -131,11 +168,17 @@
131168 _repo_id: Uuid,
132169 input: ApplyDiffPatchFileOptions,
133170) -> RepoResult<ContentsResponse> {
134 require_write(actor)?;
135 if input.content.is_empty() {
136 return Err(RepoError::BadRequest("patch content is required".into()));
171 let _timer = metrics::Timer::start(metrics::FEATURE_CONTENTS_APPLY_DIFF_PATCH);
172 let result: RepoResult<ContentsResponse> = async {
173 require_write(actor)?;
174 if input.content.is_empty() {
175 return Err(RepoError::BadRequest("patch content is required".into()));
176 }
177 Err(RepoError::Internal(
178 "apply_diff_patch requires lib/git rewire (in progress)".into(),
179 ))
137180 }
138 Err(RepoError::Internal(
139 "apply_diff_patch requires lib/git rewire (in progress)".into(),
140 ))
181 .await;
182 metrics::record_outcome(metrics::FEATURE_CONTENTS_APPLY_DIFF_PATCH, &result);
183 result
141184}
Modified lib/repo/features/deploy_key/usecase.rs lib/repo/features/deploy_key/usecase.rs +79 −54 View lib/repo/features/deploy_key/usecase.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{RepoError, RepoResult};
77use crate::features::RepoService;
8use crate::metrics;
89
910use super::{CreateDeployKeyOption, DeployKeyPage, DeployKeyView, ListDeployKeyQuery};
1011
@@ -33,31 +34,37 @@
3334 repo_id: Uuid,
3435 q: ListDeployKeyQuery,
3536) -> RepoResult<DeployKeyPage> {
36 let _ = actor;
37 let limit = q.limit.unwrap_or(svc.config.default_pagination);
38 let offset = q.page.unwrap_or(0).saturating_mul(limit);
39 let limit = std::cmp::min(limit, svc.config.max_pagination);
40 let db = svc.db.reader();
41 let mut query = key_entity::Entity::find().filter(key_entity::Column::RepoId.eq(repo_id));
42 if let Some(id) = q.key_id {
43 query = query.filter(key_entity::Column::Id.eq(id));
44 }
45 if let Some(fp) = q.fingerprint {
46 query = query.filter(key_entity::Column::Fingerprint.eq(fp));
37 let _timer = metrics::Timer::start(metrics::FEATURE_DEPLOY_KEYS_LIST);
38 let result: RepoResult<DeployKeyPage> = async {
39 let _ = actor;
40 let limit = q.limit.unwrap_or(svc.config.default_pagination);
41 let offset = q.page.unwrap_or(0).saturating_mul(limit);
42 let limit = std::cmp::min(limit, svc.config.max_pagination);
43 let db = svc.db.reader();
44 let mut query = key_entity::Entity::find().filter(key_entity::Column::RepoId.eq(repo_id));
45 if let Some(id) = q.key_id {
46 query = query.filter(key_entity::Column::Id.eq(id));
47 }
48 if let Some(fp) = q.fingerprint {
49 query = query.filter(key_entity::Column::Fingerprint.eq(fp));
50 }
51 let total = query.clone().count(&db).await?;
52 let items = query
53 .limit(Some(limit))
54 .offset(Some(offset))
55 .all(&db)
56 .await?
57 .into_iter()
58 .map(DeployKeyView::from)
59 .collect();
60 Ok(DeployKeyPage {
61 items,
62 total: total as i64,
63 })
4764 }
48 let total = query.clone().count(&db).await?;
49 let items = query
50 .limit(Some(limit))
51 .offset(Some(offset))
52 .all(&db)
53 .await?
54 .into_iter()
55 .map(DeployKeyView::from)
56 .collect();
57 Ok(DeployKeyPage {
58 items,
59 total: total as i64,
60 })
65 .await;
66 metrics::record_outcome(metrics::FEATURE_DEPLOY_KEYS_LIST, &result);
67 result
6168}
6269
6370pub async fn get_deploy_key(
@@ -66,9 +73,15 @@
6673 repo_id: Uuid,
6774 key_id: Uuid,
6875) -> RepoResult<DeployKeyView> {
69 let _ = actor;
70 let key = load_key(svc, repo_id, key_id).await?;
71 Ok(DeployKeyView::from(key))
76 let _timer = metrics::Timer::start(metrics::FEATURE_DEPLOY_KEYS_GET);
77 let result: RepoResult<DeployKeyView> = async {
78 let _ = actor;
79 let key = load_key(svc, repo_id, key_id).await?;
80 Ok(DeployKeyView::from(key))
81 }
82 .await;
83 metrics::record_outcome(metrics::FEATURE_DEPLOY_KEYS_GET, &result);
84 result
7285}
7386
7487pub async fn create_deploy_key(
@@ -77,29 +90,35 @@
7790 repo_id: Uuid,
7891 input: CreateDeployKeyOption,
7992) -> RepoResult<DeployKeyView> {
80 require_admin(actor)?;
81 if input.title.trim().is_empty() {
82 return Err(RepoError::BadRequest("title is required".into()));
93 let _timer = metrics::Timer::start(metrics::FEATURE_DEPLOY_KEYS_CREATE);
94 let result: RepoResult<DeployKeyView> = async {
95 require_admin(actor)?;
96 if input.title.trim().is_empty() {
97 return Err(RepoError::BadRequest("title is required".into()));
98 }
99 if input.key.trim().is_empty() {
100 return Err(RepoError::BadRequest("key content is required".into()));
101 }
102 let fingerprint = format!("sha256:{}", blake3_like_hash(&input.key));
103 let db = svc.db.writer();
104 let now = OffsetDateTime::now_utc();
105 let active: key_entity::ActiveModel = key_entity::ActiveModel {
106 id: Set(Uuid::new_v4()),
107 repo_id: Set(repo_id),
108 title: Set(input.title),
109 key_type: Set(db::repo::repo_deploy_keys::DeployKeyType::Ed25519),
110 fingerprint: Set(fingerprint),
111 content: Set(input.key),
112 is_read_only: Set(input.read_only.unwrap_or(false)),
113 last_used_at: Set(None),
114 created_at: Set(now),
115 };
116 let saved = active.insert(&db).await?;
117 Ok(DeployKeyView::from(saved))
83118 }
84 if input.key.trim().is_empty() {
85 return Err(RepoError::BadRequest("key content is required".into()));
86 }
87 let fingerprint = format!("sha256:{}", blake3_like_hash(&input.key));
88 let db = svc.db.writer();
89 let now = OffsetDateTime::now_utc();
90 let active: key_entity::ActiveModel = key_entity::ActiveModel {
91 id: Set(Uuid::new_v4()),
92 repo_id: Set(repo_id),
93 title: Set(input.title),
94 key_type: Set(db::repo::repo_deploy_keys::DeployKeyType::Ed25519),
95 fingerprint: Set(fingerprint),
96 content: Set(input.key),
97 is_read_only: Set(input.read_only.unwrap_or(false)),
98 last_used_at: Set(None),
99 created_at: Set(now),
100 };
101 let saved = active.insert(&db).await?;
102 Ok(DeployKeyView::from(saved))
119 .await;
120 metrics::record_outcome(metrics::FEATURE_DEPLOY_KEYS_CREATE, &result);
121 result
103122}
104123
105124pub async fn delete_deploy_key(
@@ -108,11 +127,17 @@
108127 repo_id: Uuid,
109128 key_id: Uuid,
110129) -> RepoResult<()> {
111 require_admin(actor)?;
112 let _ = load_key(svc, repo_id, key_id).await?;
113 let db = svc.db.writer();
114 key_entity::Entity::delete_by_id(key_id).exec(&db).await?;
115 Ok(())
130 let _timer = metrics::Timer::start(metrics::FEATURE_DEPLOY_KEYS_DELETE);
131 let result: RepoResult<()> = async {
132 require_admin(actor)?;
133 let _ = load_key(svc, repo_id, key_id).await?;
134 let db = svc.db.writer();
135 key_entity::Entity::delete_by_id(key_id).exec(&db).await?;
136 Ok(())
137 }
138 .await;
139 metrics::record_outcome(metrics::FEATURE_DEPLOY_KEYS_DELETE, &result);
140 result
116141}
117142
118143fn blake3_like_hash(s: &str) -> String {
Modified lib/repo/features/extras/usecase.rs lib/repo/features/extras/usecase.rs +291 −98 View lib/repo/features/extras/usecase.rs
@@ -4,34 +4,59 @@
44
55use crate::error::{RepoError, RepoResult};
66use crate::features::RepoService;
7use crate::metrics;
78
89use super::*;
910
1011pub async fn search_repos(_svc: &RepoService) -> RepoResult<SearchResults> {
11 Ok(SearchResults {
12 data: Vec::new(),
13 ok: true,
14 })
12 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_SEARCH_REPOS);
13 let result: RepoResult<SearchResults> = async {
14 Ok(SearchResults {
15 data: Vec::new(),
16 ok: true,
17 })
18 }
19 .await;
20 metrics::record_outcome(metrics::FEATURE_EXTRAS_SEARCH_REPOS, &result);
21 result
1522}
1623
1724pub async fn get_repo_by_id(_svc: &RepoService) -> RepoResult<RepoSummary> {
18 Err(RepoError::NotFound(
19 "repo by id lookup not implemented".into(),
20 ))
25 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_REPO_BY_ID);
26 let result: RepoResult<RepoSummary> = async {
27 Err(RepoError::NotFound(
28 "repo by id lookup not implemented".into(),
29 ))
30 }
31 .await;
32 metrics::record_outcome(metrics::FEATURE_EXTRAS_GET_REPO_BY_ID, &result);
33 result
2134}
2235
2336pub async fn update_repo_avatar(_svc: &RepoService, actor: Uuid, _repo_id: Uuid) -> RepoResult<()> {
24 if actor.is_nil() {
25 return Err(RepoError::Unauthorized("authentication required".into()));
37 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_UPDATE_REPO_AVATAR);
38 let result: RepoResult<()> = async {
39 if actor.is_nil() {
40 return Err(RepoError::Unauthorized("authentication required".into()));
41 }
42 Ok(())
2643 }
27 Ok(())
44 .await;
45 metrics::record_outcome(metrics::FEATURE_EXTRAS_UPDATE_REPO_AVATAR, &result);
46 result
2847}
2948
3049pub async fn delete_repo_avatar(_svc: &RepoService, actor: Uuid, _repo_id: Uuid) -> RepoResult<()> {
31 if actor.is_nil() {
32 return Err(RepoError::Unauthorized("authentication required".into()));
50 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_DELETE_REPO_AVATAR);
51 let result: RepoResult<()> = async {
52 if actor.is_nil() {
53 return Err(RepoError::Unauthorized("authentication required".into()));
54 }
55 Ok(())
3356 }
34 Ok(())
57 .await;
58 metrics::record_outcome(metrics::FEATURE_EXTRAS_DELETE_REPO_AVATAR, &result);
59 result
3560}
3661
3762pub async fn list_issue_templates(
@@ -39,7 +64,10 @@
3964 _actor: Uuid,
4065 _repo_id: Uuid,
4166) -> RepoResult<Vec<IssueTemplate>> {
42 Ok(Vec::new())
67 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_LIST_ISSUE_TEMPLATES);
68 let result: RepoResult<Vec<IssueTemplate>> = async { Ok(Vec::new()) }.await;
69 metrics::record_outcome(metrics::FEATURE_EXTRAS_LIST_ISSUE_TEMPLATES, &result);
70 result
4371}
4472
4573pub async fn get_issue_config(
@@ -47,10 +75,16 @@
4775 _actor: Uuid,
4876 _repo_id: Uuid,
4977) -> RepoResult<IssueConfig> {
50 Ok(IssueConfig {
51 blank_issues_enabled: true,
52 contact_links: None,
53 })
78 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_ISSUE_CONFIG);
79 let result: RepoResult<IssueConfig> = async {
80 Ok(IssueConfig {
81 blank_issues_enabled: true,
82 contact_links: None,
83 })
84 }
85 .await;
86 metrics::record_outcome(metrics::FEATURE_EXTRAS_GET_ISSUE_CONFIG, &result);
87 result
5488}
5589
5690pub async fn validate_issue_config(
@@ -58,10 +92,16 @@
5892 _actor: Uuid,
5993 _repo_id: Uuid,
6094) -> RepoResult<IssueConfigValidation> {
61 Ok(IssueConfigValidation {
62 valid: true,
63 message: None,
64 })
95 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_VALIDATE_ISSUE_CONFIG);
96 let result: RepoResult<IssueConfigValidation> = async {
97 Ok(IssueConfigValidation {
98 valid: true,
99 message: None,
100 })
101 }
102 .await;
103 metrics::record_outcome(metrics::FEATURE_EXTRAS_VALIDATE_ISSUE_CONFIG, &result);
104 result
65105}
66106
67107pub async fn get_languages(
@@ -69,9 +109,15 @@
69109 _actor: Uuid,
70110 _repo_id: Uuid,
71111) -> RepoResult<LanguagesResponse> {
72 Ok(LanguagesResponse {
73 languages: std::collections::BTreeMap::new(),
74 })
112 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_LANGUAGES);
113 let result: RepoResult<LanguagesResponse> = async {
114 Ok(LanguagesResponse {
115 languages: std::collections::BTreeMap::new(),
116 })
117 }
118 .await;
119 metrics::record_outcome(metrics::FEATURE_EXTRAS_GET_LANGUAGES, &result);
120 result
75121}
76122
77123pub async fn get_licenses(
@@ -79,120 +125,231 @@
79125 _actor: Uuid,
80126 _repo_id: Uuid,
81127) -> RepoResult<LicensesResponse> {
82 Ok(LicensesResponse {
83 licenses: Vec::new(),
84 })
128 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_LICENSES);
129 let result: RepoResult<LicensesResponse> = async {
130 Ok(LicensesResponse {
131 licenses: Vec::new(),
132 })
133 }
134 .await;
135 metrics::record_outcome(metrics::FEATURE_EXTRAS_GET_LICENSES, &result);
136 result
85137}
86138
87139pub async fn list_stargazers(_svc: &RepoService) -> RepoResult<Vec<UserMini>> {
88 Ok(Vec::new())
140 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_LIST_STARGAZERS);
141 let result: RepoResult<Vec<UserMini>> = async { Ok(Vec::new()) }.await;
142 metrics::record_outcome(metrics::FEATURE_EXTRAS_LIST_STARGAZERS, &result);
143 result
89144}
90145
91146pub async fn list_assignees(_svc: &RepoService) -> RepoResult<Vec<UserMini>> {
92 Ok(Vec::new())
147 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_LIST_ASSIGNEES);
148 let result: RepoResult<Vec<UserMini>> = async { Ok(Vec::new()) }.await;
149 metrics::record_outcome(metrics::FEATURE_EXTRAS_LIST_ASSIGNEES, &result);
150 result
93151}
94152
95153pub async fn check_assignee(_svc: &RepoService) -> RepoResult<bool> {
96 Ok(false)
154 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_CHECK_ASSIGNEE);
155 let result: RepoResult<bool> = async { Ok(false) }.await;
156 metrics::record_outcome(metrics::FEATURE_EXTRAS_CHECK_ASSIGNEE, &result);
157 result
97158}
98159
99160pub async fn list_reviewers(_svc: &RepoService) -> RepoResult<Vec<UserMini>> {
100 Ok(Vec::new())
161 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_LIST_REVIEWERS);
162 let result: RepoResult<Vec<UserMini>> = async { Ok(Vec::new()) }.await;
163 metrics::record_outcome(metrics::FEATURE_EXTRAS_LIST_REVIEWERS, &result);
164 result
101165}
102166
103167pub async fn check_new_pin_allowed(_svc: &RepoService) -> RepoResult<NewPinAllowedResponse> {
104 Ok(NewPinAllowedResponse { allowed: true })
168 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_CHECK_NEW_PIN_ALLOWED);
169 let result: RepoResult<NewPinAllowedResponse> = async {
170 Ok(NewPinAllowedResponse { allowed: true })
171 }
172 .await;
173 metrics::record_outcome(metrics::FEATURE_EXTRAS_CHECK_NEW_PIN_ALLOWED, &result);
174 result
105175}
106176
107177pub async fn get_file_contents(_svc: &RepoService) -> RepoResult<Vec<serde_json::Value>> {
108 Err(RepoError::Internal(
109 "batch file-contents requires lib/git rewire".into(),
110 ))
178 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_FILE_CONTENTS);
179 let result: RepoResult<Vec<serde_json::Value>> = async {
180 Err(RepoError::Internal(
181 "batch file-contents requires lib/git rewire".into(),
182 ))
183 }
184 .await;
185 metrics::record_outcome(metrics::FEATURE_EXTRAS_GET_FILE_CONTENTS, &result);
186 result
111187}
112188
113189pub async fn list_commits(_svc: &RepoService) -> RepoResult<CommitsResponse> {
114 Ok(CommitsResponse {
115 items: Vec::new(),
116 total: 0,
117 })
190 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_LIST_COMMITS);
191 let result: RepoResult<CommitsResponse> = async {
192 Ok(CommitsResponse {
193 items: Vec::new(),
194 total: 0,
195 })
196 }
197 .await;
198 metrics::record_outcome(metrics::FEATURE_EXTRAS_LIST_COMMITS, &result);
199 result
118200}
119201
120202pub async fn get_commit(_svc: &RepoService) -> RepoResult<CommitSummary> {
121 Err(RepoError::Internal(
122 "get_commit requires lib/git rewire".into(),
123 ))
203 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_COMMIT);
204 let result: RepoResult<CommitSummary> = async {
205 Err(RepoError::Internal(
206 "get_commit requires lib/git rewire".into(),
207 ))
208 }
209 .await;
210 metrics::record_outcome(metrics::FEATURE_EXTRAS_GET_COMMIT, &result);
211 result
124212}
125213
126214pub async fn get_commit_diff_or_patch(_svc: &RepoService) -> RepoResult<String> {
127 Err(RepoError::Internal(
128 "get_commit_diff_or_patch requires lib/git rewire".into(),
129 ))
215 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_COMMIT_DIFF_OR_PATCH);
216 let result: RepoResult<String> = async {
217 Err(RepoError::Internal(
218 "get_commit_diff_or_patch requires lib/git rewire".into(),
219 ))
220 }
221 .await;
222 metrics::record_outcome(metrics::FEATURE_EXTRAS_GET_COMMIT_DIFF_OR_PATCH, &result);
223 result
130224}
131225
132226pub async fn get_combined_status_by_ref(_svc: &RepoService) -> RepoResult<serde_json::Value> {
133 Ok(serde_json::json!({
134 "state": "pending",
135 "sha": "",
136 "statuses": [],
137 "total_count": 0,
138 }))
227 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_COMBINED_STATUS_BY_REF);
228 let result: RepoResult<serde_json::Value> = async {
229 Ok(serde_json::json!({
230 "state": "pending",
231 "sha": "",
232 "statuses": [],
233 "total_count": 0,
234 }))
235 }
236 .await;
237 metrics::record_outcome(
238 metrics::FEATURE_EXTRAS_GET_COMBINED_STATUS_BY_REF,
239 &result,
240 );
241 result
139242}
140243
141244pub async fn get_statuses_by_ref(_svc: &RepoService) -> RepoResult<Vec<serde_json::Value>> {
142 Ok(Vec::new())
245 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_STATUSES_BY_REF);
246 let result: RepoResult<Vec<serde_json::Value>> = async { Ok(Vec::new()) }.await;
247 metrics::record_outcome(metrics::FEATURE_EXTRAS_GET_STATUSES_BY_REF, &result);
248 result
143249}
144250
145251pub async fn compare_commits(_svc: &RepoService) -> RepoResult<CommitComparison> {
146 Err(RepoError::Internal(
147 "compare requires lib/git rewire".into(),
148 ))
252 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_COMPARE_COMMITS);
253 let result: RepoResult<CommitComparison> = async {
254 Err(RepoError::Internal(
255 "compare requires lib/git rewire".into(),
256 ))
257 }
258 .await;
259 metrics::record_outcome(metrics::FEATURE_EXTRAS_COMPARE_COMMITS, &result);
260 result
149261}
150262
151263pub async fn list_git_refs(_svc: &RepoService) -> RepoResult<Vec<GitRef>> {
152 Ok(Vec::new())
264 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_LIST_GIT_REFS);
265 let result: RepoResult<Vec<GitRef>> = async { Ok(Vec::new()) }.await;
266 metrics::record_outcome(metrics::FEATURE_EXTRAS_LIST_GIT_REFS, &result);
267 result
153268}
154269
155270pub async fn get_tree(_svc: &RepoService) -> RepoResult<GitTreeResponse> {
156 Err(RepoError::Internal(
157 "get_tree requires lib/git rewire".into(),
158 ))
271 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_TREE);
272 let result: RepoResult<GitTreeResponse> = async {
273 Err(RepoError::Internal(
274 "get_tree requires lib/git rewire".into(),
275 ))
276 }
277 .await;
278 metrics::record_outcome(metrics::FEATURE_EXTRAS_GET_TREE, &result);
279 result
159280}
160281
161282pub async fn get_blob(_svc: &RepoService) -> RepoResult<GitBlobResponse> {
162 Err(RepoError::Internal(
163 "get_blob requires lib/git rewire".into(),
164 ))
283 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_BLOB);
284 let result: RepoResult<GitBlobResponse> = async {
285 Err(RepoError::Internal(
286 "get_blob requires lib/git rewire".into(),
287 ))
288 }
289 .await;
290 metrics::record_outcome(metrics::FEATURE_EXTRAS_GET_BLOB, &result);
291 result
165292}
166293
167294pub async fn get_note(_svc: &RepoService) -> RepoResult<GitNote> {
168 Err(RepoError::Internal(
169 "get_note requires lib/git rewire".into(),
170 ))
295 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_NOTE);
296 let result: RepoResult<GitNote> = async {
297 Err(RepoError::Internal(
298 "get_note requires lib/git rewire".into(),
299 ))
300 }
301 .await;
302 metrics::record_outcome(metrics::FEATURE_EXTRAS_GET_NOTE, &result);
303 result
171304}
172305
173306pub async fn get_raw_file(_svc: &RepoService) -> RepoResult<Vec<u8>> {
174 Err(RepoError::Internal(
175 "get_raw_file requires lib/git rewire".into(),
176 ))
307 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_RAW_FILE);
308 let result: RepoResult<Vec<u8>> = async {
309 Err(RepoError::Internal(
310 "get_raw_file requires lib/git rewire".into(),
311 ))
312 }
313 .await;
314 metrics::record_outcome(metrics::FEATURE_EXTRAS_GET_RAW_FILE, &result);
315 result
177316}
178317
179318pub async fn get_media(_svc: &RepoService) -> RepoResult<Vec<u8>> {
180 Err(RepoError::Internal(
181 "get_media requires lib/git rewire".into(),
182 ))
319 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_MEDIA);
320 let result: RepoResult<Vec<u8>> = async {
321 Err(RepoError::Internal(
322 "get_media requires lib/git rewire".into(),
323 ))
324 }
325 .await;
326 metrics::record_outcome(metrics::FEATURE_EXTRAS_GET_MEDIA, &result);
327 result
183328}
184329
185330pub async fn get_editorconfig(_svc: &RepoService) -> RepoResult<String> {
186 Err(RepoError::Internal(
187 "get_editorconfig requires lib/git rewire".into(),
188 ))
331 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_EDITORCONFIG);
332 let result: RepoResult<String> = async {
333 Err(RepoError::Internal(
334 "get_editorconfig requires lib/git rewire".into(),
335 ))
336 }
337 .await;
338 metrics::record_outcome(metrics::FEATURE_EXTRAS_GET_EDITORCONFIG, &result);
339 result
189340}
190341
191342pub async fn list_forks(_svc: &RepoService) -> RepoResult<ForksResponse> {
192 Ok(ForksResponse {
193 items: Vec::new(),
194 total: 0,
195 })
343 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_LIST_FORKS);
344 let result: RepoResult<ForksResponse> = async {
345 Ok(ForksResponse {
346 items: Vec::new(),
347 total: 0,
348 })
349 }
350 .await;
351 metrics::record_outcome(metrics::FEATURE_EXTRAS_LIST_FORKS, &result);
352 result
196353}
197354
198355pub async fn list_release_attachments(
@@ -201,7 +358,16 @@
201358 _repo_id: Uuid,
202359 _release_id: Uuid,
203360) -> RepoResult<ReleaseAttachmentListResponse> {
204 Ok(ReleaseAttachmentListResponse { items: Vec::new() })
361 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_LIST_RELEASE_ATTACHMENTS);
362 let result: RepoResult<ReleaseAttachmentListResponse> = async {
363 Ok(ReleaseAttachmentListResponse { items: Vec::new() })
364 }
365 .await;
366 metrics::record_outcome(
367 metrics::FEATURE_EXTRAS_LIST_RELEASE_ATTACHMENTS,
368 &result,
369 );
370 result
205371}
206372
207373pub async fn get_release_attachment(
@@ -211,7 +377,16 @@
211377 _release_id: Uuid,
212378 _attachment_id: Uuid,
213379) -> RepoResult<ReleaseAttachmentView> {
214 Err(RepoError::NotFound("release attachment".into()))
380 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_RELEASE_ATTACHMENT);
381 let result: RepoResult<ReleaseAttachmentView> = async {
382 Err(RepoError::NotFound("release attachment".into()))
383 }
384 .await;
385 metrics::record_outcome(
386 metrics::FEATURE_EXTRAS_GET_RELEASE_ATTACHMENT,
387 &result,
388 );
389 result
215390}
216391
217392pub async fn watch_repo(
@@ -219,25 +394,37 @@
219394 actor: Uuid,
220395 _repo_id: Uuid,
221396) -> RepoResult<Subscription> {
222 if actor.is_nil() {
223 return Err(RepoError::Unauthorized("authentication required".into()));
397 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_WATCH_REPO);
398 let result: RepoResult<Subscription> = async {
399 if actor.is_nil() {
400 return Err(RepoError::Unauthorized("authentication required".into()));
401 }
402 let now = OffsetDateTime::now_utc()
403 .format(&time::format_description::well_known::Rfc3339)
404 .unwrap_or_default();
405 Ok(Subscription {
406 user_id: actor,
407 repo_id: Uuid::nil(),
408 mode: "watch".to_string(),
409 created_at: now,
410 })
224411 }
225 let now = OffsetDateTime::now_utc()
226 .format(&time::format_description::well_known::Rfc3339)
227 .unwrap_or_default();
228 Ok(Subscription {
229 user_id: actor,
230 repo_id: Uuid::nil(),
231 mode: "watch".to_string(),
232 created_at: now,
233 })
412 .await;
413 metrics::record_outcome(metrics::FEATURE_EXTRAS_WATCH_REPO, &result);
414 result
234415}
235416
236417pub async fn unwatch_repo(_svc: &RepoService, actor: Uuid, _repo_id: Uuid) -> RepoResult<()> {
237 if actor.is_nil() {
238 return Err(RepoError::Unauthorized("authentication required".into()));
418 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_UNWATCH_REPO);
419 let result: RepoResult<()> = async {
420 if actor.is_nil() {
421 return Err(RepoError::Unauthorized("authentication required".into()));
422 }
423 Ok(())
239424 }
240 Ok(())
425 .await;
426 metrics::record_outcome(metrics::FEATURE_EXTRAS_UNWATCH_REPO, &result);
427 result
241428}
242429
243430pub async fn get_signing_key_gpg(
@@ -245,7 +432,10 @@
245432 _actor: Uuid,
246433 _repo_id: Uuid,
247434) -> RepoResult<String> {
248 Ok(String::new())
435 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_SIGNING_KEY_GPG);
436 let result: RepoResult<String> = async { Ok(String::new()) }.await;
437 metrics::record_outcome(metrics::FEATURE_EXTRAS_GET_SIGNING_KEY_GPG, &result);
438 result
249439}
250440
251441pub async fn get_signing_key_ssh(
@@ -253,5 +443,8 @@
253443 _actor: Uuid,
254444 _repo_id: Uuid,
255445) -> RepoResult<String> {
256 Ok(String::new())
257}
446 let _timer = metrics::Timer::start(metrics::FEATURE_EXTRAS_GET_SIGNING_KEY_SSH);
447 let result: RepoResult<String> = async { Ok(String::new()) }.await;
448 metrics::record_outcome(metrics::FEATURE_EXTRAS_GET_SIGNING_KEY_SSH, &result);
449 result
450}
No newline at end of file
Modified lib/repo/features/git_ref/usecase.rs lib/repo/features/git_ref/usecase.rs +86 −43 View lib/repo/features/git_ref/usecase.rs
@@ -1,5 +1,6 @@
11use crate::error::{RepoError, RepoResult};
22use crate::features::RepoService;
3use crate::metrics;
34
45use super::{BlobView, CommitDiffPayload, CommitView, GitRef, NoteView, TreeView};
56
@@ -8,10 +9,16 @@
89 actor: uuid::Uuid,
910 _repo_id: uuid::Uuid,
1011) -> RepoResult<Vec<GitRef>> {
11 let _ = actor;
12 Err(RepoError::Internal(
13 "list refs requires lib/git rewire (in progress)".into(),
14 ))
12 let _timer = metrics::Timer::start(metrics::FEATURE_GIT_REFS_LIST);
13 let result: RepoResult<Vec<GitRef>> = async {
14 let _ = actor;
15 Err(RepoError::Internal(
16 "list refs requires lib/git rewire (in progress)".into(),
17 ))
18 }
19 .await;
20 metrics::record_outcome(metrics::FEATURE_GIT_REFS_LIST, &result);
21 result
1522}
1623
1724pub async fn filter_git_refs(
@@ -20,10 +27,16 @@
2027 _repo_id: uuid::Uuid,
2128 _filter: &str,
2229) -> RepoResult<Vec<GitRef>> {
23 let _ = actor;
24 Err(RepoError::Internal(
25 "filter refs requires lib/git rewire (in progress)".into(),
26 ))
30 let _timer = metrics::Timer::start(metrics::FEATURE_GIT_REFS_FILTER);
31 let result: RepoResult<Vec<GitRef>> = async {
32 let _ = actor;
33 Err(RepoError::Internal(
34 "filter refs requires lib/git rewire (in progress)".into(),
35 ))
36 }
37 .await;
38 metrics::record_outcome(metrics::FEATURE_GIT_REFS_FILTER, &result);
39 result
2740}
2841
2942pub async fn get_commit(
@@ -32,13 +45,19 @@
3245 _repo_id: uuid::Uuid,
3346 sha: &str,
3447) -> RepoResult<CommitView> {
35 let _ = actor;
36 if sha.is_empty() {
37 return Err(RepoError::BadRequest("sha is required".into()));
48 let _timer = metrics::Timer::start(metrics::FEATURE_GIT_REFS_GET_COMMIT);
49 let result: RepoResult<CommitView> = async {
50 let _ = actor;
51 if sha.is_empty() {
52 return Err(RepoError::BadRequest("sha is required".into()));
53 }
54 Err(RepoError::Internal(
55 "get_commit requires lib/git rewire (in progress)".into(),
56 ))
3857 }
39 Err(RepoError::Internal(
40 "get_commit requires lib/git rewire (in progress)".into(),
41 ))
58 .await;
59 metrics::record_outcome(metrics::FEATURE_GIT_REFS_GET_COMMIT, &result);
60 result
4261}
4362
4463pub async fn get_commit_diff_or_patch(
@@ -48,18 +67,24 @@
4867 sha: &str,
4968 diff_type: &str,
5069) -> RepoResult<CommitDiffPayload> {
51 let _ = actor;
52 if diff_type != "diff" && diff_type != "patch" {
53 return Err(RepoError::BadRequest(format!(
54 "unsupported diff format: {diff_type}"
55 )));
56 }
57 if sha.is_empty() {
58 return Err(RepoError::BadRequest("sha is required".into()));
70 let _timer = metrics::Timer::start(metrics::FEATURE_GIT_REFS_GET_COMMIT_DIFF_OR_PATCH);
71 let result: RepoResult<CommitDiffPayload> = async {
72 let _ = actor;
73 if diff_type != "diff" && diff_type != "patch" {
74 return Err(RepoError::BadRequest(format!(
75 "unsupported diff format: {diff_type}"
76 )));
77 }
78 if sha.is_empty() {
79 return Err(RepoError::BadRequest("sha is required".into()));
80 }
81 Err(RepoError::Internal(
82 "get_commit_diff_or_patch requires lib/git rewire (in progress)".into(),
83 ))
5984 }
60 Err(RepoError::Internal(
61 "get_commit_diff_or_patch requires lib/git rewire (in progress)".into(),
62 ))
85 .await;
86 metrics::record_outcome(metrics::FEATURE_GIT_REFS_GET_COMMIT_DIFF_OR_PATCH, &result);
87 result
6388}
6489
6590pub async fn get_tree(
@@ -69,13 +94,19 @@
6994 sha: &str,
7095 _recursive: bool,
7196) -> RepoResult<TreeView> {
72 let _ = actor;
73 if sha.is_empty() {
74 return Err(RepoError::BadRequest("sha is required".into()));
97 let _timer = metrics::Timer::start(metrics::FEATURE_GIT_REFS_GET_TREE);
98 let result: RepoResult<TreeView> = async {
99 let _ = actor;
100 if sha.is_empty() {
101 return Err(RepoError::BadRequest("sha is required".into()));
102 }
103 Err(RepoError::Internal(
104 "get_tree requires lib/git rewire (in progress)".into(),
105 ))
75106 }
76 Err(RepoError::Internal(
77 "get_tree requires lib/git rewire (in progress)".into(),
78 ))
107 .await;
108 metrics::record_outcome(metrics::FEATURE_GIT_REFS_GET_TREE, &result);
109 result
79110}
80111
81112pub async fn get_blob(
@@ -84,13 +115,19 @@
84115 _repo_id: uuid::Uuid,
85116 sha: &str,
86117) -> RepoResult<BlobView> {
87 let _ = actor;
88 if sha.is_empty() {
89 return Err(RepoError::BadRequest("sha is required".into()));
118 let _timer = metrics::Timer::start(metrics::FEATURE_GIT_REFS_GET_BLOB);
119 let result: RepoResult<BlobView> = async {
120 let _ = actor;
121 if sha.is_empty() {
122 return Err(RepoError::BadRequest("sha is required".into()));
123 }
124 Err(RepoError::Internal(
125 "get_blob requires lib/git rewire (in progress)".into(),
126 ))
90127 }
91 Err(RepoError::Internal(
92 "get_blob requires lib/git rewire (in progress)".into(),
93 ))
128 .await;
129 metrics::record_outcome(metrics::FEATURE_GIT_REFS_GET_BLOB, &result);
130 result
94131}
95132
96133pub async fn get_note(
@@ -99,11 +136,17 @@
99136 _repo_id: uuid::Uuid,
100137 sha: &str,
101138) -> RepoResult<NoteView> {
102 let _ = actor;
103 if sha.is_empty() {
104 return Err(RepoError::BadRequest("sha is required".into()));
139 let _timer = metrics::Timer::start(metrics::FEATURE_GIT_REFS_GET_NOTE);
140 let result: RepoResult<NoteView> = async {
141 let _ = actor;
142 if sha.is_empty() {
143 return Err(RepoError::BadRequest("sha is required".into()));
144 }
145 Err(RepoError::Internal(
146 "get_note requires lib/git rewire (in progress)".into(),
147 ))
105148 }
106 Err(RepoError::Internal(
107 "get_note requires lib/git rewire (in progress)".into(),
108 ))
149 .await;
150 metrics::record_outcome(metrics::FEATURE_GIT_REFS_GET_NOTE, &result);
151 result
109152}
Modified lib/repo/features/lifecycle/usecase/archive.rs lib/repo/features/lifecycle/usecase/archive.rs +16 −9 View lib/repo/features/lifecycle/usecase/archive.rs
@@ -4,6 +4,7 @@
44
55use crate::error::RepoResult;
66use crate::features::RepoService;
7use crate::metrics;
78
89use super::super::{ArchiveRepoOption, RepoView};
910use super::helpers::{load_repo, require_authenticated};
@@ -15,13 +16,19 @@
1516 repo_id: Uuid,
1617 input: ArchiveRepoOption,
1718) -> RepoResult<RepoView> {
18 require_authenticated(actor)?;
19 let repo = load_repo(svc, channel_id, repo_id).await?;
20 let db = svc.db.writer();
21 let now = OffsetDateTime::now_utc();
22 let mut active = db::repo::repos::ActiveModel::from(repo);
23 active.is_archived = Set(input.archived);
24 active.archived_at = Set(if input.archived { Some(now) } else { None });
25 let saved = active.update(&db).await?;
26 Ok(RepoView::from(saved))
19 let _timer = metrics::Timer::start(metrics::FEATURE_REPOS_ARCHIVE);
20 let result: RepoResult<RepoView> = async {
21 require_authenticated(actor)?;
22 let repo = load_repo(svc, channel_id, repo_id).await?;
23 let db = svc.db.writer();
24 let now = OffsetDateTime::now_utc();
25 let mut active = db::repo::repos::ActiveModel::from(repo);
26 active.is_archived = Set(input.archived);
27 active.archived_at = Set(if input.archived { Some(now) } else { None });
28 let saved = active.update(&db).await?;
29 Ok(RepoView::from(saved))
30 }
31 .await;
32 metrics::record_outcome(metrics::FEATURE_REPOS_ARCHIVE, &result);
33 result
2734}
Modified lib/repo/features/lifecycle/usecase/create.rs lib/repo/features/lifecycle/usecase/create.rs +69 −62 View lib/repo/features/lifecycle/usecase/create.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{RepoError, RepoResult};
77use crate::features::RepoService;
8use crate::metrics;
89use crate::path::{validate_object_format, validate_repo_name};
910
1011use super::super::{CreateRepoOption, RepoView};
@@ -15,71 +16,77 @@
1516 actor: Uuid,
1617 input: CreateRepoOption,
1718) -> RepoResult<RepoView> {
18 require_authenticated(actor)?;
19 let _timer = metrics::Timer::start(metrics::FEATURE_REPOS_CREATE);
20 let result: RepoResult<RepoView> = async {
21 require_authenticated(actor)?;
1922
20 let name = validate_repo_name(&input.name, svc.config.max_repo_name_len)?;
21 if let Some(ref desc) = input.description {
22 if desc.len() > svc.config.max_description_len {
23 return Err(RepoError::BadRequest(format!(
24 "description too long (max {})",
25 svc.config.max_description_len
26 )));
23 let name = validate_repo_name(&input.name, svc.config.max_repo_name_len)?;
24 if let Some(ref desc) = input.description {
25 if desc.len() > svc.config.max_description_len {
26 return Err(RepoError::BadRequest(format!(
27 "description too long (max {})",
28 svc.config.max_description_len
29 )));
30 }
2731 }
28 }
29 if let Some(ref web) = input.website {
30 if web.len() > svc.config.max_website_len {
31 return Err(RepoError::BadRequest(format!(
32 "website too long (max {})",
33 svc.config.max_website_len
34 )));
32 if let Some(ref web) = input.website {
33 if web.len() > svc.config.max_website_len {
34 return Err(RepoError::BadRequest(format!(
35 "website too long (max {})",
36 svc.config.max_website_len
37 )));
38 }
3539 }
36 }
37 let object_format = match input.object_format.as_deref() {
38 Some(fmt) => validate_object_format(fmt)?.to_string(),
39 None => svc.config.default_object_format.clone(),
40 };
41 let default_branch = input
42 .default_branch
43 .unwrap_or_else(|| svc.config.default_branch.clone());
40 let object_format = match input.object_format.as_deref() {
41 Some(fmt) => validate_object_format(fmt)?.to_string(),
42 None => svc.config.default_object_format.clone(),
43 };
44 let default_branch = input
45 .default_branch
46 .unwrap_or_else(|| svc.config.default_branch.clone());
4447
45 let db = svc.db.writer();
46 let now = OffsetDateTime::now_utc();
47 let repo_id = Uuid::new_v4();
48 let db = svc.db.writer();
49 let now = OffsetDateTime::now_utc();
50 let repo_id = Uuid::new_v4();
4851
49 let active: repo_entity::ActiveModel = repo_entity::ActiveModel {
50 id: Set(repo_id),
51 channel_id: Set(input.channel_id),
52 lower_name: Set(name.clone()),
53 name: Set(name),
54 description: Set(input.description),
55 website: Set(input.website),
56 default_branch: Set(default_branch),
57 nums_watch: Set(0),
58 nums_star: Set(0),
59 nums_pr: Set(0),
60 nums_commits: Set(0),
61 nums_branches: Set(0),
62 nums_tags: Set(0),
63 enable_lfs: Set(input.enable_lfs.unwrap_or(false)),
64 is_private: Set(input.is_private.unwrap_or(false)),
65 is_archived: Set(false),
66 storage_limit_size: Set(None),
67 size: Set(0),
68 git_size: Set(0),
69 lfs_size: Set(0),
70 avatar_url: Set(None),
71 primary_language: Set(None),
72 license_name: Set(input.license_name),
73 created_at: Set(now),
74 last_pushed_at: Set(None),
75 archived_at: Set(None),
76 readme_raw: Set(None),
77 allow_rebase: Set(true),
78 allow_rebase_explicit: Set(false),
79 allow_squash_merge: Set(true),
80 allow_merge_commits: Set(true),
81 object_format: Set(object_format),
82 };
83 let saved = active.insert(&db).await?;
84 Ok(RepoView::from(saved))
52 let active: repo_entity::ActiveModel = repo_entity::ActiveModel {
53 id: Set(repo_id),
54 channel_id: Set(input.channel_id),
55 lower_name: Set(name.clone()),
56 name: Set(name),
57 description: Set(input.description),
58 website: Set(input.website),
59 default_branch: Set(default_branch),
60 nums_watch: Set(0),
61 nums_star: Set(0),
62 nums_pr: Set(0),
63 nums_commits: Set(0),
64 nums_branches: Set(0),
65 nums_tags: Set(0),
66 enable_lfs: Set(input.enable_lfs.unwrap_or(false)),
67 is_private: Set(input.is_private.unwrap_or(false)),
68 is_archived: Set(false),
69 storage_limit_size: Set(None),
70 size: Set(0),
71 git_size: Set(0),
72 lfs_size: Set(0),
73 avatar_url: Set(None),
74 primary_language: Set(None),
75 license_name: Set(input.license_name),
76 created_at: Set(now),
77 last_pushed_at: Set(None),
78 archived_at: Set(None),
79 readme_raw: Set(None),
80 allow_rebase: Set(true),
81 allow_rebase_explicit: Set(false),
82 allow_squash_merge: Set(true),
83 allow_merge_commits: Set(true),
84 object_format: Set(object_format),
85 };
86 let saved = active.insert(&db).await?;
87 Ok(RepoView::from(saved))
88 }
89 .await;
90 metrics::record_outcome(metrics::FEATURE_REPOS_CREATE, &result);
91 result
8592}
Modified lib/repo/features/lifecycle/usecase/delete.rs lib/repo/features/lifecycle/usecase/delete.rs +14 −7 View lib/repo/features/lifecycle/usecase/delete.rs
@@ -4,6 +4,7 @@
44
55use crate::error::RepoResult;
66use crate::features::RepoService;
7use crate::metrics;
78
89use super::helpers::{load_repo, require_authenticated};
910
@@ -13,11 +14,17 @@
1314 channel_id: Uuid,
1415 repo_id: Uuid,
1516) -> RepoResult<()> {
16 require_authenticated(actor)?;
17 let repo = load_repo(svc, channel_id, repo_id).await?;
18 let db = svc.db.writer();
19 let _ = repo;
20 repo_entity::Entity::delete_by_id(repo_id).exec(&db).await?;
21 let _ = svc.cache.invalidate_repo(repo_id).await;
22 Ok(())
17 let _timer = metrics::Timer::start(metrics::FEATURE_REPOS_DELETE);
18 let result: RepoResult<()> = async {
19 require_authenticated(actor)?;
20 let repo = load_repo(svc, channel_id, repo_id).await?;
21 let db = svc.db.writer();
22 let _ = repo;
23 repo_entity::Entity::delete_by_id(repo_id).exec(&db).await?;
24 let _ = svc.cache.invalidate_repo(repo_id).await;
25 Ok(())
26 }
27 .await;
28 metrics::record_outcome(metrics::FEATURE_REPOS_DELETE, &result);
29 result
2330}
Modified lib/repo/features/lifecycle/usecase/fork.rs lib/repo/features/lifecycle/usecase/fork.rs +52 −45 View lib/repo/features/lifecycle/usecase/fork.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{RepoError, RepoResult};
77use crate::features::RepoService;
8use crate::metrics;
89use crate::path::validate_repo_name;
910
1011use super::super::{ForkRepoOption, RepoView};
@@ -17,49 +18,55 @@
1718 repo_id: Uuid,
1819 input: ForkRepoOption,
1920) -> RepoResult<RepoView> {
20 require_authenticated(actor)?;
21 let _ = load_repo(svc, channel_id, repo_id).await?;
22 let new_name = match input.name {
23 Some(n) => validate_repo_name(&n, svc.config.max_repo_name_len)?,
24 None => format!("{}-fork", Uuid::new_v4()),
25 };
26 let db = svc.db.writer();
27 let now = OffsetDateTime::now_utc();
28 let new_id = Uuid::new_v4();
29 let active: repo_entity::ActiveModel = repo_entity::ActiveModel {
30 id: Set(new_id),
31 channel_id: Set(input.to_channel_id),
32 lower_name: Set(new_name.clone()),
33 name: Set(new_name),
34 description: Set(input.description),
35 website: Set(None),
36 default_branch: Set(crate::path::DEFAULT_OBJECT_FORMAT_SHA1.to_string()),
37 nums_watch: Set(0),
38 nums_star: Set(0),
39 nums_pr: Set(0),
40 nums_commits: Set(0),
41 nums_branches: Set(0),
42 nums_tags: Set(0),
43 enable_lfs: Set(false),
44 is_private: Set(false),
45 is_archived: Set(false),
46 storage_limit_size: Set(None),
47 size: Set(0),
48 git_size: Set(0),
49 lfs_size: Set(0),
50 avatar_url: Set(None),
51 primary_language: Set(None),
52 license_name: Set(None),
53 created_at: Set(now),
54 last_pushed_at: Set(None),
55 archived_at: Set(None),
56 readme_raw: Set(None),
57 allow_rebase: Set(true),
58 allow_rebase_explicit: Set(false),
59 allow_squash_merge: Set(true),
60 allow_merge_commits: Set(true),
61 object_format: Set(svc.config.default_object_format.clone()),
62 };
63 let saved = active.insert(&db).await?;
64 Ok(RepoView::from(saved))
21 let _timer = metrics::Timer::start(metrics::FEATURE_REPOS_FORK);
22 let result: RepoResult<RepoView> = async {
23 require_authenticated(actor)?;
24 let _ = load_repo(svc, channel_id, repo_id).await?;
25 let new_name = match input.name {
26 Some(n) => validate_repo_name(&n, svc.config.max_repo_name_len)?,
27 None => format!("{}-fork", Uuid::new_v4()),
28 };
29 let db = svc.db.writer();
30 let now = OffsetDateTime::now_utc();
31 let new_id = Uuid::new_v4();
32 let active: repo_entity::ActiveModel = repo_entity::ActiveModel {
33 id: Set(new_id),
34 channel_id: Set(input.to_channel_id),
35 lower_name: Set(new_name.clone()),
36 name: Set(new_name),
37 description: Set(input.description),
38 website: Set(None),
39 default_branch: Set(crate::path::DEFAULT_OBJECT_FORMAT_SHA1.to_string()),
40 nums_watch: Set(0),
41 nums_star: Set(0),
42 nums_pr: Set(0),
43 nums_commits: Set(0),
44 nums_branches: Set(0),
45 nums_tags: Set(0),
46 enable_lfs: Set(false),
47 is_private: Set(false),
48 is_archived: Set(false),
49 storage_limit_size: Set(None),
50 size: Set(0),
51 git_size: Set(0),
52 lfs_size: Set(0),
53 avatar_url: Set(None),
54 primary_language: Set(None),
55 license_name: Set(None),
56 created_at: Set(now),
57 last_pushed_at: Set(None),
58 archived_at: Set(None),
59 readme_raw: Set(None),
60 allow_rebase: Set(true),
61 allow_rebase_explicit: Set(false),
62 allow_squash_merge: Set(true),
63 allow_merge_commits: Set(true),
64 object_format: Set(svc.config.default_object_format.clone()),
65 };
66 let saved = active.insert(&db).await?;
67 Ok(RepoView::from(saved))
68 }
69 .await;
70 metrics::record_outcome(metrics::FEATURE_REPOS_FORK, &result);
71 result
6572}
Modified lib/repo/features/lifecycle/usecase/generate.rs lib/repo/features/lifecycle/usecase/generate.rs +52 −45 View lib/repo/features/lifecycle/usecase/generate.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{RepoError, RepoResult};
77use crate::features::RepoService;
8use crate::metrics;
89use crate::path::validate_repo_name;
910
1011use super::super::{GenerateRepoOption, RepoView};
@@ -16,49 +17,55 @@
1617 repo_id: Uuid,
1718 input: GenerateRepoOption,
1819) -> RepoResult<RepoView> {
19 require_authenticated(actor)?;
20 let source = repo_entity::Entity::find_by_id(repo_id)
21 .one(&svc.db.reader())
22 .await?
23 .ok_or_else(|| RepoError::NotFound(format!("source repo {repo_id}")))?;
24 let new_name = validate_repo_name(&input.name, svc.config.max_repo_name_len)?;
25 let db = svc.db.writer();
26 let now = OffsetDateTime::now_utc();
27 let new_id = Uuid::new_v4();
28 let active: repo_entity::ActiveModel = repo_entity::ActiveModel {
29 id: Set(new_id),
30 channel_id: Set(input.to_channel_id),
31 lower_name: Set(new_name.clone()),
32 name: Set(new_name),
33 description: Set(input.description.or(source.description)),
34 website: Set(source.website),
35 default_branch: Set(source.default_branch.clone()),
36 nums_watch: Set(0),
37 nums_star: Set(0),
38 nums_pr: Set(0),
39 nums_commits: Set(0),
40 nums_branches: Set(0),
41 nums_tags: Set(0),
42 enable_lfs: Set(source.enable_lfs),
43 is_private: Set(input.is_private.unwrap_or(source.is_private)),
44 is_archived: Set(false),
45 storage_limit_size: Set(source.storage_limit_size),
46 size: Set(0),
47 git_size: Set(0),
48 lfs_size: Set(0),
49 avatar_url: Set(source.avatar_url),
50 primary_language: Set(source.primary_language),
51 license_name: Set(source.license_name),
52 created_at: Set(now),
53 last_pushed_at: Set(None),
54 archived_at: Set(None),
55 readme_raw: Set(source.readme_raw),
56 allow_rebase: Set(source.allow_rebase),
57 allow_rebase_explicit: Set(source.allow_rebase_explicit),
58 allow_squash_merge: Set(source.allow_squash_merge),
59 allow_merge_commits: Set(source.allow_merge_commits),
60 object_format: Set(source.object_format.clone()),
61 };
62 let saved = active.insert(&db).await?;
63 Ok(RepoView::from(saved))
20 let _timer = metrics::Timer::start(metrics::FEATURE_REPOS_GENERATE);
21 let result: RepoResult<RepoView> = async {
22 require_authenticated(actor)?;
23 let source = repo_entity::Entity::find_by_id(repo_id)
24 .one(&svc.db.reader())
25 .await?
26 .ok_or_else(|| RepoError::NotFound(format!("source repo {repo_id}")))?;
27 let new_name = validate_repo_name(&input.name, svc.config.max_repo_name_len)?;
28 let db = svc.db.writer();
29 let now = OffsetDateTime::now_utc();
30 let new_id = Uuid::new_v4();
31 let active: repo_entity::ActiveModel = repo_entity::ActiveModel {
32 id: Set(new_id),
33 channel_id: Set(input.to_channel_id),
34 lower_name: Set(new_name.clone()),
35 name: Set(new_name),
36 description: Set(input.description.or(source.description)),
37 website: Set(source.website),
38 default_branch: Set(source.default_branch.clone()),
39 nums_watch: Set(0),
40 nums_star: Set(0),
41 nums_pr: Set(0),
42 nums_commits: Set(0),
43 nums_branches: Set(0),
44 nums_tags: Set(0),
45 enable_lfs: Set(source.enable_lfs),
46 is_private: Set(input.is_private.unwrap_or(source.is_private)),
47 is_archived: Set(false),
48 storage_limit_size: Set(source.storage_limit_size),
49 size: Set(0),
50 git_size: Set(0),
51 lfs_size: Set(0),
52 avatar_url: Set(source.avatar_url),
53 primary_language: Set(source.primary_language),
54 license_name: Set(source.license_name),
55 created_at: Set(now),
56 last_pushed_at: Set(None),
57 archived_at: Set(None),
58 readme_raw: Set(source.readme_raw),
59 allow_rebase: Set(source.allow_rebase),
60 allow_rebase_explicit: Set(source.allow_rebase_explicit),
61 allow_squash_merge: Set(source.allow_squash_merge),
62 allow_merge_commits: Set(source.allow_merge_commits),
63 object_format: Set(source.object_format.clone()),
64 };
65 let saved = active.insert(&db).await?;
66 Ok(RepoView::from(saved))
67 }
68 .await;
69 metrics::record_outcome(metrics::FEATURE_REPOS_GENERATE, &result);
70 result
6471}
Modified lib/repo/features/lifecycle/usecase/get.rs lib/repo/features/lifecycle/usecase/get.rs +13 −6 View lib/repo/features/lifecycle/usecase/get.rs
@@ -2,6 +2,7 @@
22
33use crate::error::RepoResult;
44use crate::features::RepoService;
5use crate::metrics;
56
67use super::super::RepoView;
78use super::helpers::load_repo;
@@ -12,11 +13,17 @@
1213 channel_id: Uuid,
1314 repo_id: Uuid,
1415) -> RepoResult<RepoView> {
15 let repo = load_repo(svc, channel_id, repo_id).await?;
16 if repo.is_private && actor.is_nil() {
17 return Err(crate::error::RepoError::Unauthorized(
18 "authentication required".into(),
19 ));
16 let _timer = metrics::Timer::start(metrics::FEATURE_REPOS_GET);
17 let result: RepoResult<RepoView> = async {
18 let repo = load_repo(svc, channel_id, repo_id).await?;
19 if repo.is_private && actor.is_nil() {
20 return Err(crate::error::RepoError::Unauthorized(
21 "authentication required".into(),
22 ));
23 }
24 Ok(RepoView::from(repo))
2025 }
21 Ok(RepoView::from(repo))
26 .await;
27 metrics::record_outcome(metrics::FEATURE_REPOS_GET, &result);
28 result
2229}
Modified lib/repo/features/lifecycle/usecase/list.rs lib/repo/features/lifecycle/usecase/list.rs +39 −32 View lib/repo/features/lifecycle/usecase/list.rs
@@ -4,42 +4,49 @@
44
55use crate::error::RepoResult;
66use crate::features::RepoService;
7use crate::metrics;
78
89use super::super::{ListRepoQuery, RepoPage, RepoView};
910
1011pub async fn list_repos(svc: &RepoService, actor: Uuid, q: ListRepoQuery) -> RepoResult<RepoPage> {
11 let _ = actor;
12 let limit = q.limit.unwrap_or(svc.config.default_pagination);
13 let offset = q.offset.unwrap_or(0);
14 let limit = std::cmp::min(limit, svc.config.max_pagination);
15 let db = svc.db.reader();
16 let mut query = repo_entity::Entity::find();
17 if let Some(cid) = q.channel_id {
18 query = query.filter(repo_entity::Column::ChannelId.eq(cid));
19 }
20 if let Some(priv_) = q.is_private {
21 query = query.filter(repo_entity::Column::IsPrivate.eq(priv_));
22 }
23 if let Some(arch) = q.is_archived {
24 query = query.filter(repo_entity::Column::IsArchived.eq(arch));
25 }
26 if let Some(search) = q.q.as_deref() {
27 if !search.is_empty() {
28 query = query.filter(repo_entity::Column::LowerName.contains(search));
12 let _timer = metrics::Timer::start(metrics::FEATURE_REPOS_LIST);
13 let result: RepoResult<RepoPage> = async {
14 let _ = actor;
15 let limit = q.limit.unwrap_or(svc.config.default_pagination);
16 let offset = q.offset.unwrap_or(0);
17 let limit = std::cmp::min(limit, svc.config.max_pagination);
18 let db = svc.db.reader();
19 let mut query = repo_entity::Entity::find();
20 if let Some(cid) = q.channel_id {
21 query = query.filter(repo_entity::Column::ChannelId.eq(cid));
22 }
23 if let Some(priv_) = q.is_private {
24 query = query.filter(repo_entity::Column::IsPrivate.eq(priv_));
25 }
26 if let Some(arch) = q.is_archived {
27 query = query.filter(repo_entity::Column::IsArchived.eq(arch));
2928 }
29 if let Some(search) = q.q.as_deref() {
30 if !search.is_empty() {
31 query = query.filter(repo_entity::Column::LowerName.contains(search));
32 }
33 }
34 let total = query.clone().count(&db).await?;
35 let items = query
36 .order_by_desc(repo_entity::Column::CreatedAt)
37 .limit(Some(limit))
38 .offset(Some(offset))
39 .all(&db)
40 .await?
41 .into_iter()
42 .map(RepoView::from)
43 .collect();
44 Ok(RepoPage {
45 items,
46 total: total as i64,
47 })
3048 }
31 let total = query.clone().count(&db).await?;
32 let items = query
33 .order_by_desc(repo_entity::Column::CreatedAt)
34 .limit(Some(limit))
35 .offset(Some(offset))
36 .all(&db)
37 .await?
38 .into_iter()
39 .map(RepoView::from)
40 .collect();
41 Ok(RepoPage {
42 items,
43 total: total as i64,
44 })
49 .await;
50 metrics::record_outcome(metrics::FEATURE_REPOS_LIST, &result);
51 result
4552}
Modified lib/repo/features/lifecycle/usecase/rename.rs lib/repo/features/lifecycle/usecase/rename.rs +16 −9 View lib/repo/features/lifecycle/usecase/rename.rs
@@ -3,6 +3,7 @@
33
44use crate::error::RepoResult;
55use crate::features::RepoService;
6use crate::metrics;
67use crate::path::validate_repo_name;
78
89use super::super::{RenameRepoOption, RepoView};
@@ -15,13 +16,19 @@
1516 repo_id: Uuid,
1617 input: RenameRepoOption,
1718) -> RepoResult<RepoView> {
18 require_authenticated(actor)?;
19 let repo = load_repo(svc, channel_id, repo_id).await?;
20 let new_name = validate_repo_name(&input.new_name, svc.config.max_repo_name_len)?;
21 let db = svc.db.writer();
22 let mut active = db::repo::repos::ActiveModel::from(repo);
23 active.lower_name = Set(new_name.clone());
24 active.name = Set(new_name);
25 let saved = active.update(&db).await?;
26 Ok(RepoView::from(saved))
19 let _timer = metrics::Timer::start(metrics::FEATURE_REPOS_RENAME);
20 let result: RepoResult<RepoView> = async {
21 require_authenticated(actor)?;
22 let repo = load_repo(svc, channel_id, repo_id).await?;
23 let new_name = validate_repo_name(&input.new_name, svc.config.max_repo_name_len)?;
24 let db = svc.db.writer();
25 let mut active = db::repo::repos::ActiveModel::from(repo);
26 active.lower_name = Set(new_name.clone());
27 active.name = Set(new_name);
28 let saved = active.update(&db).await?;
29 Ok(RepoView::from(saved))
30 }
31 .await;
32 metrics::record_outcome(metrics::FEATURE_REPOS_RENAME, &result);
33 result
2734}
Modified lib/repo/features/lifecycle/usecase/transfer.rs lib/repo/features/lifecycle/usecase/transfer.rs +20 −13 View lib/repo/features/lifecycle/usecase/transfer.rs
@@ -3,6 +3,7 @@
33
44use crate::error::RepoResult;
55use crate::features::RepoService;
6use crate::metrics;
67use crate::path::validate_repo_name;
78
89use super::super::{RepoView, TransferRepoOption};
@@ -15,17 +16,23 @@
1516 repo_id: Uuid,
1617 input: TransferRepoOption,
1718) -> RepoResult<RepoView> {
18 require_authenticated(actor)?;
19 let repo = load_repo(svc, channel_id, repo_id).await?;
20 let new_name = match input.new_name {
21 Some(n) => validate_repo_name(&n, svc.config.max_repo_name_len)?,
22 None => repo.lower_name.clone(),
23 };
24 let db = svc.db.writer();
25 let mut active = db::repo::repos::ActiveModel::from(repo);
26 active.channel_id = Set(input.new_channel_id);
27 active.lower_name = Set(new_name.clone());
28 active.name = Set(new_name);
29 let saved = active.update(&db).await?;
30 Ok(RepoView::from(saved))
19 let _timer = metrics::Timer::start(metrics::FEATURE_REPOS_TRANSFER);
20 let result: RepoResult<RepoView> = async {
21 require_authenticated(actor)?;
22 let repo = load_repo(svc, channel_id, repo_id).await?;
23 let new_name = match input.new_name {
24 Some(n) => validate_repo_name(&n, svc.config.max_repo_name_len)?,
25 None => repo.lower_name.clone(),
26 };
27 let db = svc.db.writer();
28 let mut active = db::repo::repos::ActiveModel::from(repo);
29 active.channel_id = Set(input.new_channel_id);
30 active.lower_name = Set(new_name.clone());
31 active.name = Set(new_name);
32 let saved = active.update(&db).await?;
33 Ok(RepoView::from(saved))
34 }
35 .await;
36 metrics::record_outcome(metrics::FEATURE_REPOS_TRANSFER, &result);
37 result
3138}
Modified lib/repo/features/lifecycle/usecase/update.rs lib/repo/features/lifecycle/usecase/update.rs +42 −35 View lib/repo/features/lifecycle/usecase/update.rs
@@ -3,6 +3,7 @@
33
44use crate::error::RepoResult;
55use crate::features::RepoService;
6use crate::metrics;
67
78use super::super::{RepoView, UpdateRepoOption};
89use super::helpers::{load_repo, require_authenticated};
@@ -14,40 +15,46 @@
1415 repo_id: Uuid,
1516 input: UpdateRepoOption,
1617) -> RepoResult<RepoView> {
17 require_authenticated(actor)?;
18 let repo = load_repo(svc, channel_id, repo_id).await?;
19 let db = svc.db.writer();
20 let mut active = db::repo::repos::ActiveModel::from(repo);
21 if let Some(d) = input.description {
22 active.description = Set(d);
23 }
24 if let Some(w) = input.website {
25 active.website = Set(w);
26 }
27 if let Some(b) = input.default_branch {
28 active.default_branch = Set(b);
29 }
30 if let Some(p) = input.is_private {
31 active.is_private = Set(p);
32 }
33 if let Some(l) = input.enable_lfs {
34 active.enable_lfs = Set(l);
35 }
36 if let Some(l) = input.license_name {
37 active.license_name = Set(l);
38 }
39 if let Some(v) = input.allow_rebase {
40 active.allow_rebase = Set(v);
41 }
42 if let Some(v) = input.allow_rebase_explicit {
43 active.allow_rebase_explicit = Set(v);
44 }
45 if let Some(v) = input.allow_squash_merge {
46 active.allow_squash_merge = Set(v);
47 }
48 if let Some(v) = input.allow_merge_commits {
49 active.allow_merge_commits = Set(v);
18 let _timer = metrics::Timer::start(metrics::FEATURE_REPOS_UPDATE);
19 let result: RepoResult<RepoView> = async {
20 require_authenticated(actor)?;
21 let repo = load_repo(svc, channel_id, repo_id).await?;
22 let db = svc.db.writer();
23 let mut active = db::repo::repos::ActiveModel::from(repo);
24 if let Some(d) = input.description {
25 active.description = Set(d);
26 }
27 if let Some(w) = input.website {
28 active.website = Set(w);
29 }
30 if let Some(b) = input.default_branch {
31 active.default_branch = Set(b);
32 }
33 if let Some(p) = input.is_private {
34 active.is_private = Set(p);
35 }
36 if let Some(l) = input.enable_lfs {
37 active.enable_lfs = Set(l);
38 }
39 if let Some(l) = input.license_name {
40 active.license_name = Set(l);
41 }
42 if let Some(v) = input.allow_rebase {
43 active.allow_rebase = Set(v);
44 }
45 if let Some(v) = input.allow_rebase_explicit {
46 active.allow_rebase_explicit = Set(v);
47 }
48 if let Some(v) = input.allow_squash_merge {
49 active.allow_squash_merge = Set(v);
50 }
51 if let Some(v) = input.allow_merge_commits {
52 active.allow_merge_commits = Set(v);
53 }
54 let saved = active.update(&db).await?;
55 Ok(RepoView::from(saved))
5056 }
51 let saved = active.update(&db).await?;
52 Ok(RepoView::from(saved))
57 .await;
58 metrics::record_outcome(metrics::FEATURE_REPOS_UPDATE, &result);
59 result
5360}
Modified lib/repo/features/pull_request/usecase.rs lib/repo/features/pull_request/usecase.rs +595 −501 View lib/repo/features/pull_request/usecase.rs
@@ -21,6 +21,7 @@
2121
2222use crate::error::{RepoError, RepoResult};
2323use crate::features::RepoService;
24use crate::metrics;
2425use crate::path::{git_bare_path, validate_branch_name};
2526
2627use super::{
@@ -326,53 +327,59 @@
326327 repo_id: Uuid,
327328 query: ListPullRequestQuery,
328329) -> RepoResult<PullRequestPage> {
329 let _ = actor;
330 let limit = query.limit.unwrap_or(svc.config.default_pagination);
331 let offset = query.page.unwrap_or(0).saturating_mul(limit);
332 let limit = std::cmp::min(limit, svc.config.max_pagination);
330 let _timer = metrics::Timer::start(metrics::FEATURE_PULL_REQUESTS_LIST);
331 let result: RepoResult<PullRequestPage> = async {
332 let _ = actor;
333 let limit = query.limit.unwrap_or(svc.config.default_pagination);
334 let offset = query.page.unwrap_or(0).saturating_mul(limit);
335 let limit = std::cmp::min(limit, svc.config.max_pagination);
333336
334 let db = svc.db.reader();
335 let mut base = pr_entity::Entity::find().filter(pr_entity::Column::RepoId.eq(repo_id));
336 if let Some(state) = query.state.as_deref() {
337 match state {
338 "open" => {
339 base = base
340 .filter(pr_entity::Column::HasMerged.eq(false))
341 .filter(pr_entity::Column::ClosedAt.is_null());
337 let db = svc.db.reader();
338 let mut base = pr_entity::Entity::find().filter(pr_entity::Column::RepoId.eq(repo_id));
339 if let Some(state) = query.state.as_deref() {
340 match state {
341 "open" => {
342 base = base
343 .filter(pr_entity::Column::HasMerged.eq(false))
344 .filter(pr_entity::Column::ClosedAt.is_null());
345 }
346 "closed" => {
347 base = base
348 .filter(pr_entity::Column::HasMerged.eq(false))
349 .filter(pr_entity::Column::ClosedAt.is_not_null());
350 }
351 "merged" => {
352 base = base.filter(pr_entity::Column::HasMerged.eq(true));
353 }
354 _ => {}
342355 }
343 "closed" => {
344 base = base
345 .filter(pr_entity::Column::HasMerged.eq(false))
346 .filter(pr_entity::Column::ClosedAt.is_not_null());
347 }
348 "merged" => {
349 base = base.filter(pr_entity::Column::HasMerged.eq(true));
350 }
351 _ => {}
352356 }
353 }
354 if let Some(keyword) = query.keyword.as_deref()
355 && !keyword.is_empty()
356 {
357 base = base.filter(pr_entity::Column::Title.contains(keyword));
358 }
357 if let Some(keyword) = query.keyword.as_deref()
358 && !keyword.is_empty()
359 {
360 base = base.filter(pr_entity::Column::Title.contains(keyword));
361 }
359362
360 let total = base.clone().count(&db).await?;
361 let items: Vec<PullRequestView> = base
362 .clone()
363 .order_by_desc(pr_entity::Column::AutoincrementId)
364 .limit(Some(limit))
365 .offset(Some(offset))
366 .all(&db)
367 .await?
368 .into_iter()
369 .map(PullRequestView::from)
370 .collect();
363 let total = base.clone().count(&db).await?;
364 let items: Vec<PullRequestView> = base
365 .clone()
366 .order_by_desc(pr_entity::Column::AutoincrementId)
367 .limit(Some(limit))
368 .offset(Some(offset))
369 .all(&db)
370 .await?
371 .into_iter()
372 .map(PullRequestView::from)
373 .collect();
371374
372 Ok(PullRequestPage {
373 items,
374 total: total as i64,
375 })
375 Ok(PullRequestPage {
376 items,
377 total: total as i64,
378 })
379 }
380 .await;
381 metrics::record_outcome(metrics::FEATURE_PULL_REQUESTS_LIST, &result);
382 result
376383}
377384
378385pub async fn get_pull_request(
@@ -381,10 +388,16 @@
381388 repo_id: Uuid,
382389 number: i64,
383390) -> RepoResult<PullRequestView> {
384 let _ = actor;
385 let pr = load_pr(svc, repo_id, number).await?;
386 let pr = heal_merged(svc, repo_id, &pr).await;
387 Ok(PullRequestView::from(pr))
391 let _timer = metrics::Timer::start(metrics::FEATURE_PULL_REQUESTS_GET);
392 let result: RepoResult<PullRequestView> = async {
393 let _ = actor;
394 let pr = load_pr(svc, repo_id, number).await?;
395 let pr = heal_merged(svc, repo_id, &pr).await;
396 Ok(PullRequestView::from(pr))
397 }
398 .await;
399 metrics::record_outcome(metrics::FEATURE_PULL_REQUESTS_GET, &result);
400 result
388401}
389402
390403/// Heal the half-state where the base reference advanced to the merged commit
@@ -432,275 +445,305 @@
432445 repo_id: Uuid,
433446 input: CreatePullRequestOption,
434447) -> RepoResult<PullRequestView> {
435 require_authenticated(actor)?;
448 let _timer = metrics::Timer::start(metrics::FEATURE_PULL_REQUESTS_CREATE);
449 let result: RepoResult<PullRequestView> = async {
450 require_authenticated(actor)?;
436451
437 let title = input.title.trim().to_string();
438 if title.is_empty() || title.len() > svc.config.max_pr_title_len {
439 return Err(RepoError::BadRequest(format!(
440 "title length must be in 1..={}",
441 svc.config.max_pr_title_len
442 )));
443 }
444 let body = input.body.unwrap_or_default();
445 if body.len() > svc.config.max_pr_body_len {
446 return Err(RepoError::BadRequest(format!(
447 "body too long (max {})",
448 svc.config.max_pr_body_len
449 )));
450 }
451 let head = validate_branch_name(&input.head, svc.config.max_branch_name_len)?;
452 let base = validate_branch_name(&input.base, svc.config.max_branch_name_len)?;
453 if head == base {
454 return Err(RepoError::BadRequest(
455 "head and base branches must differ".into(),
456 ));
457 }
458
459 let repo = load_repo(svc, repo_id).await?;
460 let git = open_git(svc, &repo)?;
461 let head_sha = refs_resolve(&git, &format!("refs/heads/{head}"))?;
462 let base_sha = refs_resolve(&git, &format!("refs/heads/{base}"))?;
463
464 let db = svc.db.writer();
465 let txn = db.begin().await?;
466
467 let max_id: Option<i32> = pr_entity::Entity::find()
468 .filter(pr_entity::Column::RepoId.eq(repo_id))
469 .lock_exclusive()
470 .select_only()
471 .column_as(pr_entity::Column::AutoincrementId.max(), "max")
472 .into_tuple::<Option<i32>>()
473 .one(&txn)
474 .await?
475 .flatten();
476 let next_id = max_id.map(|n| n + 1).unwrap_or(1);
477
478 let now = OffsetDateTime::now_utc();
479 let issue_id = Uuid::new_v4();
480 let issue_active: issue_entity::ActiveModel = issue_entity::ActiveModel {
481 id: Set(issue_id),
482 collection_id: Set(repo_id),
483 autoincrement_id: Set(next_id),
484 author_id: Set(actor),
485 title: Set(title.clone()),
486 description: Set(None),
487 body: Set(body.clone()),
488 milestone_id: Set(None),
489 created_at: Set(now),
490 updated_at: Set(None),
491 closed_at: Set(None),
492 };
493 issue_active.insert(&txn).await?;
494
495 let pr_id = Uuid::new_v4();
496 let pr_active: pr_entity::ActiveModel = pr_entity::ActiveModel {
497 id: Set(pr_id),
498 repo_id: Set(repo_id),
499 issue_id: Set(issue_id),
500 autoincrement_id: Set(next_id),
501 author_id: Set(actor),
502 title: Set(title),
503 body: Set(body),
504 head_branch: Set(head),
505 head_sha: Set(head_sha.to_string()),
506 base_branch: Set(base),
507 base_sha: Set(base_sha.to_string()),
508 has_merged: Set(false),
509 merged_commit_sha: Set(None),
510 additions: Set(0),
511 deletions: Set(0),
512 files_changed: Set(0),
513 merger_id: Set(None),
514 merged_at: Set(None),
515 is_locked: Set(false),
516 created_at: Set(now),
517 updated_at: Set(None),
518 closed_at: Set(None),
519 };
520 let saved = pr_active.insert(&txn).await?;
521
522 insert_event(
523 &txn,
524 pr_id,
525 actor,
526 "opened",
527 Some(head_sha.to_string()),
528 None,
529 )
530 .await?;
531
532 let nums_pr = repo.nums_pr;
533 let mut repo_active: repo_entity::ActiveModel = repo.into();
534 repo_active.nums_pr = Set(nums_pr + 1);
535 repo_active.update(&txn).await?;
536
537 txn.commit().await?;
538
539 Ok(PullRequestView::from(saved))
540}
541
542pub async fn update_pull_request(
543 svc: &RepoService,
544 actor: Uuid,
545 repo_id: Uuid,
546 number: i64,
547 input: UpdatePullRequestOption,
548) -> RepoResult<PullRequestView> {
549 require_authenticated(actor)?;
550 let pr = load_pr(svc, repo_id, number).await?;
551 let mode = collaborator_mode(svc, repo_id, actor).await?;
552 if !can_manage_pr(&pr, actor, mode) {
553 return Err(RepoError::Forbidden(
554 "only the author or a collaborator can update this pull request".into(),
555 ));
556 }
557 if pr.has_merged || pr.closed_at.is_some() {
558 return Err(RepoError::Conflict(
559 "cannot update a closed or merged pull request".into(),
560 ));
561 }
562
563 let db = svc.db.writer();
564 let mut active: pr_entity::ActiveModel = pr.into();
565 if let Some(title) = input.title {
566 let title = title.trim().to_string();
452 let title = input.title.trim().to_string();
567453 if title.is_empty() || title.len() > svc.config.max_pr_title_len {
568454 return Err(RepoError::BadRequest(format!(
569455 "title length must be in 1..={}",
570456 svc.config.max_pr_title_len
571457 )));
572458 }
573 active.title = Set(title);
574 }
575 if let Some(body) = input.body {
459 let body = input.body.unwrap_or_default();
576460 if body.len() > svc.config.max_pr_body_len {
577461 return Err(RepoError::BadRequest(format!(
578462 "body too long (max {})",
579463 svc.config.max_pr_body_len
580464 )));
581465 }
582 active.body = Set(body);
583 }
584 if let Some(base) = input.base {
585 let base = validate_branch_name(&base, svc.config.max_branch_name_len)?;
586 active.base_branch = Set(base);
587 }
588 if let Some(head) = input.head {
589 let head = validate_branch_name(&head, svc.config.max_branch_name_len)?;
590 active.head_branch = Set(head);
591 }
592 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
593 let saved = active.update(&db).await?;
594 Ok(PullRequestView::from(saved))
466 let head = validate_branch_name(&input.head, svc.config.max_branch_name_len)?;
467 let base = validate_branch_name(&input.base, svc.config.max_branch_name_len)?;
468 if head == base {
469 return Err(RepoError::BadRequest(
470 "head and base branches must differ".into(),
471 ));
472 }
473
474 let repo = load_repo(svc, repo_id).await?;
475 let git = open_git(svc, &repo)?;
476 let head_sha = refs_resolve(&git, &format!("refs/heads/{head}"))?;
477 let base_sha = refs_resolve(&git, &format!("refs/heads/{base}"))?;
478
479 let db = svc.db.writer();
480 let txn = db.begin().await?;
481
482 let max_id: Option<i32> = pr_entity::Entity::find()
483 .filter(pr_entity::Column::RepoId.eq(repo_id))
484 .lock_exclusive()
485 .select_only()
486 .column_as(pr_entity::Column::AutoincrementId.max(), "max")
487 .into_tuple::<Option<i32>>()
488 .one(&txn)
489 .await?
490 .flatten();
491 let next_id = max_id.map(|n| n + 1).unwrap_or(1);
492
493 let now = OffsetDateTime::now_utc();
494 let issue_id = Uuid::new_v4();
495 let issue_active: issue_entity::ActiveModel = issue_entity::ActiveModel {
496 id: Set(issue_id),
497 collection_id: Set(repo_id),
498 autoincrement_id: Set(next_id),
499 author_id: Set(actor),
500 title: Set(title.clone()),
501 description: Set(None),
502 body: Set(body.clone()),
503 milestone_id: Set(None),
504 created_at: Set(now),
505 updated_at: Set(None),
506 closed_at: Set(None),
507 };
508 issue_active.insert(&txn).await?;
509
510 let pr_id = Uuid::new_v4();
511 let pr_active: pr_entity::ActiveModel = pr_entity::ActiveModel {
512 id: Set(pr_id),
513 repo_id: Set(repo_id),
514 issue_id: Set(issue_id),
515 autoincrement_id: Set(next_id),
516 author_id: Set(actor),
517 title: Set(title),
518 body: Set(body),
519 head_branch: Set(head),
520 head_sha: Set(head_sha.to_string()),
521 base_branch: Set(base),
522 base_sha: Set(base_sha.to_string()),
523 has_merged: Set(false),
524 merged_commit_sha: Set(None),
525 additions: Set(0),
526 deletions: Set(0),
527 files_changed: Set(0),
528 merger_id: Set(None),
529 merged_at: Set(None),
530 is_locked: Set(false),
531 created_at: Set(now),
532 updated_at: Set(None),
533 closed_at: Set(None),
534 };
535 let saved = pr_active.insert(&txn).await?;
536
537 insert_event(
538 &txn,
539 pr_id,
540 actor,
541 "opened",
542 Some(head_sha.to_string()),
543 None,
544 )
545 .await?;
546
547 let nums_pr = repo.nums_pr;
548 let mut repo_active: repo_entity::ActiveModel = repo.into();
549 repo_active.nums_pr = Set(nums_pr + 1);
550 repo_active.update(&txn).await?;
551
552 txn.commit().await?;
553
554 Ok(PullRequestView::from(saved))
555 }
556 .await;
557 metrics::record_outcome(metrics::FEATURE_PULL_REQUESTS_CREATE, &result);
558 result
595559}
596560
597pub async fn close_pull_request(
561pub async fn update_pull_request(
598562 svc: &RepoService,
599563 actor: Uuid,
600564 repo_id: Uuid,
601565 number: i64,
566 input: UpdatePullRequestOption,
602567) -> RepoResult<PullRequestView> {
603 require_authenticated(actor)?;
604 let pr = load_pr(svc, repo_id, number).await?;
605 let mode = collaborator_mode(svc, repo_id, actor).await?;
606 if !can_manage_pr(&pr, actor, mode) {
607 return Err(RepoError::Forbidden(
608 "only the author or a collaborator can close this pull request".into(),
609 ));
610 }
611 if pr.has_merged || pr.closed_at.is_some() {
612 return Err(RepoError::Conflict(
613 "pull request already closed or merged".into(),
614 ));
615 }
568 let _timer = metrics::Timer::start(metrics::FEATURE_PULL_REQUESTS_UPDATE);
569 let result: RepoResult<PullRequestView> = async {
570 require_authenticated(actor)?;
571 let pr = load_pr(svc, repo_id, number).await?;
572 let mode = collaborator_mode(svc, repo_id, actor).await?;
573 if !can_manage_pr(&pr, actor, mode) {
574 return Err(RepoError::Forbidden(
575 "only the author or a collaborator can update this pull request".into(),
576 ));
577 }
578 if pr.has_merged || pr.closed_at.is_some() {
579 return Err(RepoError::Conflict(
580 "cannot update a closed or merged pull request".into(),
581 ));
582 }
616583
617 let db = svc.db.writer();
618 let txn = db.begin().await?;
619 let now = OffsetDateTime::now_utc();
620
621 let mut active: pr_entity::ActiveModel = pr.clone().into();
622 active.closed_at = Set(Some(now));
623 active.updated_at = Set(Some(now));
624 let saved = active.update(&txn).await?;
625
626 if let Some(issue) = issue_entity::Entity::find_by_id(pr.issue_id)
627 .one(&txn)
628 .await?
629 {
630 let mut ia: issue_entity::ActiveModel = issue.into();
631 ia.closed_at = Set(Some(now));
632 ia.updated_at = Set(Some(now));
633 ia.update(&txn).await?;
584 let db = svc.db.writer();
585 let mut active: pr_entity::ActiveModel = pr.into();
586 if let Some(title) = input.title {
587 let title = title.trim().to_string();
588 if title.is_empty() || title.len() > svc.config.max_pr_title_len {
589 return Err(RepoError::BadRequest(format!(
590 "title length must be in 1..={}",
591 svc.config.max_pr_title_len
592 )));
593 }
594 active.title = Set(title);
595 }
596 if let Some(body) = input.body {
597 if body.len() > svc.config.max_pr_body_len {
598 return Err(RepoError::BadRequest(format!(
599 "body too long (max {})",
600 svc.config.max_pr_body_len
601 )));
602 }
603 active.body = Set(body);
604 }
605 if let Some(base) = input.base {
606 let base = validate_branch_name(&base, svc.config.max_branch_name_len)?;
607 active.base_branch = Set(base);
608 }
609 if let Some(head) = input.head {
610 let head = validate_branch_name(&head, svc.config.max_branch_name_len)?;
611 active.head_branch = Set(head);
612 }
613 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
614 let saved = active.update(&db).await?;
615 Ok(PullRequestView::from(saved))
634616 }
635
636 insert_event(&txn, pr.id, actor, "closed", None, None).await?;
637 txn.commit().await?;
638 Ok(PullRequestView::from(saved))
617 .await;
618 metrics::record_outcome(metrics::FEATURE_PULL_REQUESTS_UPDATE, &result);
619 result
639620}
640621
641pub async fn reopen_pull_request(
622pub async fn close_pull_request(
642623 svc: &RepoService,
643624 actor: Uuid,
644625 repo_id: Uuid,
645626 number: i64,
646627) -> RepoResult<PullRequestView> {
647 require_authenticated(actor)?;
648 let pr = load_pr(svc, repo_id, number).await?;
649 let mode = collaborator_mode(svc, repo_id, actor).await?;
650 if !can_manage_pr(&pr, actor, mode) {
651 return Err(RepoError::Forbidden(
652 "only the author or a collaborator can reopen this pull request".into(),
653 ));
654 }
655 if pr.has_merged || pr.closed_at.is_none() {
656 return Err(RepoError::Conflict("pull request is not closed".into()));
657 }
628 let _timer = metrics::Timer::start(metrics::FEATURE_PULL_REQUESTS_CLOSE);
629 let result: RepoResult<PullRequestView> = async {
630 require_authenticated(actor)?;
631 let pr = load_pr(svc, repo_id, number).await?;
632 let mode = collaborator_mode(svc, repo_id, actor).await?;
633 if !can_manage_pr(&pr, actor, mode) {
634 return Err(RepoError::Forbidden(
635 "only the author or a collaborator can close this pull request".into(),
636 ));
637 }
638 if pr.has_merged || pr.closed_at.is_some() {
639 return Err(RepoError::Conflict(
640 "pull request already closed or merged".into(),
641 ));
642 }
643
644 let db = svc.db.writer();
645 let txn = db.begin().await?;
646 let now = OffsetDateTime::now_utc();
658647
659 let db = svc.db.writer();
660 let txn = db.begin().await?;
661 let now = OffsetDateTime::now_utc();
648 let mut active: pr_entity::ActiveModel = pr.clone().into();
649 active.closed_at = Set(Some(now));
650 active.updated_at = Set(Some(now));
651 let saved = active.update(&txn).await?;
662652
663 let mut active: pr_entity::ActiveModel = pr.clone().into();
664 active.closed_at = Set(None);
665 active.updated_at = Set(Some(now));
666 let saved = active.update(&txn).await?;
653 if let Some(issue) = issue_entity::Entity::find_by_id(pr.issue_id)
654 .one(&txn)
655 .await?
656 {
657 let mut ia: issue_entity::ActiveModel = issue.into();
658 ia.closed_at = Set(Some(now));
659 ia.updated_at = Set(Some(now));
660 ia.update(&txn).await?;
661 }
667662
668 if let Some(issue) = issue_entity::Entity::find_by_id(pr.issue_id)
669 .one(&txn)
670 .await?
671 {
672 let mut ia: issue_entity::ActiveModel = issue.into();
673 ia.closed_at = Set(None);
674 ia.updated_at = Set(Some(now));
675 ia.update(&txn).await?;
663 insert_event(&txn, pr.id, actor, "closed", None, None).await?;
664 txn.commit().await?;
665 Ok(PullRequestView::from(saved))
676666 }
677
678 insert_event(&txn, pr.id, actor, "reopened", None, None).await?;
679 txn.commit().await?;
680 Ok(PullRequestView::from(saved))
667 .await;
668 metrics::record_outcome(metrics::FEATURE_PULL_REQUESTS_CLOSE, &result);
669 result
681670}
682671
683pub async fn lock_pull_request(
672pub async fn reopen_pull_request(
684673 svc: &RepoService,
685674 actor: Uuid,
686675 repo_id: Uuid,
687676 number: i64,
688677) -> RepoResult<PullRequestView> {
689 require_authenticated(actor)?;
690 let pr = load_pr(svc, repo_id, number).await?;
691 let mode = collaborator_mode(svc, repo_id, actor).await?;
692 if !can_manage_pr(&pr, actor, mode) {
693 return Err(RepoError::Forbidden(
694 "only the author or a collaborator can lock this pull request".into(),
695 ));
678 let _timer = metrics::Timer::start(metrics::FEATURE_PULL_REQUESTS_REOPEN);
679 let result: RepoResult<PullRequestView> = async {
680 require_authenticated(actor)?;
681 let pr = load_pr(svc, repo_id, number).await?;
682 let mode = collaborator_mode(svc, repo_id, actor).await?;
683 if !can_manage_pr(&pr, actor, mode) {
684 return Err(RepoError::Forbidden(
685 "only the author or a collaborator can reopen this pull request".into(),
686 ));
687 }
688 if pr.has_merged || pr.closed_at.is_none() {
689 return Err(RepoError::Conflict("pull request is not closed".into()));
690 }
691
692 let db = svc.db.writer();
693 let txn = db.begin().await?;
694 let now = OffsetDateTime::now_utc();
695
696 let mut active: pr_entity::ActiveModel = pr.clone().into();
697 active.closed_at = Set(None);
698 active.updated_at = Set(Some(now));
699 let saved = active.update(&txn).await?;
700
701 if let Some(issue) = issue_entity::Entity::find_by_id(pr.issue_id)
702 .one(&txn)
703 .await?
704 {
705 let mut ia: issue_entity::ActiveModel = issue.into();
706 ia.closed_at = Set(None);
707 ia.updated_at = Set(Some(now));
708 ia.update(&txn).await?;
709 }
710
711 insert_event(&txn, pr.id, actor, "reopened", None, None).await?;
712 txn.commit().await?;
713 Ok(PullRequestView::from(saved))
696714 }
715 .await;
716 metrics::record_outcome(metrics::FEATURE_PULL_REQUESTS_REOPEN, &result);
717 result
718}
719
720pub async fn lock_pull_request(
721 svc: &RepoService,
722 actor: Uuid,
723 repo_id: Uuid,
724 number: i64,
725) -> RepoResult<PullRequestView> {
726 let _timer = metrics::Timer::start(metrics::FEATURE_PULL_REQUESTS_LOCK);
727 let result: RepoResult<PullRequestView> = async {
728 require_authenticated(actor)?;
729 let pr = load_pr(svc, repo_id, number).await?;
730 let mode = collaborator_mode(svc, repo_id, actor).await?;
731 if !can_manage_pr(&pr, actor, mode) {
732 return Err(RepoError::Forbidden(
733 "only the author or a collaborator can lock this pull request".into(),
734 ));
735 }
697736
698 let db = svc.db.writer();
699 let mut active: pr_entity::ActiveModel = pr.into();
700 active.is_locked = Set(true);
701 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
702 let saved = active.update(&db).await?;
703 Ok(PullRequestView::from(saved))
737 let db = svc.db.writer();
738 let mut active: pr_entity::ActiveModel = pr.into();
739 active.is_locked = Set(true);
740 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
741 let saved = active.update(&db).await?;
742 Ok(PullRequestView::from(saved))
743 }
744 .await;
745 metrics::record_outcome(metrics::FEATURE_PULL_REQUESTS_LOCK, &result);
746 result
704747}
705748
706749pub async fn unlock_pull_request(
@@ -709,21 +752,27 @@
709752 repo_id: Uuid,
710753 number: i64,
711754) -> RepoResult<PullRequestView> {
712 require_authenticated(actor)?;
713 let pr = load_pr(svc, repo_id, number).await?;
714 let mode = collaborator_mode(svc, repo_id, actor).await?;
715 if !can_manage_pr(&pr, actor, mode) {
716 return Err(RepoError::Forbidden(
717 "only the author or a collaborator can unlock this pull request".into(),
718 ));
755 let _timer = metrics::Timer::start(metrics::FEATURE_PULL_REQUESTS_UNLOCK);
756 let result: RepoResult<PullRequestView> = async {
757 require_authenticated(actor)?;
758 let pr = load_pr(svc, repo_id, number).await?;
759 let mode = collaborator_mode(svc, repo_id, actor).await?;
760 if !can_manage_pr(&pr, actor, mode) {
761 return Err(RepoError::Forbidden(
762 "only the author or a collaborator can unlock this pull request".into(),
763 ));
764 }
765
766 let db = svc.db.writer();
767 let mut active: pr_entity::ActiveModel = pr.into();
768 active.is_locked = Set(false);
769 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
770 let saved = active.update(&db).await?;
771 Ok(PullRequestView::from(saved))
719772 }
720
721 let db = svc.db.writer();
722 let mut active: pr_entity::ActiveModel = pr.into();
723 active.is_locked = Set(false);
724 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
725 let saved = active.update(&db).await?;
726 Ok(PullRequestView::from(saved))
773 .await;
774 metrics::record_outcome(metrics::FEATURE_PULL_REQUESTS_UNLOCK, &result);
775 result
727776}
728777
729778pub async fn create_pull_request_comment(
@@ -733,49 +782,55 @@
733782 number: i64,
734783 input: CreatePullRequestCommentOption,
735784) -> RepoResult<PullRequestCommentView> {
736 require_authenticated(actor)?;
737 let pr = load_pr(svc, repo_id, number).await?;
738 let body = input.body.trim().to_string();
739 if body.is_empty() {
740 return Err(RepoError::BadRequest("comment body is empty".into()));
741 }
742 if body.len() > svc.config.max_pr_body_len {
743 return Err(RepoError::BadRequest(format!(
744 "comment too long (max {})",
745 svc.config.max_pr_body_len
746 )));
747 }
785 let _timer = metrics::Timer::start(metrics::FEATURE_PULL_REQUESTS_CREATE_COMMENT);
786 let result: RepoResult<PullRequestCommentView> = async {
787 require_authenticated(actor)?;
788 let pr = load_pr(svc, repo_id, number).await?;
789 let body = input.body.trim().to_string();
790 if body.is_empty() {
791 return Err(RepoError::BadRequest("comment body is empty".into()));
792 }
793 if body.len() > svc.config.max_pr_body_len {
794 return Err(RepoError::BadRequest(format!(
795 "comment too long (max {})",
796 svc.config.max_pr_body_len
797 )));
798 }
748799
749 let db = svc.db.writer();
750 let txn = db.begin().await?;
751 let now = OffsetDateTime::now_utc();
800 let db = svc.db.writer();
801 let txn = db.begin().await?;
802 let now = OffsetDateTime::now_utc();
752803
753 let active: comment_entity::ActiveModel = comment_entity::ActiveModel {
754 id: Set(Uuid::new_v4()),
755 pull_request_id: Set(pr.id),
756 user_id: Set(actor),
757 review_id: Set(None),
758 commit_sha: Set(input.commit_sha.unwrap_or_default()),
759 path: Set(input.path.unwrap_or_default()),
760 line: Set(input.line),
761 body: Set(body.clone()),
762 created_at: Set(now),
763 updated_at: Set(None),
764 };
765 let saved = active.insert(&txn).await?;
804 let active: comment_entity::ActiveModel = comment_entity::ActiveModel {
805 id: Set(Uuid::new_v4()),
806 pull_request_id: Set(pr.id),
807 user_id: Set(actor),
808 review_id: Set(None),
809 commit_sha: Set(input.commit_sha.unwrap_or_default()),
810 path: Set(input.path.unwrap_or_default()),
811 line: Set(input.line),
812 body: Set(body.clone()),
813 created_at: Set(now),
814 updated_at: Set(None),
815 };
816 let saved = active.insert(&txn).await?;
766817
767 insert_event(
768 &txn,
769 pr.id,
770 actor,
771 "comment",
772 (!saved.commit_sha.is_empty()).then_some(saved.commit_sha.clone()),
773 Some(body),
774 )
775 .await?;
818 insert_event(
819 &txn,
820 pr.id,
821 actor,
822 "comment",
823 (!saved.commit_sha.is_empty()).then_some(saved.commit_sha.clone()),
824 Some(body),
825 )
826 .await?;
776827
777 txn.commit().await?;
778 Ok(PullRequestCommentView::from(saved))
828 txn.commit().await?;
829 Ok(PullRequestCommentView::from(saved))
830 }
831 .await;
832 metrics::record_outcome(metrics::FEATURE_PULL_REQUESTS_CREATE_COMMENT, &result);
833 result
779834}
780835
781836pub async fn list_pull_request_comments(
@@ -784,18 +839,24 @@
784839 repo_id: Uuid,
785840 number: i64,
786841) -> RepoResult<Vec<PullRequestCommentView>> {
787 let _ = actor;
788 let pr = load_pr(svc, repo_id, number).await?;
789 let db = svc.db.reader();
790 let items: Vec<PullRequestCommentView> = comment_entity::Entity::find()
791 .filter(comment_entity::Column::PullRequestId.eq(pr.id))
792 .order_by_asc(comment_entity::Column::CreatedAt)
793 .all(&db)
794 .await?
795 .into_iter()
796 .map(PullRequestCommentView::from)
797 .collect();
798 Ok(items)
842 let _timer = metrics::Timer::start(metrics::FEATURE_PULL_REQUESTS_LIST_COMMENTS);
843 let result: RepoResult<Vec<PullRequestCommentView>> = async {
844 let _ = actor;
845 let pr = load_pr(svc, repo_id, number).await?;
846 let db = svc.db.reader();
847 let items: Vec<PullRequestCommentView> = comment_entity::Entity::find()
848 .filter(comment_entity::Column::PullRequestId.eq(pr.id))
849 .order_by_asc(comment_entity::Column::CreatedAt)
850 .all(&db)
851 .await?
852 .into_iter()
853 .map(PullRequestCommentView::from)
854 .collect();
855 Ok(items)
856 }
857 .await;
858 metrics::record_outcome(metrics::FEATURE_PULL_REQUESTS_LIST_COMMENTS, &result);
859 result
799860}
800861
801862pub async fn submit_pull_request_review(
@@ -805,38 +866,44 @@
805866 number: i64,
806867 input: SubmitPullRequestReviewOption,
807868) -> RepoResult<PullRequestReviewView> {
808 require_authenticated(actor)?;
809 let pr = load_pr(svc, repo_id, number).await?;
869 let _timer = metrics::Timer::start(metrics::FEATURE_PULL_REQUESTS_SUBMIT_REVIEW);
870 let result: RepoResult<PullRequestReviewView> = async {
871 require_authenticated(actor)?;
872 let pr = load_pr(svc, repo_id, number).await?;
810873
811 let db = svc.db.writer();
812 let txn = db.begin().await?;
813 let now = OffsetDateTime::now_utc();
814
815 let active: review_entity::ActiveModel = review_entity::ActiveModel {
816 id: Set(Uuid::new_v4()),
817 pull_request_id: Set(pr.id),
818 user_id: Set(actor),
819 commit_sha: Set(input.commit_sha.unwrap_or(pr.head_sha)),
820 official: Set(true),
821 content: Set(input.content.clone()),
822 state: Set(review_state_to_db(input.state)),
823 created_at: Set(now),
824 updated_at: Set(None),
825 };
826 let saved = active.insert(&txn).await?;
874 let db = svc.db.writer();
875 let txn = db.begin().await?;
876 let now = OffsetDateTime::now_utc();
827877
828 insert_event(
829 &txn,
830 pr.id,
831 actor,
832 "review",
833 (!saved.commit_sha.is_empty()).then_some(saved.commit_sha.clone()),
834 input.content,
835 )
836 .await?;
878 let active: review_entity::ActiveModel = review_entity::ActiveModel {
879 id: Set(Uuid::new_v4()),
880 pull_request_id: Set(pr.id),
881 user_id: Set(actor),
882 commit_sha: Set(input.commit_sha.unwrap_or(pr.head_sha)),
883 official: Set(true),
884 content: Set(input.content.clone()),
885 state: Set(review_state_to_db(input.state)),
886 created_at: Set(now),
887 updated_at: Set(None),
888 };
889 let saved = active.insert(&txn).await?;
837890
838 txn.commit().await?;
839 Ok(PullRequestReviewView::from(saved))
891 insert_event(
892 &txn,
893 pr.id,
894 actor,
895 "review",
896 (!saved.commit_sha.is_empty()).then_some(saved.commit_sha.clone()),
897 input.content,
898 )
899 .await?;
900
901 txn.commit().await?;
902 Ok(PullRequestReviewView::from(saved))
903 }
904 .await;
905 metrics::record_outcome(metrics::FEATURE_PULL_REQUESTS_SUBMIT_REVIEW, &result);
906 result
840907}
841908
842909pub async fn list_pull_request_reviews(
@@ -845,18 +912,24 @@
845912 repo_id: Uuid,
846913 number: i64,
847914) -> RepoResult<Vec<PullRequestReviewView>> {
848 let _ = actor;
849 let pr = load_pr(svc, repo_id, number).await?;
850 let db = svc.db.reader();
851 let items: Vec<PullRequestReviewView> = review_entity::Entity::find()
852 .filter(review_entity::Column::PullRequestId.eq(pr.id))
853 .order_by_asc(review_entity::Column::CreatedAt)
854 .all(&db)
855 .await?
856 .into_iter()
857 .map(PullRequestReviewView::from)
858 .collect();
859 Ok(items)
915 let _timer = metrics::Timer::start(metrics::FEATURE_PULL_REQUESTS_LIST_REVIEWS);
916 let result: RepoResult<Vec<PullRequestReviewView>> = async {
917 let _ = actor;
918 let pr = load_pr(svc, repo_id, number).await?;
919 let db = svc.db.reader();
920 let items: Vec<PullRequestReviewView> = review_entity::Entity::find()
921 .filter(review_entity::Column::PullRequestId.eq(pr.id))
922 .order_by_asc(review_entity::Column::CreatedAt)
923 .all(&db)
924 .await?
925 .into_iter()
926 .map(PullRequestReviewView::from)
927 .collect();
928 Ok(items)
929 }
930 .await;
931 metrics::record_outcome(metrics::FEATURE_PULL_REQUESTS_LIST_REVIEWS, &result);
932 result
860933}
861934
862935pub async fn list_pull_request_events(
@@ -865,18 +938,24 @@
865938 repo_id: Uuid,
866939 number: i64,
867940) -> RepoResult<Vec<PullRequestEventView>> {
868 let _ = actor;
869 let pr = load_pr(svc, repo_id, number).await?;
870 let db = svc.db.reader();
871 let items: Vec<PullRequestEventView> = event_entity::Entity::find()
872 .filter(event_entity::Column::PullRequestId.eq(pr.id))
873 .order_by_asc(event_entity::Column::CreatedAt)
874 .all(&db)
875 .await?
876 .into_iter()
877 .map(PullRequestEventView::from)
878 .collect();
879 Ok(items)
941 let _timer = metrics::Timer::start(metrics::FEATURE_PULL_REQUESTS_LIST_EVENTS);
942 let result: RepoResult<Vec<PullRequestEventView>> = async {
943 let _ = actor;
944 let pr = load_pr(svc, repo_id, number).await?;
945 let db = svc.db.reader();
946 let items: Vec<PullRequestEventView> = event_entity::Entity::find()
947 .filter(event_entity::Column::PullRequestId.eq(pr.id))
948 .order_by_asc(event_entity::Column::CreatedAt)
949 .all(&db)
950 .await?
951 .into_iter()
952 .map(PullRequestEventView::from)
953 .collect();
954 Ok(items)
955 }
956 .await;
957 metrics::record_outcome(metrics::FEATURE_PULL_REQUESTS_LIST_EVENTS, &result);
958 result
880959}
881960
882961pub async fn merge_pull_request(
@@ -886,131 +965,146 @@
886965 number: i64,
887966 opt: MergePullRequestOption,
888967) -> RepoResult<MergeView> {
889 require_authenticated(actor)?;
890 let pr = load_pr(svc, repo_id, number).await?;
891 let mode = collaborator_mode(svc, repo_id, actor).await?;
892 if !can_manage_pr(&pr, actor, mode) {
893 return Err(RepoError::Forbidden(
894 "only the author or a collaborator can merge this pull request".into(),
895 ));
896 }
897 if pr.has_merged {
898 return Err(RepoError::Conflict("pull request already merged".into()));
899 }
900 let repo = load_repo(svc, repo_id).await?;
901 let git = open_git(svc, &repo)?;
968 let _timer = metrics::Timer::start(metrics::FEATURE_PULL_REQUESTS_MERGE);
969 let result: RepoResult<MergeView> = async {
970 require_authenticated(actor)?;
971 let pr = load_pr(svc, repo_id, number).await?;
972 let mode = collaborator_mode(svc, repo_id, actor).await?;
973 if !can_manage_pr(&pr, actor, mode) {
974 return Err(RepoError::Forbidden(
975 "only the author or a collaborator can merge this pull request".into(),
976 ));
977 }
978 if pr.has_merged {
979 return Err(RepoError::Conflict("pull request already merged".into()));
980 }
981 let repo = load_repo(svc, repo_id).await?;
982 let git = open_git(svc, &repo)?;
902983
903 let base_ref = format!("refs/heads/{}", pr.base_branch);
904 let head_ref = format!("refs/heads/{}", pr.head_branch);
905 let live_base = refs_resolve(&git, &base_ref)?;
906 let head = refs_resolve(&git, &head_ref)?;
984 let base_ref = format!("refs/heads/{}", pr.base_branch);
985 let head_ref = format!("refs/heads/{}", pr.head_branch);
986 let live_base = refs_resolve(&git, &base_ref)?;
987 let head = refs_resolve(&git, &head_ref)?;
907988
908 let status = threeway_merge_status(&git, live_base, head, parse_oid(&pr.base_sha)?)?;
909 if !status.mergeable && opt.style != MergeStyle::Merge && opt.style != MergeStyle::FastForward {
910 let detail = serde_json::to_string(&status.conflicts).unwrap_or_default();
911 return Err(RepoError::Conflict(format!(
912 "pull request has conflicts; cannot merge: {detail}"
913 )));
914 }
989 let status = threeway_merge_status(&git, live_base, head, parse_oid(&pr.base_sha)?)?;
990 if !status.mergeable && opt.style != MergeStyle::Merge && opt.style != MergeStyle::FastForward
991 {
992 let detail = serde_json::to_string(&status.conflicts).unwrap_or_default();
993 return Err(RepoError::Conflict(format!(
994 "pull request has conflicts; cannot merge: {detail}"
995 )));
996 }
915997
916 if opt.style == MergeStyle::Squash && !repo.allow_squash_merge {
917 return Err(RepoError::Unprocessable(
918 "squash merge is disabled for this repository".into(),
919 ));
920 }
921 if opt.style == MergeStyle::Merge && !repo.allow_merge_commits {
922 return Err(RepoError::Unprocessable(
923 "merge commits are disabled for this repository".into(),
924 ));
925 }
998 if opt.style == MergeStyle::Squash && !repo.allow_squash_merge {
999 return Err(RepoError::Unprocessable(
1000 "squash merge is disabled for this repository".into(),
1001 ));
1002 }
1003 if opt.style == MergeStyle::Merge && !repo.allow_merge_commits {
1004 return Err(RepoError::Unprocessable(
1005 "merge commits are disabled for this repository".into(),
1006 ));
1007 }
9261008
927 let merger = load_user(svc, actor).await?;
928 let signature = merger_signature(merger.as_ref());
1009 let merger = load_user(svc, actor).await?;
1010 let signature = merger_signature(merger.as_ref());
9291011
930 let new_sha = match opt.style {
931 MergeStyle::FastForward => {
932 if !status.fast_forwardable {
933 return Err(RepoError::Conflict(
934 "pull request cannot be fast-forwarded; base branch has diverged".into(),
935 ));
1012 let new_sha = match opt.style {
1013 MergeStyle::FastForward => {
1014 if !status.fast_forwardable {
1015 return Err(RepoError::Conflict(
1016 "pull request cannot be fast-forwarded; base branch has diverged".into(),
1017 ));
1018 }
1019 head
1020 }
1021 MergeStyle::Squash => {
1022 let tree = resolve_tree_id(&git, head)?;
1023 let message = merge_message(&pr, &opt, true);
1024 write_commit(&git, vec![live_base], tree, &message, &signature)?
1025 }
1026 MergeStyle::Merge => {
1027 let tree = resolve_tree_id(&git, head)?;
1028 let message = merge_message(&pr, &opt, false);
1029 write_commit(&git, vec![live_base, head], tree, &message, &signature)?
9361030 }
937 head
938 }
939 MergeStyle::Squash => {
940 let tree = resolve_tree_id(&git, head)?;
941 let message = merge_message(&pr, &opt, true);
942 write_commit(&git, vec![live_base], tree, &message, &signature)?
943 }
944 MergeStyle::Merge => {
945 let tree = resolve_tree_id(&git, head)?;
946 let message = merge_message(&pr, &opt, false);
947 write_commit(&git, vec![live_base, head], tree, &message, &signature)?
948 }
949 MergeStyle::Rebase | MergeStyle::RebaseMerge | MergeStyle::ManuallyMerged => {
950 return Err(RepoError::BadRequest(format!(
951 "merge style {:?} is not supported yet",
952 opt.style
953 )));
954 }
955 };
1031 MergeStyle::Rebase | MergeStyle::RebaseMerge | MergeStyle::ManuallyMerged => {
1032 return Err(RepoError::BadRequest(format!(
1033 "merge style {:?} is not supported yet",
1034 opt.style
1035 )));
1036 }
1037 };
1038
1039 refs_update(&git, &base_ref, new_sha, Some(live_base))?;
9561040
957 refs_update(&git, &base_ref, new_sha, Some(live_base))?;
1041 let db = svc.db.writer();
1042 let txn = db.begin().await?;
1043 let now = OffsetDateTime::now_utc();
9581044
959 let db = svc.db.writer();
960 let txn = db.begin().await?;
961 let now = OffsetDateTime::now_utc();
1045 let locked: pr_entity::Model = pr_entity::Entity::find()
1046 .filter(pr_entity::Column::Id.eq(pr.id))
1047 .lock_exclusive()
1048 .one(&txn)
1049 .await?
1050 .ok_or_else(|| RepoError::NotFound(format!("pull request #{number}")))?;
1051 if locked.has_merged {
1052 return Err(RepoError::Conflict("pull request already merged".into()));
1053 }
9621054
963 let locked: pr_entity::Model = pr_entity::Entity::find()
964 .filter(pr_entity::Column::Id.eq(pr.id))
965 .lock_exclusive()
966 .one(&txn)
967 .await?
968 .ok_or_else(|| RepoError::NotFound(format!("pull request #{number}")))?;
969 if locked.has_merged {
970 return Err(RepoError::Conflict("pull request already merged".into()));
971 }
1055 let mut active: pr_entity::ActiveModel = locked.into();
1056 active.has_merged = Set(true);
1057 active.merged_commit_sha = Set(Some(new_sha.to_string()));
1058 active.base_sha = Set(new_sha.to_string());
1059 active.merged_at = Set(Some(now));
1060 active.merger_id = Set(Some(actor));
1061 active.closed_at = Set(Some(now));
1062 active.updated_at = Set(Some(now));
1063 active.update(&txn).await?;
9721064
973 let mut active: pr_entity::ActiveModel = locked.into();
974 active.has_merged = Set(true);
975 active.merged_commit_sha = Set(Some(new_sha.to_string()));
976 active.base_sha = Set(new_sha.to_string());
977 active.merged_at = Set(Some(now));
978 active.merger_id = Set(Some(actor));
979 active.closed_at = Set(Some(now));
980 active.updated_at = Set(Some(now));
981 active.update(&txn).await?;
1065 if let Some(issue) = issue_entity::Entity::find_by_id(pr.issue_id)
1066 .one(&txn)
1067 .await?
1068 {
1069 let mut ia: issue_entity::ActiveModel = issue.into();
1070 ia.closed_at = Set(Some(now));
1071 ia.updated_at = Set(Some(now));
1072 ia.update(&txn).await?;
1073 }
9821074
983 if let Some(issue) = issue_entity::Entity::find_by_id(pr.issue_id)
984 .one(&txn)
985 .await?
986 {
987 let mut ia: issue_entity::ActiveModel = issue.into();
988 ia.closed_at = Set(Some(now));
989 ia.updated_at = Set(Some(now));
990 ia.update(&txn).await?;
991 }
1075 insert_event(
1076 &txn,
1077 pr.id,
1078 actor,
1079 "merged",
1080 Some(new_sha.to_string()),
1081 Some(
1082 serde_json::json!({ "style": format!("{:?}", opt.style), "mergeable": status.mergeable }).to_string(),
1083 ),
1084 )
1085 .await?;
9921086
993 insert_event(
994 &txn,
995 pr.id,
996 actor,
997 "merged",
998 Some(new_sha.to_string()),
999 Some(serde_json::json!({ "style": format!("{:?}", opt.style), "mergeable": status.mergeable }).to_string()),
1000 )
1001 .await?;
1087 txn.commit().await?;
10021088
1003 txn.commit().await?;
1089 if opt.delete_branch_after_merge {
1090 let _ = refs_delete(&git, &head_ref);
1091 }
10041092
1005 if opt.delete_branch_after_merge {
1006 let _ = refs_delete(&git, &head_ref);
1093 Ok(MergeView {
1094 sha: new_sha.to_string(),
1095 merged: true,
1096 message: None,
1097 })
10071098 }
1008
1009 Ok(MergeView {
1010 sha: new_sha.to_string(),
1011 merged: true,
1012 message: None,
1013 })
1099 .await;
1100 metrics::record_pull_request_merge(match &result {
1101 Ok(_) => metrics::PullRequestMergeOutcome::Success,
1102 Err(RepoError::NotFound(_)) => metrics::PullRequestMergeOutcome::NotFound,
1103 Err(RepoError::Conflict(_)) => metrics::PullRequestMergeOutcome::Conflict,
1104 Err(RepoError::Forbidden(_)) => metrics::PullRequestMergeOutcome::Forbidden,
1105 Err(_) => metrics::PullRequestMergeOutcome::Other,
1106 });
1107 result
10141108}
10151109
10161110fn parse_oid(hex: &str) -> RepoResult<ObjectId> {
Modified lib/repo/features/release/usecase.rs lib/repo/features/release/usecase.rs +180 −131 View lib/repo/features/release/usecase.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{RepoError, RepoResult};
77use crate::features::RepoService;
8use crate::metrics;
89use crate::path::validate_tag_name;
910
1011use super::{
@@ -40,34 +41,40 @@
4041 repo_id: Uuid,
4142 q: ListReleaseQuery,
4243) -> RepoResult<ReleasePage> {
43 let _ = actor;
44 let limit = q.limit.unwrap_or(svc.config.default_pagination);
45 let offset = q.page.unwrap_or(0).saturating_mul(limit);
46 let limit = std::cmp::min(limit, svc.config.max_pagination);
47 let db = svc.db.reader();
48 let mut query = release_entity::Entity::find()
49 .filter(release_entity::Column::RepoId.eq(repo_id))
50 .filter(release_entity::Column::IsTag.eq(false));
51 if let Some(d) = q.draft {
52 query = query.filter(release_entity::Column::IsDraft.eq(d));
44 let _timer = metrics::Timer::start(metrics::FEATURE_RELEASES_LIST);
45 let result: RepoResult<ReleasePage> = async {
46 let _ = actor;
47 let limit = q.limit.unwrap_or(svc.config.default_pagination);
48 let offset = q.page.unwrap_or(0).saturating_mul(limit);
49 let limit = std::cmp::min(limit, svc.config.max_pagination);
50 let db = svc.db.reader();
51 let mut query = release_entity::Entity::find()
52 .filter(release_entity::Column::RepoId.eq(repo_id))
53 .filter(release_entity::Column::IsTag.eq(false));
54 if let Some(d) = q.draft {
55 query = query.filter(release_entity::Column::IsDraft.eq(d));
56 }
57 if let Some(p) = q.prerelease {
58 query = query.filter(release_entity::Column::IsPrerelease.eq(p));
59 }
60 let total = query.clone().count(&db).await?;
61 let items = query
62 .order_by_desc(release_entity::Column::CreatedAt)
63 .limit(Some(limit))
64 .offset(Some(offset))
65 .all(&db)
66 .await?
67 .into_iter()
68 .map(ReleaseView::from)
69 .collect();
70 Ok(ReleasePage {
71 items,
72 total: total as i64,
73 })
5374 }
54 if let Some(p) = q.prerelease {
55 query = query.filter(release_entity::Column::IsPrerelease.eq(p));
56 }
57 let total = query.clone().count(&db).await?;
58 let items = query
59 .order_by_desc(release_entity::Column::CreatedAt)
60 .limit(Some(limit))
61 .offset(Some(offset))
62 .all(&db)
63 .await?
64 .into_iter()
65 .map(ReleaseView::from)
66 .collect();
67 Ok(ReleasePage {
68 items,
69 total: total as i64,
70 })
75 .await;
76 metrics::record_outcome(metrics::FEATURE_RELEASES_LIST, &result);
77 result
7178}
7279
7380pub async fn get_release(
@@ -76,9 +83,15 @@
7683 repo_id: Uuid,
7784 release_id: Uuid,
7885) -> RepoResult<ReleaseView> {
79 let _ = actor;
80 let release = load_release(svc, repo_id, release_id).await?;
81 Ok(ReleaseView::from(release))
86 let _timer = metrics::Timer::start(metrics::FEATURE_RELEASES_GET);
87 let result: RepoResult<ReleaseView> = async {
88 let _ = actor;
89 let release = load_release(svc, repo_id, release_id).await?;
90 Ok(ReleaseView::from(release))
91 }
92 .await;
93 metrics::record_outcome(metrics::FEATURE_RELEASES_GET, &result);
94 result
8295}
8396
8497pub async fn get_latest_release(
@@ -86,17 +99,23 @@
8699 actor: Uuid,
87100 repo_id: Uuid,
88101) -> RepoResult<ReleaseView> {
89 let _ = actor;
90 let db = svc.db.reader();
91 let release = release_entity::Entity::find()
92 .filter(release_entity::Column::RepoId.eq(repo_id))
93 .filter(release_entity::Column::IsTag.eq(false))
94 .filter(release_entity::Column::IsDraft.eq(false))
95 .order_by_desc(release_entity::Column::CreatedAt)
96 .one(&db)
97 .await?
98 .ok_or_else(|| RepoError::NotFound("no published release".into()))?;
99 Ok(ReleaseView::from(release))
102 let _timer = metrics::Timer::start(metrics::FEATURE_RELEASES_GET_LATEST);
103 let result: RepoResult<ReleaseView> = async {
104 let _ = actor;
105 let db = svc.db.reader();
106 let release = release_entity::Entity::find()
107 .filter(release_entity::Column::RepoId.eq(repo_id))
108 .filter(release_entity::Column::IsTag.eq(false))
109 .filter(release_entity::Column::IsDraft.eq(false))
110 .order_by_desc(release_entity::Column::CreatedAt)
111 .one(&db)
112 .await?
113 .ok_or_else(|| RepoError::NotFound("no published release".into()))?;
114 Ok(ReleaseView::from(release))
115 }
116 .await;
117 metrics::record_outcome(metrics::FEATURE_RELEASES_GET_LATEST, &result);
118 result
100119}
101120
102121pub async fn get_release_by_tag(
@@ -105,18 +124,24 @@
105124 repo_id: Uuid,
106125 tag: &str,
107126) -> RepoResult<ReleaseView> {
108 let _ = actor;
109 let db = svc.db.reader();
110 let release = release_entity::Entity::find()
111 .filter(release_entity::Column::RepoId.eq(repo_id))
112 .filter(release_entity::Column::TagName.eq(tag))
113 .one(&db)
114 .await?
115 .ok_or_else(|| RepoError::NotFound(format!("release for tag {tag}")))?;
116 if release.is_tag {
117 return Err(RepoError::NotFound(format!("release for tag {tag}")));
127 let _timer = metrics::Timer::start(metrics::FEATURE_RELEASES_GET_BY_TAG);
128 let result: RepoResult<ReleaseView> = async {
129 let _ = actor;
130 let db = svc.db.reader();
131 let release = release_entity::Entity::find()
132 .filter(release_entity::Column::RepoId.eq(repo_id))
133 .filter(release_entity::Column::TagName.eq(tag))
134 .one(&db)
135 .await?
136 .ok_or_else(|| RepoError::NotFound(format!("release for tag {tag}")))?;
137 if release.is_tag {
138 return Err(RepoError::NotFound(format!("release for tag {tag}")));
139 }
140 Ok(ReleaseView::from(release))
118141 }
119 Ok(ReleaseView::from(release))
142 .await;
143 metrics::record_outcome(metrics::FEATURE_RELEASES_GET_BY_TAG, &result);
144 result
120145}
121146
122147pub async fn create_release(
@@ -125,39 +150,45 @@
125150 repo_id: Uuid,
126151 input: CreateReleaseOption,
127152) -> RepoResult<ReleaseView> {
128 require_authenticated(actor)?;
129 let tag = validate_tag_name(&input.tag_name, svc.config.max_tag_name_len)?;
130 if let Some(ref c) = input.content {
131 if c.len() > svc.config.max_release_body_len {
132 return Err(RepoError::BadRequest(format!(
133 "release content too long (max {})",
134 svc.config.max_release_body_len
135 )));
153 let _timer = metrics::Timer::start(metrics::FEATURE_RELEASES_CREATE);
154 let result: RepoResult<ReleaseView> = async {
155 require_authenticated(actor)?;
156 let tag = validate_tag_name(&input.tag_name, svc.config.max_tag_name_len)?;
157 if let Some(ref c) = input.content {
158 if c.len() > svc.config.max_release_body_len {
159 return Err(RepoError::BadRequest(format!(
160 "release content too long (max {})",
161 svc.config.max_release_body_len
162 )));
163 }
136164 }
165 let _ = input.tag_message;
166 let db = svc.db.writer();
167 let now = OffsetDateTime::now_utc();
168 let active: release_entity::ActiveModel = release_entity::ActiveModel {
169 id: Set(Uuid::new_v4()),
170 repo_id: Set(repo_id),
171 publisher_id: Set(actor),
172 publisher_name: Set(String::new()),
173 tag_name: Set(tag.clone()),
174 lower_tag_name: Set(tag.to_lowercase()),
175 target: Set(input.target.unwrap_or_else(|| "main".to_string())),
176 title: Set(input.title),
177 content: Set(input.content),
178 sha: Set(String::new()),
179 num_commits: Set(0),
180 is_draft: Set(input.is_draft.unwrap_or(false)),
181 is_prerelease: Set(input.is_prerelease.unwrap_or(false)),
182 is_tag: Set(false),
183 created_at: Set(now),
184 updated_at: Set(now),
185 };
186 let saved = active.insert(&db).await?;
187 Ok(ReleaseView::from(saved))
137188 }
138 let _ = input.tag_message;
139 let db = svc.db.writer();
140 let now = OffsetDateTime::now_utc();
141 let active: release_entity::ActiveModel = release_entity::ActiveModel {
142 id: Set(Uuid::new_v4()),
143 repo_id: Set(repo_id),
144 publisher_id: Set(actor),
145 publisher_name: Set(String::new()),
146 tag_name: Set(tag.clone()),
147 lower_tag_name: Set(tag.to_lowercase()),
148 target: Set(input.target.unwrap_or_else(|| "main".to_string())),
149 title: Set(input.title),
150 content: Set(input.content),
151 sha: Set(String::new()),
152 num_commits: Set(0),
153 is_draft: Set(input.is_draft.unwrap_or(false)),
154 is_prerelease: Set(input.is_prerelease.unwrap_or(false)),
155 is_tag: Set(false),
156 created_at: Set(now),
157 updated_at: Set(now),
158 };
159 let saved = active.insert(&db).await?;
160 Ok(ReleaseView::from(saved))
189 .await;
190 metrics::record_outcome(metrics::FEATURE_RELEASES_CREATE, &result);
191 result
161192}
162193
163194pub async fn edit_release(
@@ -167,41 +198,47 @@
167198 release_id: Uuid,
168199 input: EditReleaseOption,
169200) -> RepoResult<ReleaseView> {
170 require_authenticated(actor)?;
171 let release = load_release(svc, repo_id, release_id).await?;
172 let db = svc.db.writer();
173 let mut active: release_entity::ActiveModel = release.into();
174 if let Some(t) = input.tag_name {
175 let validated = validate_tag_name(&t, svc.config.max_tag_name_len)?;
176 active.tag_name = Set(validated.clone());
177 active.lower_tag_name = Set(validated.to_lowercase());
178 }
179 if let Some(t) = input.target {
180 active.target = Set(t);
181 }
182 if let Some(t) = input.title {
183 active.title = Set(t);
184 }
185 if let Some(c) = input.content {
186 if let Some(ref body) = c {
187 if body.len() > svc.config.max_release_body_len {
188 return Err(RepoError::BadRequest(format!(
189 "release content too long (max {})",
190 svc.config.max_release_body_len
191 )));
201 let _timer = metrics::Timer::start(metrics::FEATURE_RELEASES_EDIT);
202 let result: RepoResult<ReleaseView> = async {
203 require_authenticated(actor)?;
204 let release = load_release(svc, repo_id, release_id).await?;
205 let db = svc.db.writer();
206 let mut active: release_entity::ActiveModel = release.into();
207 if let Some(t) = input.tag_name {
208 let validated = validate_tag_name(&t, svc.config.max_tag_name_len)?;
209 active.tag_name = Set(validated.clone());
210 active.lower_tag_name = Set(validated.to_lowercase());
211 }
212 if let Some(t) = input.target {
213 active.target = Set(t);
214 }
215 if let Some(t) = input.title {
216 active.title = Set(t);
217 }
218 if let Some(c) = input.content {
219 if let Some(ref body) = c {
220 if body.len() > svc.config.max_release_body_len {
221 return Err(RepoError::BadRequest(format!(
222 "release content too long (max {})",
223 svc.config.max_release_body_len
224 )));
225 }
192226 }
227 active.content = Set(c);
193228 }
194 active.content = Set(c);
195 }
196 if let Some(d) = input.is_draft {
197 active.is_draft = Set(d);
198 }
199 if let Some(p) = input.is_prerelease {
200 active.is_prerelease = Set(p);
229 if let Some(d) = input.is_draft {
230 active.is_draft = Set(d);
231 }
232 if let Some(p) = input.is_prerelease {
233 active.is_prerelease = Set(p);
234 }
235 active.updated_at = Set(OffsetDateTime::now_utc());
236 let saved = active.update(&db).await?;
237 Ok(ReleaseView::from(saved))
201238 }
202 active.updated_at = Set(OffsetDateTime::now_utc());
203 let saved = active.update(&db).await?;
204 Ok(ReleaseView::from(saved))
239 .await;
240 metrics::record_outcome(metrics::FEATURE_RELEASES_EDIT, &result);
241 result
205242}
206243
207244pub async fn delete_release(
@@ -210,13 +247,19 @@
210247 repo_id: Uuid,
211248 release_id: Uuid,
212249) -> RepoResult<()> {
213 require_authenticated(actor)?;
214 let _ = load_release(svc, repo_id, release_id).await?;
215 let db = svc.db.writer();
216 release_entity::Entity::delete_by_id(release_id)
217 .exec(&db)
218 .await?;
219 Ok(())
250 let _timer = metrics::Timer::start(metrics::FEATURE_RELEASES_DELETE);
251 let result: RepoResult<()> = async {
252 require_authenticated(actor)?;
253 let _ = load_release(svc, repo_id, release_id).await?;
254 let db = svc.db.writer();
255 release_entity::Entity::delete_by_id(release_id)
256 .exec(&db)
257 .await?;
258 Ok(())
259 }
260 .await;
261 metrics::record_outcome(metrics::FEATURE_RELEASES_DELETE, &result);
262 result
220263}
221264
222265pub async fn delete_release_by_tag(
@@ -225,13 +268,19 @@
225268 repo_id: Uuid,
226269 tag: &str,
227270) -> RepoResult<()> {
228 require_authenticated(actor)?;
229 let db = svc.db.writer();
230 release_entity::Entity::delete_many()
231 .filter(release_entity::Column::RepoId.eq(repo_id))
232 .filter(release_entity::Column::TagName.eq(tag))
233 .filter(release_entity::Column::IsTag.eq(false))
234 .exec(&db)
235 .await?;
236 Ok(())
271 let _timer = metrics::Timer::start(metrics::FEATURE_RELEASES_DELETE_BY_TAG);
272 let result: RepoResult<()> = async {
273 require_authenticated(actor)?;
274 let db = svc.db.writer();
275 release_entity::Entity::delete_many()
276 .filter(release_entity::Column::RepoId.eq(repo_id))
277 .filter(release_entity::Column::TagName.eq(tag))
278 .filter(release_entity::Column::IsTag.eq(false))
279 .exec(&db)
280 .await?;
281 Ok(())
282 }
283 .await;
284 metrics::record_outcome(metrics::FEATURE_RELEASES_DELETE_BY_TAG, &result);
285 result
237286}
Modified lib/repo/features/subscriber/usecase.rs lib/repo/features/subscriber/usecase.rs +28 −20 View lib/repo/features/subscriber/usecase.rs
@@ -3,6 +3,7 @@
33
44use crate::error::RepoResult;
55use crate::features::RepoService;
6use crate::metrics;
67
78use super::{ListSubscriberQuery, SubscriberPage, SubscriberView};
89
@@ -12,23 +13,30 @@
1213 repo_id: uuid::Uuid,
1314 q: ListSubscriberQuery,
1415) -> RepoResult<SubscriberPage> {
15 let _ = actor;
16 let limit = q.limit.unwrap_or(svc.config.default_pagination);
17 let offset = q.page.unwrap_or(0).saturating_mul(limit);
18 let limit = std::cmp::min(limit, svc.config.max_pagination);
19 let db = svc.db.reader();
20 let query = watch_entity::Entity::find().filter(watch_entity::Column::RepoId.eq(repo_id));
21 let total = query.clone().count(&db).await?;
22 let items = query
23 .limit(Some(limit))
24 .offset(Some(offset))
25 .all(&db)
26 .await?
27 .into_iter()
28 .map(SubscriberView::from)
29 .collect();
30 Ok(SubscriberPage {
31 items,
32 total: total as i64,
33 })
34}
16 let _timer = metrics::Timer::start(metrics::FEATURE_SUBSCRIBERS_LIST);
17 let result: RepoResult<SubscriberPage> = async {
18 let _ = actor;
19 let limit = q.limit.unwrap_or(svc.config.default_pagination);
20 let offset = q.page.unwrap_or(0).saturating_mul(limit);
21 let limit = std::cmp::min(limit, svc.config.max_pagination);
22 let db = svc.db.reader();
23 let query = watch_entity::Entity::find()
24 .filter(watch_entity::Column::RepoId.eq(repo_id));
25 let total = query.clone().count(&db).await?;
26 let items = query
27 .limit(Some(limit))
28 .offset(Some(offset))
29 .all(&db)
30 .await?
31 .into_iter()
32 .map(SubscriberView::from)
33 .collect();
34 Ok(SubscriberPage {
35 items,
36 total: total as i64,
37 })
38 }
39 .await;
40 metrics::record_outcome(metrics::FEATURE_SUBSCRIBERS_LIST, &result);
41 result
42}
No newline at end of file
Modified lib/repo/features/sync/usecase.rs lib/repo/features/sync/usecase.rs +65 −52 View lib/repo/features/sync/usecase.rs
@@ -6,6 +6,7 @@
66
77use crate::error::{RepoError, RepoResult};
88use crate::features::RepoService;
9use crate::metrics;
910
1011use super::{SyncResult, SyncStatus};
1112
@@ -21,36 +22,42 @@
2122 actor: Uuid,
2223 repo_id: Uuid,
2324) -> RepoResult<SyncStatus> {
24 let _ = actor;
25 let db = svc.db.reader();
26 let branches_total = branch_entity::Entity::find()
27 .filter(branch_entity::Column::RepoId.eq(repo_id))
28 .filter(branch_entity::Column::IsDeleted.eq(false))
29 .count(&db)
30 .await?;
31 let tags_total = tag_entity::Entity::find()
32 .filter(tag_entity::Column::RepoId.eq(repo_id))
33 .count(&db)
34 .await?;
35 let repo = db::repo::repos::Entity::find_by_id(repo_id)
36 .one(&db)
37 .await?
38 .ok_or_else(|| RepoError::NotFound(format!("repo {repo_id}")))?;
39 Ok(SyncStatus {
40 repo_id,
41 branches_total: branches_total as i64,
42 tags_total: tags_total as i64,
43 commits_total: repo.nums_commits as i64,
44 primary_language: repo.primary_language,
45 size: repo.size,
46 last_pushed_at: repo.last_pushed_at.map(|t| {
47 t.format(&time::format_description::well_known::Rfc3339)
48 .unwrap_or_default()
49 }),
50 synced_at: OffsetDateTime::now_utc()
51 .format(&time::format_description::well_known::Rfc3339)
52 .unwrap_or_default(),
53 })
25 let _timer = metrics::Timer::start(metrics::FEATURE_SYNC_GET_STATUS);
26 let result: RepoResult<SyncStatus> = async {
27 let _ = actor;
28 let db = svc.db.reader();
29 let branches_total = branch_entity::Entity::find()
30 .filter(branch_entity::Column::RepoId.eq(repo_id))
31 .filter(branch_entity::Column::IsDeleted.eq(false))
32 .count(&db)
33 .await?;
34 let tags_total = tag_entity::Entity::find()
35 .filter(tag_entity::Column::RepoId.eq(repo_id))
36 .count(&db)
37 .await?;
38 let repo = db::repo::repos::Entity::find_by_id(repo_id)
39 .one(&db)
40 .await?
41 .ok_or_else(|| RepoError::NotFound(format!("repo {repo_id}")))?;
42 Ok(SyncStatus {
43 repo_id,
44 branches_total: branches_total as i64,
45 tags_total: tags_total as i64,
46 commits_total: repo.nums_commits as i64,
47 primary_language: repo.primary_language,
48 size: repo.size,
49 last_pushed_at: repo.last_pushed_at.map(|t| {
50 t.format(&time::format_description::well_known::Rfc3339)
51 .unwrap_or_default()
52 }),
53 synced_at: OffsetDateTime::now_utc()
54 .format(&time::format_description::well_known::Rfc3339)
55 .unwrap_or_default(),
56 })
57 }
58 .await;
59 metrics::record_outcome(metrics::FEATURE_SYNC_GET_STATUS, &result);
60 result
5461}
5562
5663pub async fn sync_repo_metadata(
@@ -58,25 +65,31 @@
5865 actor: Uuid,
5966 repo_id: Uuid,
6067) -> RepoResult<SyncResult> {
61 require_authenticated(actor)?;
62 let db = svc.db.reader();
63 let branches_added = branch_entity::Entity::find()
64 .filter(branch_entity::Column::RepoId.eq(repo_id))
65 .filter(branch_entity::Column::IsDeleted.eq(false))
66 .count(&db)
67 .await?;
68 let tags_added = tag_entity::Entity::find()
69 .filter(tag_entity::Column::RepoId.eq(repo_id))
70 .count(&db)
71 .await?;
72 let _ = db::repo::repos::Entity::find_by_id(repo_id)
73 .one(&db)
74 .await?;
75 Ok(SyncResult {
76 repo_id,
77 branches_added: branches_added as i64,
78 tags_added: tags_added as i64,
79 commits_added: 0,
80 size_bytes: 0,
81 })
82}
68 let _timer = metrics::Timer::start(metrics::FEATURE_SYNC_REPO_METADATA);
69 let result: RepoResult<SyncResult> = async {
70 require_authenticated(actor)?;
71 let db = svc.db.reader();
72 let branches_added = branch_entity::Entity::find()
73 .filter(branch_entity::Column::RepoId.eq(repo_id))
74 .filter(branch_entity::Column::IsDeleted.eq(false))
75 .count(&db)
76 .await?;
77 let tags_added = tag_entity::Entity::find()
78 .filter(tag_entity::Column::RepoId.eq(repo_id))
79 .count(&db)
80 .await?;
81 let _ = db::repo::repos::Entity::find_by_id(repo_id)
82 .one(&db)
83 .await?;
84 Ok(SyncResult {
85 repo_id,
86 branches_added: branches_added as i64,
87 tags_added: tags_added as i64,
88 commits_added: 0,
89 size_bytes: 0,
90 })
91 }
92 .await;
93 metrics::record_outcome(metrics::FEATURE_SYNC_REPO_METADATA, &result);
94 result
95}
No newline at end of file
Modified lib/repo/features/tag/usecase.rs lib/repo/features/tag/usecase.rs +80 −55 View lib/repo/features/tag/usecase.rs
@@ -6,6 +6,7 @@
66
77use crate::error::{RepoError, RepoResult};
88use crate::features::RepoService;
9use crate::metrics;
910use crate::path::validate_tag_name;
1011
1112use super::{CreateTagOption, ListTagQuery, TagPage, TagView};
@@ -34,26 +35,32 @@
3435 repo_id: Uuid,
3536 q: ListTagQuery,
3637) -> RepoResult<TagPage> {
37 let _ = actor;
38 let limit = q.limit.unwrap_or(svc.config.default_pagination);
39 let offset = q.page.unwrap_or(0).saturating_mul(limit);
40 let limit = std::cmp::min(limit, svc.config.max_pagination);
41 let db = svc.db.reader();
42 let query = tag_entity::Entity::find().filter(tag_entity::Column::RepoId.eq(repo_id));
43 let total = query.clone().count(&db).await?;
44 let items = query
45 .order_by_asc(tag_entity::Column::Name)
46 .limit(Some(limit))
47 .offset(Some(offset))
48 .all(&db)
49 .await?
50 .into_iter()
51 .map(TagView::from)
52 .collect();
53 Ok(TagPage {
54 items,
55 total: total as i64,
56 })
38 let _timer = metrics::Timer::start(metrics::FEATURE_TAGS_LIST);
39 let result: RepoResult<TagPage> = async {
40 let _ = actor;
41 let limit = q.limit.unwrap_or(svc.config.default_pagination);
42 let offset = q.page.unwrap_or(0).saturating_mul(limit);
43 let limit = std::cmp::min(limit, svc.config.max_pagination);
44 let db = svc.db.reader();
45 let query = tag_entity::Entity::find().filter(tag_entity::Column::RepoId.eq(repo_id));
46 let total = query.clone().count(&db).await?;
47 let items = query
48 .order_by_asc(tag_entity::Column::Name)
49 .limit(Some(limit))
50 .offset(Some(offset))
51 .all(&db)
52 .await?
53 .into_iter()
54 .map(TagView::from)
55 .collect();
56 Ok(TagPage {
57 items,
58 total: total as i64,
59 })
60 }
61 .await;
62 metrics::record_outcome(metrics::FEATURE_TAGS_LIST, &result);
63 result
5764}
5865
5966pub async fn get_tag(
@@ -62,9 +69,15 @@
6269 repo_id: Uuid,
6370 name: &str,
6471) -> RepoResult<TagView> {
65 let _ = actor;
66 let tag = load_tag(svc, repo_id, name).await?;
67 Ok(TagView::from(tag))
72 let _timer = metrics::Timer::start(metrics::FEATURE_TAGS_GET);
73 let result: RepoResult<TagView> = async {
74 let _ = actor;
75 let tag = load_tag(svc, repo_id, name).await?;
76 Ok(TagView::from(tag))
77 }
78 .await;
79 metrics::record_outcome(metrics::FEATURE_TAGS_GET, &result);
80 result
6881}
6982
7083pub async fn create_tag(
@@ -73,29 +86,35 @@
7386 repo_id: Uuid,
7487 input: CreateTagOption,
7588) -> RepoResult<TagView> {
76 require_authenticated(actor)?;
77 let name = validate_tag_name(&input.tag_name, svc.config.max_tag_name_len)?;
78 let commit_sha = input.target.unwrap_or_else(|| "HEAD".to_string());
79 let is_annotated = input.message.is_some();
80 let db = svc.db.writer();
81 let now = OffsetDateTime::now_utc();
82 let active: tag_entity::ActiveModel = tag_entity::ActiveModel {
83 id: Set(Uuid::new_v4()),
84 repo_id: Set(repo_id),
85 name: Set(name),
86 tag_type: Set(if is_annotated {
87 TagType::Annotated
88 } else {
89 TagType::Lightweight
90 }),
91 commit_sha: Set(commit_sha),
92 message: Set(input.message),
93 tagger_name: Set(None),
94 tagger_email: Set(None),
95 created_at: Set(now),
96 };
97 let saved = active.insert(&db).await?;
98 Ok(TagView::from(saved))
89 let _timer = metrics::Timer::start(metrics::FEATURE_TAGS_CREATE);
90 let result: RepoResult<TagView> = async {
91 require_authenticated(actor)?;
92 let name = validate_tag_name(&input.tag_name, svc.config.max_tag_name_len)?;
93 let commit_sha = input.target.unwrap_or_else(|| "HEAD".to_string());
94 let is_annotated = input.message.is_some();
95 let db = svc.db.writer();
96 let now = OffsetDateTime::now_utc();
97 let active: tag_entity::ActiveModel = tag_entity::ActiveModel {
98 id: Set(Uuid::new_v4()),
99 repo_id: Set(repo_id),
100 name: Set(name),
101 tag_type: Set(if is_annotated {
102 TagType::Annotated
103 } else {
104 TagType::Lightweight
105 }),
106 commit_sha: Set(commit_sha),
107 message: Set(input.message),
108 tagger_name: Set(None),
109 tagger_email: Set(None),
110 created_at: Set(now),
111 };
112 let saved = active.insert(&db).await?;
113 Ok(TagView::from(saved))
114 }
115 .await;
116 metrics::record_outcome(metrics::FEATURE_TAGS_CREATE, &result);
117 result
99118}
100119
101120pub async fn delete_tag(
@@ -104,13 +123,19 @@
104123 repo_id: Uuid,
105124 name: &str,
106125) -> RepoResult<()> {
107 require_authenticated(actor)?;
108 let _ = load_tag(svc, repo_id, name).await?;
109 let db = svc.db.writer();
110 tag_entity::Entity::delete_many()
111 .filter(tag_entity::Column::RepoId.eq(repo_id))
112 .filter(tag_entity::Column::Name.eq(name))
113 .exec(&db)
114 .await?;
115 Ok(())
126 let _timer = metrics::Timer::start(metrics::FEATURE_TAGS_DELETE);
127 let result: RepoResult<()> = async {
128 require_authenticated(actor)?;
129 let _ = load_tag(svc, repo_id, name).await?;
130 let db = svc.db.writer();
131 tag_entity::Entity::delete_many()
132 .filter(tag_entity::Column::RepoId.eq(repo_id))
133 .filter(tag_entity::Column::Name.eq(name))
134 .exec(&db)
135 .await?;
136 Ok(())
137 }
138 .await;
139 metrics::record_outcome(metrics::FEATURE_TAGS_DELETE, &result);
140 result
116141}
Modified lib/repo/features/tag_protection/usecase.rs lib/repo/features/tag_protection/usecase.rs +90 −58 View lib/repo/features/tag_protection/usecase.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{RepoError, RepoResult};
77use crate::features::RepoService;
8use crate::metrics;
89
910use super::{
1011 CreateTagProtectionOption, EditTagProtectionOption, ListTagProtectionQuery, TagProtectionPage,
@@ -51,26 +52,33 @@
5152 repo_id: Uuid,
5253 q: ListTagProtectionQuery,
5354) -> RepoResult<TagProtectionPage> {
54 let _ = actor;
55 let limit = q.limit.unwrap_or(svc.config.default_pagination);
56 let offset = q.page.unwrap_or(0).saturating_mul(limit);
57 let limit = std::cmp::min(limit, svc.config.max_pagination);
58 let db = svc.db.reader();
59 let query = policy_entity::Entity::find().filter(policy_entity::Column::RepoId.eq(repo_id));
60 let total = query.clone().count(&db).await?;
61 let items = query
62 .order_by_asc(policy_entity::Column::NamePattern)
63 .limit(Some(limit))
64 .offset(Some(offset))
65 .all(&db)
66 .await?
67 .into_iter()
68 .map(TagProtectionView::from)
69 .collect();
70 Ok(TagProtectionPage {
71 items,
72 total: total as i64,
73 })
55 let _timer = metrics::Timer::start(metrics::FEATURE_TAG_PROTECTIONS_LIST);
56 let result: RepoResult<TagProtectionPage> = async {
57 let _ = actor;
58 let limit = q.limit.unwrap_or(svc.config.default_pagination);
59 let offset = q.page.unwrap_or(0).saturating_mul(limit);
60 let limit = std::cmp::min(limit, svc.config.max_pagination);
61 let db = svc.db.reader();
62 let query =
63 policy_entity::Entity::find().filter(policy_entity::Column::RepoId.eq(repo_id));
64 let total = query.clone().count(&db).await?;
65 let items = query
66 .order_by_asc(policy_entity::Column::NamePattern)
67 .limit(Some(limit))
68 .offset(Some(offset))
69 .all(&db)
70 .await?
71 .into_iter()
72 .map(TagProtectionView::from)
73 .collect();
74 Ok(TagProtectionPage {
75 items,
76 total: total as i64,
77 })
78 }
79 .await;
80 metrics::record_outcome(metrics::FEATURE_TAG_PROTECTIONS_LIST, &result);
81 result
7482}
7583
7684pub async fn create_tag_protection(
@@ -79,23 +87,29 @@
7987 repo_id: Uuid,
8088 input: CreateTagProtectionOption,
8189) -> RepoResult<TagProtectionView> {
82 require_admin(actor)?;
83 if input.name_pattern.trim().is_empty() {
84 return Err(RepoError::BadRequest("name_pattern is required".into()));
90 let _timer = metrics::Timer::start(metrics::FEATURE_TAG_PROTECTIONS_CREATE);
91 let result: RepoResult<TagProtectionView> = async {
92 require_admin(actor)?;
93 if input.name_pattern.trim().is_empty() {
94 return Err(RepoError::BadRequest("name_pattern is required".into()));
95 }
96 let db = svc.db.writer();
97 let now = OffsetDateTime::now_utc();
98 let users = input.whitelist_usernames.unwrap_or_default();
99 let active: policy_entity::ActiveModel = policy_entity::ActiveModel {
100 id: Set(Uuid::new_v4()),
101 repo_id: Set(repo_id),
102 name_pattern: Set(input.name_pattern),
103 allowlist_user_ids: Set(join_uuids(&users)),
104 created_at: Set(now),
105 updated_at: Set(now),
106 };
107 let saved = active.insert(&db).await?;
108 Ok(TagProtectionView::from(saved))
85109 }
86 let db = svc.db.writer();
87 let now = OffsetDateTime::now_utc();
88 let users = input.whitelist_usernames.unwrap_or_default();
89 let active: policy_entity::ActiveModel = policy_entity::ActiveModel {
90 id: Set(Uuid::new_v4()),
91 repo_id: Set(repo_id),
92 name_pattern: Set(input.name_pattern),
93 allowlist_user_ids: Set(join_uuids(&users)),
94 created_at: Set(now),
95 updated_at: Set(now),
96 };
97 let saved = active.insert(&db).await?;
98 Ok(TagProtectionView::from(saved))
110 .await;
111 metrics::record_outcome(metrics::FEATURE_TAG_PROTECTIONS_CREATE, &result);
112 result
99113}
100114
101115pub async fn get_tag_protection(
@@ -104,9 +118,15 @@
104118 repo_id: Uuid,
105119 policy_id: Uuid,
106120) -> RepoResult<TagProtectionView> {
107 let _ = actor;
108 let policy = load_policy(svc, repo_id, policy_id).await?;
109 Ok(TagProtectionView::from(policy))
121 let _timer = metrics::Timer::start(metrics::FEATURE_TAG_PROTECTIONS_GET);
122 let result: RepoResult<TagProtectionView> = async {
123 let _ = actor;
124 let policy = load_policy(svc, repo_id, policy_id).await?;
125 Ok(TagProtectionView::from(policy))
126 }
127 .await;
128 metrics::record_outcome(metrics::FEATURE_TAG_PROTECTIONS_GET, &result);
129 result
110130}
111131
112132pub async fn edit_tag_protection(
@@ -116,19 +136,25 @@
116136 policy_id: Uuid,
117137 input: EditTagProtectionOption,
118138) -> RepoResult<TagProtectionView> {
119 require_admin(actor)?;
120 let policy = load_policy(svc, repo_id, policy_id).await?;
121 let db = svc.db.writer();
122 let mut active: policy_entity::ActiveModel = policy.into();
123 if let Some(p) = input.name_pattern {
124 active.name_pattern = Set(p);
125 }
126 if let Some(ids) = input.whitelist_usernames {
127 active.allowlist_user_ids = Set(join_uuids(&ids));
139 let _timer = metrics::Timer::start(metrics::FEATURE_TAG_PROTECTIONS_EDIT);
140 let result: RepoResult<TagProtectionView> = async {
141 require_admin(actor)?;
142 let policy = load_policy(svc, repo_id, policy_id).await?;
143 let db = svc.db.writer();
144 let mut active: policy_entity::ActiveModel = policy.into();
145 if let Some(p) = input.name_pattern {
146 active.name_pattern = Set(p);
147 }
148 if let Some(ids) = input.whitelist_usernames {
149 active.allowlist_user_ids = Set(join_uuids(&ids));
150 }
151 active.updated_at = Set(OffsetDateTime::now_utc());
152 let saved = active.update(&db).await?;
153 Ok(TagProtectionView::from(saved))
128154 }
129 active.updated_at = Set(OffsetDateTime::now_utc());
130 let saved = active.update(&db).await?;
131 Ok(TagProtectionView::from(saved))
155 .await;
156 metrics::record_outcome(metrics::FEATURE_TAG_PROTECTIONS_EDIT, &result);
157 result
132158}
133159
134160pub async fn delete_tag_protection(
@@ -137,11 +163,17 @@
137163 repo_id: Uuid,
138164 policy_id: Uuid,
139165) -> RepoResult<()> {
140 require_admin(actor)?;
141 let _ = load_policy(svc, repo_id, policy_id).await?;
142 let db = svc.db.writer();
143 policy_entity::Entity::delete_by_id(policy_id)
144 .exec(&db)
145 .await?;
146 Ok(())
166 let _timer = metrics::Timer::start(metrics::FEATURE_TAG_PROTECTIONS_DELETE);
167 let result: RepoResult<()> = async {
168 require_admin(actor)?;
169 let _ = load_policy(svc, repo_id, policy_id).await?;
170 let db = svc.db.writer();
171 policy_entity::Entity::delete_by_id(policy_id)
172 .exec(&db)
173 .await?;
174 Ok(())
175 }
176 .await;
177 metrics::record_outcome(metrics::FEATURE_TAG_PROTECTIONS_DELETE, &result);
178 result
147179}
Modified lib/repo/features/topic/usecase.rs lib/repo/features/topic/usecase.rs +151 −120 View lib/repo/features/topic/usecase.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{RepoError, RepoResult};
77use crate::features::RepoService;
8use crate::metrics;
89use crate::path::{normalize_topic, sanitize_topics};
910
1011use super::{RepoTopicOptions, TopicList, TopicSearchQuery, TopicSearchResult, TopicView};
@@ -40,24 +41,30 @@
4041 actor: Uuid,
4142 repo_id: Uuid,
4243) -> RepoResult<TopicList> {
43 let _ = actor;
44 let db = svc.db.reader();
45 let links = db::repo::repo_relation_topic::Entity::find()
46 .filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
47 .all(&db)
48 .await?;
49 let mut names: Vec<String> = Vec::with_capacity(links.len());
50 for link in links {
51 if let Some(t) = topic_entity::Entity::find_by_id(link.topic_id)
52 .one(&db)
53 .await?
54 {
55 names.push(t.name);
44 let _timer = metrics::Timer::start(metrics::FEATURE_TOPICS_LIST);
45 let result: RepoResult<TopicList> = async {
46 let _ = actor;
47 let db = svc.db.reader();
48 let links = db::repo::repo_relation_topic::Entity::find()
49 .filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
50 .all(&db)
51 .await?;
52 let mut names: Vec<String> = Vec::with_capacity(links.len());
53 for link in links {
54 if let Some(t) = topic_entity::Entity::find_by_id(link.topic_id)
55 .one(&db)
56 .await?
57 {
58 names.push(t.name);
59 }
5660 }
61 names.sort();
62 names.dedup();
63 Ok(TopicList { topics: names })
5764 }
58 names.sort();
59 names.dedup();
60 Ok(TopicList { topics: names })
65 .await;
66 metrics::record_outcome(metrics::FEATURE_TOPICS_LIST, &result);
67 result
6168}
6269
6370pub async fn update_repo_topics(
@@ -66,44 +73,50 @@
6673 repo_id: Uuid,
6774 input: RepoTopicOptions,
6875) -> RepoResult<()> {
69 require_authenticated(actor)?;
70 let normalized = sanitize_topics(&input.topics, svc.config.max_topics_per_repo)?;
71 let db = svc.db.writer();
72 let existing = db::repo::repo_relation_topic::Entity::find()
73 .filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
74 .all(&db)
75 .await?;
76 for r in existing {
77 if let Some(t) = topic_entity::Entity::find_by_id(r.topic_id)
78 .one(&db)
79 .await?
80 {
81 if !normalized.contains(&t.name) {
82 db::repo::repo_relation_topic::Entity::delete_by_id(r.id)
83 .exec(&db)
84 .await?;
76 let _timer = metrics::Timer::start(metrics::FEATURE_TOPICS_UPDATE);
77 let result: RepoResult<()> = async {
78 require_authenticated(actor)?;
79 let normalized = sanitize_topics(&input.topics, svc.config.max_topics_per_repo)?;
80 let db = svc.db.writer();
81 let existing = db::repo::repo_relation_topic::Entity::find()
82 .filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
83 .all(&db)
84 .await?;
85 for r in existing {
86 if let Some(t) = topic_entity::Entity::find_by_id(r.topic_id)
87 .one(&db)
88 .await?
89 {
90 if !normalized.contains(&t.name) {
91 db::repo::repo_relation_topic::Entity::delete_by_id(r.id)
92 .exec(&db)
93 .await?;
94 }
8595 }
8696 }
87 }
88 for name in &normalized {
89 let topic = ensure_topic(svc, name).await?;
90 let exists = db::repo::repo_relation_topic::Entity::find()
91 .filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
92 .filter(db::repo::repo_relation_topic::Column::TopicId.eq(topic.id))
93 .one(&db)
94 .await?;
95 if exists.is_none() {
96 let active: db::repo::repo_relation_topic::ActiveModel =
97 db::repo::repo_relation_topic::ActiveModel {
98 id: Set(Uuid::new_v4()),
99 repo_id: Set(repo_id),
100 topic_id: Set(topic.id),
101 created_at: Set(OffsetDateTime::now_utc()),
102 };
103 active.insert(&db).await?;
97 for name in &normalized {
98 let topic = ensure_topic(svc, name).await?;
99 let exists = db::repo::repo_relation_topic::Entity::find()
100 .filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
101 .filter(db::repo::repo_relation_topic::Column::TopicId.eq(topic.id))
102 .one(&db)
103 .await?;
104 if exists.is_none() {
105 let active: db::repo::repo_relation_topic::ActiveModel =
106 db::repo::repo_relation_topic::ActiveModel {
107 id: Set(Uuid::new_v4()),
108 repo_id: Set(repo_id),
109 topic_id: Set(topic.id),
110 created_at: Set(OffsetDateTime::now_utc()),
111 };
112 active.insert(&db).await?;
113 }
104114 }
115 Ok(())
105116 }
106 Ok(())
117 .await;
118 metrics::record_outcome(metrics::FEATURE_TOPICS_UPDATE, &result);
119 result
107120}
108121
109122pub async fn add_repo_topic(
@@ -112,37 +125,43 @@
112125 repo_id: Uuid,
113126 topic: &str,
114127) -> RepoResult<()> {
115 require_authenticated(actor)?;
116 let name = normalize_topic(topic)?;
117 let current = db::repo::repo_relation_topic::Entity::find()
118 .filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
119 .count(&svc.db.reader())
120 .await?;
121 if current >= svc.config.max_topics_per_repo as u64 {
122 return Err(RepoError::BadRequest(format!(
123 "too many topics (max {})",
124 svc.config.max_topics_per_repo
125 )));
126 }
127 let topic_model = ensure_topic(svc, &name).await?;
128 let db = svc.db.writer();
129 let exists = db::repo::repo_relation_topic::Entity::find()
130 .filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
131 .filter(db::repo::repo_relation_topic::Column::TopicId.eq(topic_model.id))
132 .one(&db)
133 .await?;
134 if exists.is_some() {
135 return Ok(());
128 let _timer = metrics::Timer::start(metrics::FEATURE_TOPICS_ADD);
129 let result: RepoResult<()> = async {
130 require_authenticated(actor)?;
131 let name = normalize_topic(topic)?;
132 let current = db::repo::repo_relation_topic::Entity::find()
133 .filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
134 .count(&svc.db.reader())
135 .await?;
136 if current >= svc.config.max_topics_per_repo as u64 {
137 return Err(RepoError::BadRequest(format!(
138 "too many topics (max {})",
139 svc.config.max_topics_per_repo
140 )));
141 }
142 let topic_model = ensure_topic(svc, &name).await?;
143 let db = svc.db.writer();
144 let exists = db::repo::repo_relation_topic::Entity::find()
145 .filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
146 .filter(db::repo::repo_relation_topic::Column::TopicId.eq(topic_model.id))
147 .one(&db)
148 .await?;
149 if exists.is_some() {
150 return Ok(());
151 }
152 let active: db::repo::repo_relation_topic::ActiveModel =
153 db::repo::repo_relation_topic::ActiveModel {
154 id: Set(Uuid::new_v4()),
155 repo_id: Set(repo_id),
156 topic_id: Set(topic_model.id),
157 created_at: Set(OffsetDateTime::now_utc()),
158 };
159 active.insert(&db).await?;
160 Ok(())
136161 }
137 let active: db::repo::repo_relation_topic::ActiveModel =
138 db::repo::repo_relation_topic::ActiveModel {
139 id: Set(Uuid::new_v4()),
140 repo_id: Set(repo_id),
141 topic_id: Set(topic_model.id),
142 created_at: Set(OffsetDateTime::now_utc()),
143 };
144 active.insert(&db).await?;
145 Ok(())
162 .await;
163 metrics::record_outcome(metrics::FEATURE_TOPICS_ADD, &result);
164 result
146165}
147166
148167pub async fn delete_repo_topic(
@@ -151,20 +170,26 @@
151170 repo_id: Uuid,
152171 topic: &str,
153172) -> RepoResult<()> {
154 require_authenticated(actor)?;
155 let name = normalize_topic(topic)?;
156 let db = svc.db.writer();
157 let topic_model = topic_entity::Entity::find()
158 .filter(topic_entity::Column::Name.eq(&name))
159 .one(&db)
160 .await?
161 .ok_or_else(|| RepoError::NotFound(format!("topic {name}")))?;
162 db::repo::repo_relation_topic::Entity::delete_many()
163 .filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
164 .filter(db::repo::repo_relation_topic::Column::TopicId.eq(topic_model.id))
165 .exec(&db)
166 .await?;
167 Ok(())
173 let _timer = metrics::Timer::start(metrics::FEATURE_TOPICS_DELETE);
174 let result: RepoResult<()> = async {
175 require_authenticated(actor)?;
176 let name = normalize_topic(topic)?;
177 let db = svc.db.writer();
178 let topic_model = topic_entity::Entity::find()
179 .filter(topic_entity::Column::Name.eq(&name))
180 .one(&db)
181 .await?
182 .ok_or_else(|| RepoError::NotFound(format!("topic {name}")))?;
183 db::repo::repo_relation_topic::Entity::delete_many()
184 .filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
185 .filter(db::repo::repo_relation_topic::Column::TopicId.eq(topic_model.id))
186 .exec(&db)
187 .await?;
188 Ok(())
189 }
190 .await;
191 metrics::record_outcome(metrics::FEATURE_TOPICS_DELETE, &result);
192 result
168193}
169194
170195pub async fn search_topics(
@@ -172,31 +197,37 @@
172197 actor: Uuid,
173198 q: TopicSearchQuery,
174199) -> RepoResult<TopicSearchResult> {
175 let _ = actor;
176 let limit = q.limit.unwrap_or(svc.config.default_pagination);
177 let offset = q.page.unwrap_or(0).saturating_mul(limit);
178 let limit = std::cmp::min(limit, svc.config.max_pagination);
179 let db = svc.db.reader();
180 let mut query = topic_entity::Entity::find();
181 if let Some(s) = q.q.as_deref() {
182 if !s.is_empty() {
183 query = query.filter(topic_entity::Column::Name.contains(s));
200 let _timer = metrics::Timer::start(metrics::FEATURE_TOPICS_SEARCH);
201 let result: RepoResult<TopicSearchResult> = async {
202 let _ = actor;
203 let limit = q.limit.unwrap_or(svc.config.default_pagination);
204 let offset = q.page.unwrap_or(0).saturating_mul(limit);
205 let limit = std::cmp::min(limit, svc.config.max_pagination);
206 let db = svc.db.reader();
207 let mut query = topic_entity::Entity::find();
208 if let Some(s) = q.q.as_deref() {
209 if !s.is_empty() {
210 query = query.filter(topic_entity::Column::Name.contains(s));
211 }
184212 }
213 let items = query
214 .order_by_asc(topic_entity::Column::Name)
215 .limit(Some(limit))
216 .offset(Some(offset))
217 .all(&db)
218 .await?
219 .into_iter()
220 .map(|t| TopicView {
221 id: t.id,
222 name: t.name.clone(),
223 repo_count: 0,
224 created_at: super::fmt_ts_pub(t.created_at),
225 updated_at: super::fmt_ts_pub(t.updated_at),
226 })
227 .collect();
228 Ok(TopicSearchResult { topics: items })
185229 }
186 let items = query
187 .order_by_asc(topic_entity::Column::Name)
188 .limit(Some(limit))
189 .offset(Some(offset))
190 .all(&db)
191 .await?
192 .into_iter()
193 .map(|t| TopicView {
194 id: t.id,
195 name: t.name.clone(),
196 repo_count: 0,
197 created_at: super::fmt_ts_pub(t.created_at),
198 updated_at: super::fmt_ts_pub(t.updated_at),
199 })
200 .collect();
201 Ok(TopicSearchResult { topics: items })
202}
230 .await;
231 metrics::record_outcome(metrics::FEATURE_TOPICS_SEARCH, &result);
232 result
233}
No newline at end of file