Skip to content
Clone

style(rust): clippy sweep - let-chains, is_multiple_of, split_once, derive(Default)

Mechanical cleanups now that Rust 2024 let-chains are available: nested
`if let Some(x) { if cond }` collapses into one chain, `% n == 0` becomes
`is_multiple_of(n)`, `splitn(2, ..).nth(1)` becomes `split_once(..).map(|x| x.1)`,
a hand-written Default impl becomes `#[derive(Default)]`, redundant derefs
in web/lib.rs tests go away, and the axum-session `rest_mode` build gets an
allow for the parameter it compiles out. Everything else in this commit is
`cargo fmt` reflow of the touched lines.
ZhenYi committed
parent 5e77804 commit ffc8ef9 Browse files

83 files changed

+545 −515

File tree 83
Modified app/aihost/src/main.rs View app/aihost/src/main.rs
@@ -111,7 +111,6 @@
111111 info!(%job_id, kind = task.kind_str(), "ai task completed");
112112 }
113113 Err(error) => {
114
115114 if !error.is_retryable() {
116115 attempts.remove(&job_id);
117116 warn!(%error, %job_id, "ai task is not retryable; sending to DLQ");
Modified app/im/src/gateway.rs View app/im/src/gateway.rs
@@ -101,7 +101,7 @@
101101 allowed.push(host);
102102 }
103103 }
104 allowed.iter().any(|host| *host == origin_host)
104 allowed.contains(&origin_host)
105105}
106106
107107fn extract_jwt(headers: &hyper::HeaderMap) -> Option<String> {
@@ -111,10 +111,10 @@
111111 };
112112 for proto in raw.split(',') {
113113 let proto = proto.trim();
114 if let Some(token) = proto.strip_prefix(JWT_PROTOCOL_PREFIX) {
115 if !token.is_empty() {
116 return Some(token.to_string());
117 }
114 if let Some(token) = proto.strip_prefix(JWT_PROTOCOL_PREFIX)
115 && !token.is_empty()
116 {
117 return Some(token.to_string());
118118 }
119119 }
120120 }
Modified app/im/src/socket.rs View app/im/src/socket.rs
@@ -201,7 +201,7 @@
201201 // worker 把他当成离线,于是同一个事件既弹 toast 又弹系统通知。
202202 online.refresh().await;
203203 tick_count = tick_count.wrapping_add(1);
204 if tick_count % 2 == 0 {
204 if tick_count.is_multiple_of(2) {
205205 let mut revoked: Vec<Uuid> = Vec::new();
206206 for &room_id in subscriptions.keys() {
207207 if service_can_read(&service, uid, room_id).await.is_err() {
Modified app/mails/src/main.rs View app/mails/src/main.rs
@@ -16,7 +16,11 @@
1616 let redis_url = cfg.redis.url().to_string();
1717 let redis_host = match redis_url.rsplit_once('@') {
1818 Some((_, host)) => host.to_string(),
19 None => redis_url.splitn(2, "://").nth(1).unwrap_or("?").to_string(),
19 None => redis_url
20 .split_once("://")
21 .map(|x| x.1)
22 .unwrap_or("?")
23 .to_string(),
2024 };
2125
2226 info!(
Modified app/webhook/src/main.rs View app/webhook/src/main.rs
@@ -197,7 +197,7 @@
197197 warn!(%error, "run finalization failed");
198198 }
199199 ticks = ticks.wrapping_add(1);
200 if ticks % 3600 == 0
200 if ticks.is_multiple_of(3600)
201201 && let Err(error) = service.prune_expired().await
202202 {
203203 warn!(%error, "retention prune failed");
Modified lib/admin/src/services/logs.rs View lib/admin/src/services/logs.rs
@@ -277,7 +277,7 @@
277277 }
278278 }
279279
280 logs.sort_by(|left, right| right.created_at.cmp(&left.created_at));
280 logs.sort_by_key(|log| std::cmp::Reverse(log.created_at));
281281 let total = logs.len() as i64;
282282 let items = logs
283283 .into_iter()
Modified lib/admin/src/services/tasks.rs View lib/admin/src/services/tasks.rs
@@ -238,7 +238,7 @@
238238 .is_some_and(|error| error.to_ascii_lowercase().contains(&needle))
239239 });
240240 }
241 tasks.sort_by(|left, right| right.created_at.cmp(&left.created_at));
241 tasks.sort_by_key(|task| std::cmp::Reverse(task.created_at));
242242 let total = tasks.len() as i64;
243243 let items = tasks
244244 .into_iter()
@@ -802,16 +802,9 @@
802802 ));
803803 }
804804 // 仅改数据库状态不会有消费者看到任务:必须按持久化载荷原样重投。
805 git::sync::retry_ai_job(
806 self.db().write(),
807 self.redis(),
808 &self.config().ai,
809 &row,
810 )
811 .await
812 .map_err(|error| {
813 AdminError::Internal(format!("requeue ai job: {error}"))
814 })?;
805 git::sync::retry_ai_job(self.db().write(), self.redis(), &self.config().ai, &row)
806 .await
807 .map_err(|error| AdminError::Internal(format!("requeue ai job: {error}")))?;
815808 "pending".to_string()
816809 }
817810 _ => return Err(AdminError::BadRequest("unknown task source".to_string())),
Modified lib/admin/src/services/users.rs View lib/admin/src/services/users.rs
@@ -117,7 +117,10 @@
117117 pub suspension_type: SuspensionType,
118118 #[serde(default)]
119119 pub reason: Option<String>,
120 #[serde(default, deserialize_with = "export::serde_datetime::deserialize_opt_datetime")]
120 #[serde(
121 default,
122 deserialize_with = "export::serde_datetime::deserialize_opt_datetime"
123 )]
121124 pub ends_at: Option<OffsetDateTime>,
122125}
123126
@@ -745,7 +748,10 @@
745748 pub reason: Option<String>,
746749 #[serde(default)]
747750 pub suspension_type: Option<SuspensionType>,
748 #[serde(default, deserialize_with = "export::serde_datetime::deserialize_opt_datetime")]
751 #[serde(
752 default,
753 deserialize_with = "export::serde_datetime::deserialize_opt_datetime"
754 )]
749755 pub ends_at: Option<OffsetDateTime>,
750756}
751757
Modified lib/agent/src/context.rs View lib/agent/src/context.rs
@@ -196,6 +196,9 @@
196196 out
197197}
198198
199/// 让 `GitContext` 在本 crate 内可用(避免调用方各自引入 git crate)。
200pub type RepoContext = GitContext;
201
199202#[cfg(test)]
200203mod tests {
201204 use super::*;
@@ -208,7 +211,10 @@
208211 let message = "fix: 修正接收器计数\n\nBufferedUdpMetricSink 在批量发送后少记一次,\n对监控口径有影响。\n\nSigned-off-by: Someone <s@example.com>\n";
209212 let body = commit_body(message, "fix: 修正接收器计数").expect("正文应被保留");
210213 assert!(body.contains("少记一次"), "正文要点必须保留: {body}");
211 assert!(!body.contains("Signed-off-by"), "trailer 不应进入提示词: {body}");
214 assert!(
215 !body.contains("Signed-off-by"),
216 "trailer 不应进入提示词: {body}"
217 );
212218 }
213219
214220 /// 没有正文、或正文与标题重复时不必占用预算。
@@ -227,6 +233,3 @@
227233 assert!(body.ends_with("…\n"));
228234 }
229235}
230
231/// 让 `GitContext` 在本 crate 内可用(避免调用方各自引入 git crate)。
232pub type RepoContext = GitContext;
Modified lib/agent/src/prompt.rs View lib/agent/src/prompt.rs
@@ -238,10 +238,10 @@
238238 let Some((_, target)) = rest.trim().split_once(" b/") else {
239239 continue;
240240 };
241 if let Some(language) = language_of_path(target.trim()) {
242 if !found.iter().any(|have| have == language) {
243 found.push(language.to_string());
244 }
241 if let Some(language) = language_of_path(target.trim())
242 && !found.iter().any(|have| have == language)
243 {
244 found.push(language.to_string());
245245 }
246246 }
247247 found
Modified lib/auth/src/handlers/mark_notifications_read.rs View lib/auth/src/handlers/mark_notifications_read.rs
@@ -48,20 +48,14 @@
4848 u64::MAX
4949 } else if let Some(ids) = cmd.ids {
5050 if ids.is_empty() {
51 return Err(ApiError::BadRequest(AppResponseData::err(
52 400,
53 "ids must not be empty",
54 )));
51 return Err(ApiError::bad_request("ids must not be empty"));
5552 }
5653 service
5754 .mark_notifications_read(uid, ids)
5855 .await
5956 .map_err(ApiError::from)?
6057 } else {
61 return Err(ApiError::BadRequest(AppResponseData::err(
62 400,
63 "provide ids or all",
64 )));
58 return Err(ApiError::bad_request("provide ids or all"));
6559 };
6660 Ok(AppResponseData::ok(MarkedCountResp { marked }))
6761}
Modified lib/auth/src/services/access_keys.rs View lib/auth/src/services/access_keys.rs
@@ -19,7 +19,10 @@
1919pub struct CreateAccessKeyCmd {
2020 pub name: String,
2121 pub scope: AccessKeyScope,
22 #[serde(default, deserialize_with = "export::serde_datetime::deserialize_opt_datetime")]
22 #[serde(
23 default,
24 deserialize_with = "export::serde_datetime::deserialize_opt_datetime"
25 )]
2326 pub expires_at: Option<OffsetDateTime>,
2427}
2528
Modified lib/auth/src/services/email.rs View lib/auth/src/services/email.rs
@@ -279,8 +279,13 @@
279279 &artifacts.code,
280280 cmd.purpose,
281281 );
282 let html =
283 crate::templates::mail_verify_code(&artifacts.code, minutes, subject, link.as_deref(), locale);
282 let html = crate::templates::mail_verify_code(
283 &artifacts.code,
284 minutes,
285 subject,
286 link.as_deref(),
287 locale,
288 );
284289 let body = i18n::catalog::verify_text(locale, &artifacts.code);
285290 let sent = self
286291 .send_email(email_row.email.as_str(), subject, &body, html.0.as_str())
@@ -340,9 +345,7 @@
340345 &artifacts.code,
341346 EmailVerificationPurpose::Reclaim,
342347 );
343 let locale = self
344 .mail_locale(Some(user.uid), accept_language)
345 .await;
348 let locale = self.mail_locale(Some(user.uid), accept_language).await;
346349 let subject = i18n::catalog::verify_subject(locale, i18n::VerifyPurpose::Reclaim);
347350 let html = crate::templates::mail_verify_code(
348351 &artifacts.code,
@@ -353,12 +356,7 @@
353356 );
354357 let body = i18n::catalog::verify_text(locale, &artifacts.code);
355358 let sent = self
356 .send_email(
357 email_row.email.as_str(),
358 subject,
359 &body,
360 html.0.as_str(),
361 )
359 .send_email(email_row.email.as_str(), subject, &body, html.0.as_str())
362360 .await
363361 .map(|_| true)
364362 .unwrap_or_else(|error| {
@@ -640,13 +638,11 @@
640638
641639/// Bridge the model enum to the catalogue's own purpose enum, keeping `i18n`
642640/// free of a dependency on `models`.
643pub(crate) fn verify_purpose(
644 purpose: EmailVerificationPurpose,
645) -> i18n::VerifyPurpose {
641pub(crate) fn verify_purpose(purpose: EmailVerificationPurpose) -> i18n::VerifyPurpose {
646642 match purpose {
647643 EmailVerificationPurpose::Signup => i18n::VerifyPurpose::Signup,
648644 EmailVerificationPurpose::PasswordReset => i18n::VerifyPurpose::PasswordReset,
649645 EmailVerificationPurpose::AddEmail => i18n::VerifyPurpose::AddEmail,
650646 EmailVerificationPurpose::Reclaim => i18n::VerifyPurpose::Reclaim,
651647 }
652}
No newline at end of file
648}
Modified lib/auth/src/services/login.rs View lib/auth/src/services/login.rs
@@ -301,7 +301,7 @@
301301 let _ = active.update(db.write()).await;
302302
303303 Ok(LoginResp {
304 user: user_dto(&user),
304 user: user_dto(user),
305305 needs_2fa: false,
306306 challenge: None,
307307 })
Modified lib/auth/src/services/mod.rs View lib/auth/src/services/mod.rs
@@ -64,9 +64,7 @@
6464 let fallback = i18n::UiLocale::parse(self.config().email.default_locale())
6565 .unwrap_or(i18n::UiLocale::En);
6666 match uid {
67 Some(uid) => {
68 i18n::resolve_user_locale(self.db().read(), uid, fallback).await
69 }
67 Some(uid) => i18n::resolve_user_locale(self.db().read(), uid, fallback).await,
7068 None => i18n::negotiate_accept_language(accept_language, fallback),
7169 }
7270 }
Modified lib/auth/src/services/register.rs View lib/auth/src/services/register.rs
@@ -166,8 +166,13 @@
166166 );
167167 let locale = self.mail_locale(None, accept_language).await;
168168 let purpose = i18n::catalog::verify_subject(locale, i18n::VerifyPurpose::Signup);
169 let html =
170 crate::templates::mail_verify_code(&artifacts.code, minutes, purpose, link.as_deref(), locale);
169 let html = crate::templates::mail_verify_code(
170 &artifacts.code,
171 minutes,
172 purpose,
173 link.as_deref(),
174 locale,
175 );
171176 let text = i18n::catalog::verify_text(locale, &artifacts.code);
172177 if let Err(error) = self
173178 .send_email(&email, purpose, &text, html.0.as_str())
Modified lib/auth/src/services/two_factor.rs View lib/auth/src/services/two_factor.rs
@@ -53,7 +53,7 @@
5353}
5454
5555async fn insert_recovery_codes(
56 txn: &(impl sea_orm::ConnectionTrait + Sync),
56 txn: &impl sea_orm::ConnectionTrait,
5757 uid: Uuid,
5858 codes: &[String],
5959 now: OffsetDateTime,
Modified lib/auth/src/templates/page.rs View lib/auth/src/templates/page.rs
@@ -1,6 +1,6 @@
11use axum::response::Html;
2use i18n::{UiLocale, catalog};
23use maud::html;
3use i18n::{UiLocale, catalog};
44
55use crate::share::types::AgreementDoc;
66
Modified lib/axum-session/src/sec/encrypt.rs View lib/axum-session/src/sec/encrypt.rs
@@ -1,7 +1,9 @@
11use crate::SessionError;
2use aes_gcm::aead::common::array::Array;
3use aes_gcm::aead::{Aead, AeadInPlace, KeyInit, Payload};
4use aes_gcm::Aes256Gcm;
2use aes_gcm::aead::array::typenum::consts::{U12, U16};
3use aes_gcm::aead::array::Array;
4use aes_gcm::aead::inout::InOutBuf;
5use aes_gcm::aead::{AeadInOut, KeyInit};
6use aes_gcm::{Aes256Gcm, Nonce};
57use base64::{engine::general_purpose, Engine as _};
68use cookie::Key;
79use rand::TryRng;
@@ -10,6 +12,17 @@
1012pub(crate) const TAG_LEN: usize = 16;
1113pub(crate) const KEY_LEN: usize = 32;
1214
15type GcmNonce = Nonce<U12>;
16type GcmTag = Array<u8, U16>;
17
18fn cipher(key: &Key) -> Result<Aes256Gcm, &'static str> {
19 Aes256Gcm::new_from_slice(key.encryption()).map_err(|_| "encryption failure!")
20}
21
22fn bad_seal(error: impl std::fmt::Display) -> SessionError {
23 SessionError::GenericNotSupportedError(format!("invalid key/nonce/value: {error}"))
24}
25
1326///Used to encrypt the database Values
1427pub(crate) fn encrypt(name: &str, value: &str, key: &Key) -> Result<String, &'static str> {
1528 let val = value.as_bytes();
@@ -23,43 +36,78 @@
2336 rng.try_fill_bytes(nonce)
2437 .map_err(|_| "couldn't random fill nonce")?;
2538
26 let nonce = Array::clone_from_slice(nonce);
39 let nonce = GcmNonce::try_from(&*nonce).map_err(|_| "invalid nonce length")?;
2740
2841 // Use the UUID to preform actual cookie Sealing.
2942 let aad = name.as_bytes();
30 let aead =
31 Aes256Gcm::new(&Array::try_from(key.encryption()).map_err(|_| "encryption failure!")?);
32 let aad_tag = aead
33 .encrypt_in_place_detached(&nonce, aad, in_out)
43 let tag_out = cipher(key)?
44 .encrypt_inout_detached(&nonce, aad, InOutBuf::from(in_out))
3445 .map_err(|_| "encryption failure!")?;
3546
36 tag.copy_from_slice(aad_tag.as_slice());
47 tag.copy_from_slice(tag_out.as_slice());
3748
3849 Ok(general_purpose::STANDARD.encode(&data))
3950}
4051
4152///Used to decrypt the database Values.
4253pub(crate) fn decrypt(name: &str, value: &str, key: &Key) -> Result<String, SessionError> {
43 let data = general_purpose::STANDARD.decode(value)?;
44 if data.len() <= NONCE_LEN {
54 let mut data = general_purpose::STANDARD.decode(value)?;
55 if data.len() <= NONCE_LEN + TAG_LEN {
4556 return Err(SessionError::GenericNotSupportedError(
46 "length of decoded data is <= NONCE_LEN".to_owned(),
57 "length of decoded data is <= NONCE_LEN + TAG_LEN".to_owned(),
4758 ));
4859 }
4960
50 let (nonce, cipher) = data.split_at(NONCE_LEN);
51 let payload = Payload {
52 msg: cipher,
53 aad: name.as_bytes(),
54 };
61 let (nonce, ciphertext) = data.split_at_mut(NONCE_LEN);
62 let nonce = GcmNonce::try_from(&*nonce).map_err(bad_seal)?;
63 let (msg, tag) = ciphertext.split_at_mut(ciphertext.len() - TAG_LEN);
64 let tag = GcmTag::try_from(&*tag).map_err(bad_seal)?;
65
66 cipher(key)
67 .map_err(|error| SessionError::GenericNotSupportedError(error.to_string()))?
68 // `InOutBuf` borrows the buffer mutably for the call only; the plaintext
69 // is readable again afterwards because the borrow ends here.
70 .decrypt_inout_detached(&nonce, name.as_bytes(), InOutBuf::from(&mut *msg), &tag)
71 .map_err(bad_seal)?;
72
73 Ok(String::from_utf8(msg.to_vec())?)
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 fn key() -> Key {
81 Key::from(&[7u8; 64])
82 }
83
84 #[test]
85 fn round_trips_and_binds_the_name_as_aad() {
86 let sealed = encrypt("cookie:a", "hello world", &key()).expect("encrypt");
87 assert_eq!(
88 decrypt("cookie:a", &sealed, &key()).expect("decrypt"),
89 "hello world"
90 );
91 // The associated data is authenticated, so a different name cannot decrypt.
92 assert!(decrypt("cookie:b", &sealed, &key()).is_err());
93 }
94
95 #[test]
96 fn rejects_truncated_or_tampered_payloads() {
97 assert!(decrypt("n", "AAAA", &key()).is_err());
98
99 let sealed = encrypt("n", "value", &key()).expect("encrypt");
100 let mut raw = general_purpose::STANDARD.decode(&sealed).expect("base64");
101 let last = raw.len() - 1;
102 raw[last] ^= 0xff;
103 let tampered = general_purpose::STANDARD.encode(&raw);
104 assert!(decrypt("n", &tampered, &key()).is_err());
105 }
55106
56 let aead = Aes256Gcm::new(Array::from_slice(key.encryption()));
57 Ok(String::from_utf8(
58 aead.decrypt(Array::from_slice(nonce), payload)
59 .map_err(|_| {
60 SessionError::GenericNotSupportedError(
61 "invalid key/nonce/value: bad seal".to_owned(),
62 )
63 })?,
64 )?)
107 #[test]
108 fn ciphertext_layout_is_nonce_msg_tag() {
109 let sealed = encrypt("n", "abcd", &key()).expect("encrypt");
110 let raw = general_purpose::STANDARD.decode(&sealed).expect("base64");
111 assert_eq!(raw.len(), NONCE_LEN + 4 + TAG_LEN);
112 }
65113}
Modified lib/axum-session/src/headers.rs View lib/axum-session/src/headers.rs
@@ -278,6 +278,9 @@
278278}
279279
280280/// Used to Set either the Header Values or the Cookie Values.
281///
282/// `longterm` only feeds the cookie encoding, which `rest_mode` compiles out.
283#[cfg_attr(feature = "rest_mode", allow(unused_variables))]
281284pub(crate) async fn set_headers<T>(
282285 session: &Session<T>,
283286 headers: &mut HeaderMap,
Modified lib/chat/src/handlers/voice.rs View lib/chat/src/handlers/voice.rs
@@ -490,12 +490,10 @@
490490 "participant_left" => {
491491 if let Some(participant) = event.participant.as_ref()
492492 && let Some(uid) = crate::services::voice::identity_to_uid(&participant.identity)
493 && let Err(error) =
494 crate::services::voice::remove_member(&service, room_id, uid).await
493495 {
494 if let Err(error) =
495 crate::services::voice::remove_member(&service, room_id, uid).await
496 {
497 tracing::warn!(%error, %room_id, "voice member remove failed");
498 }
496 tracing::warn!(%error, %room_id, "voice member remove failed");
499497 }
500498 service
501499 .realtime
Modified lib/chat/src/services/rooms.rs View lib/chat/src/services/rooms.rs
@@ -349,12 +349,12 @@
349349 if ctx.space.is_none() && (cmd.name.is_some() || cmd.is_private.is_some()) {
350350 return Err(ChatError::Forbidden);
351351 }
352 if let Some(rate) = cmd.rate_limit_per_user {
353 if !(0..=3600).contains(&rate) {
354 return Err(ChatError::BadRequest(
355 "rate_limit_per_user must be within 0..=3600".to_string(),
356 ));
357 }
352 if let Some(rate) = cmd.rate_limit_per_user
353 && !(0..=3600).contains(&rate)
354 {
355 return Err(ChatError::BadRequest(
356 "rate_limit_per_user must be within 0..=3600".to_string(),
357 ));
358358 }
359359
360360 let now = OffsetDateTime::now_utc();
Modified lib/chat/src/services/unfurl.rs View lib/chat/src/services/unfurl.rs
@@ -70,7 +70,7 @@
7070 if cleaned.len() < 12 || cleaned.len() > 2048 {
7171 continue;
7272 }
73 if !urls.iter().any(|u: &String| *u == cleaned) {
73 if !urls.contains(&cleaned) {
7474 urls.push(cleaned);
7575 }
7676 if urls.len() >= MAX_URLS_PER_MESSAGE {
@@ -473,9 +473,8 @@
473473 }
474474 let mut embeds = Vec::new();
475475 for url in urls {
476 match self.resolve_link(&url).await {
477 Some(value) => embeds.push(value),
478 None => {}
476 if let Some(value) = self.resolve_link(&url).await {
477 embeds.push(value)
479478 }
480479 }
481480 if embeds.is_empty() {
Modified lib/chat/src/services/webhooks.rs View lib/chat/src/services/webhooks.rs
@@ -382,16 +382,15 @@
382382 }
383383 .insert(txn)
384384 .await?;
385 if let Some(tid) = thread_id {
386 if let Some(t) = models::chat::threads::Entity::find_by_id(tid)
385 if let Some(tid) = thread_id
386 && let Some(t) = models::chat::threads::Entity::find_by_id(tid)
387387 .one(txn)
388388 .await?
389 {
390 crate::services::threads::maintain_thread_on_reply(
391 txn, &t, message_id, owner, now,
392 )
393 .await?;
394 }
389 {
390 crate::services::threads::maintain_thread_on_reply(
391 txn, &t, message_id, owner, now,
392 )
393 .await?;
395394 }
396395 rooms::Entity::update_many()
397396 .filter(rooms::Column::Id.eq(room_id))
Modified lib/ci/src/handlers/runner_protocol.rs View lib/ci/src/handlers/runner_protocol.rs
@@ -83,10 +83,10 @@
8383 let labels = service.runner_labels(auth.runner_id).await?;
8484 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(wait.max(1));
8585 loop {
86 if let Some(job_id) = service.claim_job(auth.runner_id, &labels).await? {
87 if let Some(payload) = service.claim_payload(job_id, auth.runner_id).await? {
88 return Ok(AppResponseData::ok(ClaimEnvelope { job: Some(payload) }));
89 }
86 if let Some(job_id) = service.claim_job(auth.runner_id, &labels).await?
87 && let Some(payload) = service.claim_payload(job_id, auth.runner_id).await?
88 {
89 return Ok(AppResponseData::ok(ClaimEnvelope { job: Some(payload) }));
9090 }
9191 if std::time::Instant::now() >= deadline {
9292 return Ok(AppResponseData::ok(ClaimEnvelope { job: None }));
Modified lib/ci/src/logs.rs View lib/ci/src/logs.rs
@@ -78,7 +78,7 @@
7878 after_seq: i32,
7979 limit: u64,
8080 ) -> CiResult<LogPageDto> {
81 let limit = limit.clamp(1, 100) as u64;
81 let limit = limit.clamp(1, 100);
8282 let rows = ci_log_chunk::Entity::find()
8383 .filter(ci_log_chunk::Column::JobId.eq(job_id))
8484 .filter(ci_log_chunk::Column::Seq.gt(after_seq))
Modified lib/config/src/auth.rs View lib/config/src/auth.rs
@@ -169,12 +169,11 @@
169169 );
170170 }
171171 }
172 if let Some(key) = &self.chat_jwt_sec {
173 if !key.is_empty() && key.as_bytes().len() < 32 {
174 return Err(
175 "auth.chat_jwt_sec must be at least 32 bytes when configured".to_string(),
176 );
177 }
172 if let Some(key) = &self.chat_jwt_sec
173 && !key.is_empty()
174 && key.len() < 32
175 {
176 return Err("auth.chat_jwt_sec must be at least 32 bytes when configured".to_string());
178177 }
179178 Ok(())
180179 }
Modified lib/config/src/webhook.rs View lib/config/src/webhook.rs
@@ -4,22 +4,12 @@
44
55#[derive(Clone, Debug, Deserialize)]
66#[serde(default)]
7#[derive(Default)]
78pub struct WebhookConfig {
89 pub enabled: Option<bool>,
910 pub bind_addr: Option<String>,
1011 pub port: Option<u16>,
1112 pub public_base_url: String,
12}
13
14impl Default for WebhookConfig {
15 fn default() -> Self {
16 Self {
17 enabled: None,
18 bind_addr: None,
19 port: None,
20 public_base_url: String::new(),
21 }
22 }
2313}
2414
2515impl WebhookConfig {
Modified lib/email/src/templates/email_change_confirm.rs View lib/email/src/templates/email_change_confirm.rs
@@ -1,6 +1,6 @@
1use i18n::{UiLocale, catalog};
12use maud::Markup;
23use maud::html;
3use i18n::{UiLocale, catalog};
44
55use super::{append_query, mail_shell, url_join};
66
Modified lib/email/src/templates/reset_link.rs View lib/email/src/templates/reset_link.rs
@@ -1,6 +1,6 @@
1use i18n::{UiLocale, catalog};
12use maud::Markup;
23use maud::html;
3use i18n::{UiLocale, catalog};
44
55use super::{append_query, mail_shell, url_join};
66
Modified lib/email/src/templates/verify_code.rs View lib/email/src/templates/verify_code.rs
@@ -1,6 +1,6 @@
1use i18n::{UiLocale, catalog};
12use maud::Markup;
23use maud::html;
3use i18n::{UiLocale, catalog};
44
55use super::{append_query, mail_shell, url_join};
66
Modified lib/email/src/queue.rs View lib/email/src/queue.rs
@@ -189,9 +189,13 @@
189189 pub raw: String,
190190}
191191
192/// The join handle of a running subscriber loop, shared so `shutdown` can await
193/// it from outside the task.
194type SharedJoinHandle = Arc<tokio::sync::Mutex<JoinHandle<Result<(), EmailError>>>>;
195
192196pub struct SubscribeHandle<T> {
193197 cancel: CancellationToken,
194 join: Option<Arc<tokio::sync::Mutex<JoinHandle<Result<(), EmailError>>>>>,
198 join: Option<SharedJoinHandle>,
195199 _marker: PhantomData<T>,
196200}
197201
Modified lib/email/src/service.rs View lib/email/src/service.rs
@@ -85,10 +85,10 @@
8585 metadata: req.metadata,
8686 };
8787
88 if let Some(q) = &self.inner.queue {
89 if let Err(err) = q.publish_to(CHANNEL_SENT, &sent).await {
90 warn!(error = %err, "failed to publish EmailSent event");
91 }
88 if let Some(q) = &self.inner.queue
89 && let Err(err) = q.publish_to(CHANNEL_SENT, &sent).await
90 {
91 warn!(error = %err, "failed to publish EmailSent event");
9292 }
9393 info!(
9494 request_id = %sent.request_id,
@@ -191,10 +191,10 @@
191191 failed_at_ms: now_ms(),
192192 metadata: Default::default(),
193193 };
194 if let Some(q) = &self.inner.queue {
195 if let Err(err) = q.publish_to(CHANNEL_FAILED, &failed).await {
196 warn!(error = %err, "failed to publish EmailFailed event");
197 }
194 if let Some(q) = &self.inner.queue
195 && let Err(err) = q.publish_to(CHANNEL_FAILED, &failed).await
196 {
197 warn!(error = %err, "failed to publish EmailFailed event");
198198 }
199199 }
200200 }
@@ -227,7 +227,10 @@
227227 .as_deref()
228228 .and_then(i18n::UiLocale::parse)
229229 .unwrap_or(i18n::UiLocale::En);
230 object.insert("locale".to_string(), serde_json::Value::String(tag.tag().to_string()));
230 object.insert(
231 "locale".to_string(),
232 serde_json::Value::String(tag.tag().to_string()),
233 );
231234 }
232235 let html = self.inner.templates.render(&req.template, &vars).await?;
233236 debug!(
Modified lib/email/src/subscriber.rs View lib/email/src/subscriber.rs
@@ -30,20 +30,20 @@
3030 let b = self.batch;
3131 let r = self.render;
3232 let mut last_err: Option<EmailError> = None;
33 if let Some(h) = s {
34 if let Err(e) = h.shutdown(timeout).await {
35 last_err = Some(e);
36 }
33 if let Some(h) = s
34 && let Err(e) = h.shutdown(timeout).await
35 {
36 last_err = Some(e);
3737 }
38 if let Some(h) = b {
39 if let Err(e) = h.shutdown(timeout).await {
40 last_err = Some(e);
41 }
38 if let Some(h) = b
39 && let Err(e) = h.shutdown(timeout).await
40 {
41 last_err = Some(e);
4242 }
43 if let Some(h) = r {
44 if let Err(e) = h.shutdown(timeout).await {
45 last_err = Some(e);
46 }
43 if let Some(h) = r
44 && let Err(e) = h.shutdown(timeout).await
45 {
46 last_err = Some(e);
4747 }
4848 match last_err {
4949 Some(e) => Err(e),
Modified lib/export/src/serde_datetime.rs View lib/export/src/serde_datetime.rs
@@ -32,9 +32,7 @@
3232}
3333
3434/// Deserialize `Option<OffsetDateTime>`, tolerating both shapes plus `null`.
35pub fn deserialize_opt_datetime<'de, D>(
36 deserializer: D,
37) -> Result<Option<OffsetDateTime>, D::Error>
35pub fn deserialize_opt_datetime<'de, D>(deserializer: D) -> Result<Option<OffsetDateTime>, D::Error>
3836where
3937 D: Deserializer<'de>,
4038{
Modified lib/export/src/serde_patch.rs View lib/export/src/serde_patch.rs
@@ -44,4 +44,4 @@
4444 fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Self::Value, D::Error> {
4545 T::deserialize(deserializer).map(|value| Some(Some(value)))
4646 }
47}
No newline at end of file
47}
Modified lib/git/src/handler/html/mod.rs View lib/git/src/handler/html/mod.rs
@@ -124,6 +124,10 @@
124124 pub repo_name: String,
125125}
126126
127/// The error arm is a ready-made response, not a value: every one of the twenty
128/// page fragments either renders or immediately returns it unchanged. Boxing it
129/// would only add a dereference at each call site, so the lint is allowed here.
130#[allow(clippy::result_large_err)]
127131pub async fn resolve(
128132 service: GitService,
129133 path: HtmlRepoPath,
@@ -178,6 +182,8 @@
178182 pub clean_path: String,
179183}
180184
185/// See [`resolve`]: the error arm is the response itself.
186#[allow(clippy::result_large_err)]
181187pub async fn resolve_view(
182188 service: GitService,
183189 path: HtmlRepoPath,
@@ -292,7 +298,7 @@
292298}
293299
294300pub fn unix_to_time(unix: i64) -> OffsetDateTime {
295 OffsetDateTime::from_unix_timestamp(unix).unwrap_or_else(|_| OffsetDateTime::UNIX_EPOCH)
301 OffsetDateTime::from_unix_timestamp(unix).unwrap_or(OffsetDateTime::UNIX_EPOCH)
296302}
297303
298304pub fn relative_time_unix(unix: i64) -> String {
Modified lib/git/src/service/ai_review.rs View lib/git/src/service/ai_review.rs
@@ -66,6 +66,15 @@
6666 pub model: String,
6767}
6868
69/// One commit-status write for the `ai-review` context. Grouped so the four
70/// string-ish arguments cannot be passed in the wrong order.
71pub struct AiReviewStatus {
72 pub sha: String,
73 pub state: crate::service::commits::CommitStatusDtoState,
74 pub description: String,
75 pub target_url: Option<String>,
76}
77
6978impl GitService {
7079 /// 触发(或重跑)某个提交的 AI 评审。
7180 ///
@@ -114,10 +123,12 @@
114123 space_name,
115124 repo_name,
116125 creator,
117 &sha,
118 crate::service::commits::CommitStatusDtoState::Pending,
119 "AI 评审进行中",
120 None,
126 AiReviewStatus {
127 sha,
128 state: crate::service::commits::CommitStatusDtoState::Pending,
129 description: "AI 评审进行中".to_string(),
130 target_url: None,
131 },
121132 )
122133 .await?;
123134
@@ -134,20 +145,23 @@
134145 space_name: String,
135146 repo_name: String,
136147 creator: Uuid,
137 sha: &str,
138 state: crate::service::commits::CommitStatusDtoState,
139 description: &str,
140 target_url: Option<String>,
148 status: AiReviewStatus,
141149 ) -> Result<(), GitError> {
150 let AiReviewStatus {
151 sha,
152 state,
153 description,
154 target_url,
155 } = status;
142156 self.set_commit_status(
143157 space_name,
144158 repo_name,
145159 creator,
146160 crate::service::commits::SetCommitStatusCmd {
147 sha: sha.to_string(),
161 sha,
148162 state,
149163 context: AI_REVIEW_CONTEXT.to_string(),
150 description: Some(description.to_string()),
164 description: Some(description),
151165 target_url,
152166 },
153167 )
Modified lib/git/src/service/archives.rs View lib/git/src/service/archives.rs
@@ -1,4 +1,4 @@
1use std::path::PathBuf;
1use std::path::Path;
22
33use export::storage::AppStorage;
44use gitbaby::archive::types::{ArchiveFormat, CreateArchiveRequest};
@@ -168,7 +168,7 @@
168168}
169169
170170async fn read_archive_bytes(
171 dir: &PathBuf,
171 dir: &Path,
172172 rev: &str,
173173 ext: &str,
174174 timeout_secs: u64,
Modified lib/git/src/service/protection.rs View lib/git/src/service/protection.rs
@@ -312,7 +312,6 @@
312312 required_status_contexts: Set(flags.required_status_contexts.unwrap_or_default()),
313313 created_at: Set(now),
314314 updated_at: Set(now),
315 ..Default::default()
316315 };
317316 let created = match row.insert(self.db_write()).await {
318317 Ok(created) => created,
@@ -518,7 +517,6 @@
518517 name_pattern: Set(cmd.name_pattern.trim().to_string()),
519518 created_at: Set(now),
520519 updated_at: Set(now),
521 ..Default::default()
522520 };
523521 let created = match row.insert(self.db_write()).await {
524522 Ok(created) => created,
Modified lib/git/src/share/git_env.rs View lib/git/src/share/git_env.rs
@@ -59,7 +59,10 @@
5959 std::env::set_var(format!("GIT_CONFIG_VALUE_{count}"), "off");
6060 std::env::set_var("GIT_CONFIG_COUNT", (count + 1).to_string());
6161 }
62 tracing::debug!(slot = key_slot.as_str(), "forced core.quotepath=off for git children");
62 tracing::debug!(
63 slot = key_slot.as_str(),
64 "forced core.quotepath=off for git children"
65 );
6366 });
6467}
6568
Modified lib/git/src/share/policy.rs View lib/git/src/share/policy.rs
@@ -73,14 +73,13 @@
7373impl WhitelistSet {
7474 pub fn push_actor_allowed(&self, subject: WhitelistSubject) -> bool {
7575 match subject {
76 WhitelistSubject::User(uid) => self.push_allowed.iter().any(|entry| *entry == uid),
76 WhitelistSubject::User(uid) => self.push_allowed.contains(&uid),
7777 WhitelistSubject::Group(_) => false,
7878 }
7979 }
8080
8181 pub fn tag_modifier_allowed(&self, uid: Uuid) -> bool {
82 self.merge_allowed.iter().any(|entry| *entry == uid)
83 || self.approval_allowed.iter().any(|entry| *entry == uid)
82 self.merge_allowed.contains(&uid) || self.approval_allowed.contains(&uid)
8483 }
8584}
8685
Modified lib/git/src/share/validate.rs View lib/git/src/share/validate.rs
@@ -201,10 +201,10 @@
201201 if host.ends_with(".local") || host.ends_with(".internal") {
202202 return true;
203203 }
204 if let Some(mapped) = host.strip_prefix("::ffff:") {
205 if parse_ipv4(mapped).is_some() {
206 return true;
207 }
204 if let Some(mapped) = host.strip_prefix("::ffff:")
205 && parse_ipv4(mapped).is_some()
206 {
207 return true;
208208 }
209209 if let Some(segments) = parse_ipv4(host) {
210210 if segments[0] == 127
Modified lib/git/src/smart/dispatch.rs View lib/git/src/smart/dispatch.rs
@@ -121,23 +121,23 @@
121121 }
122122 let timeout = service.git.state.config.git.git_rpc_timeout_secs;
123123 let stream = rpc::run_rpc_stream(&bare, git_service, false, input, timeout).await?;
124 let post = (
124 let post = PostReceiveStream {
125125 is_receive,
126 service.queue.clone(),
127 service.sync_settings.clone(),
128 service.ci_event_topic().map(|t| t.to_string()),
126 queue: service.queue.clone(),
127 settings: service.sync_settings.clone(),
128 ci_topic: service.ci_event_topic().map(|t| t.to_string()),
129129 space_id,
130 ctx.model.id,
130 repo_id: ctx.model.id,
131131 actor_id,
132 bare.clone(),
133 service
132 bare: bare.clone(),
133 ai_ctx: service
134134 .ai_config()
135135 .map(|cfg| crate::sync::post_receive::AiPushCtx {
136136 db: service.git.state.database.clone(),
137137 redis: service.git.state.redis.clone(),
138138 config: cfg.clone(),
139139 }),
140 );
140 };
141141 let stream = stream.map(|chunk| chunk.map_err(|e| axum::Error::new(e.to_string())));
142142 let stream = body_stream_with_post_receive(stream, post);
143143 Ok::<_, SmartError>(
@@ -178,34 +178,50 @@
178178 }
179179}
180180
181/// Everything the post-receive hook needs once the response body has been
182/// streamed through. A named struct rather than a nine-element tuple: the fields
183/// are all `Uuid`-shaped, so positional access silently swaps tenant and repo.
184pub struct PostReceiveStream {
185 pub is_receive: bool,
186 pub queue: Option<Arc<crate::sync::queue::SyncQueue>>,
187 pub settings: config::git::SyncSettings,
188 pub ci_topic: Option<String>,
189 pub space_id: Uuid,
190 pub repo_id: Uuid,
191 pub actor_id: Option<Uuid>,
192 pub bare: std::path::PathBuf,
193 pub ai_ctx: Option<crate::sync::post_receive::AiPushCtx>,
194}
195
181196fn body_stream_with_post_receive(
182197 stream: impl futures::Stream<Item = Result<Bytes, axum::Error>> + Send + 'static,
183 post: (
184 bool,
185 Option<Arc<crate::sync::queue::SyncQueue>>,
186 config::git::SyncSettings,
187 Option<String>,
188 Uuid,
189 Uuid,
190 Option<Uuid>,
191 std::path::PathBuf,
192 Option<crate::sync::post_receive::AiPushCtx>,
193 ),
198 post: PostReceiveStream,
194199) -> impl futures::Stream<Item = Result<Bytes, axum::Error>> + Send + 'static {
195200 use futures::{StreamExt, stream};
196201 stream.chain(stream::once(async move {
197 let (is_receive, queue, settings, ci_topic, space_id, repo_id, actor_id, bare, ai_ctx) =
198 post;
202 let PostReceiveStream {
203 is_receive,
204 queue,
205 settings,
206 ci_topic,
207 space_id,
208 repo_id,
209 actor_id,
210 bare,
211 ai_ctx,
212 } = post;
199213 if is_receive && let Some(queue) = queue {
200214 enqueue_post_receive_push_with_ci_logged(
201215 &queue,
202216 &settings,
203217 ci_topic.as_deref(),
204218 ai_ctx,
205 space_id,
206 repo_id,
207 actor_id,
208 &bare,
219 &crate::sync::PushTarget {
220 space_id,
221 repo_id,
222 actor_id,
223 bare: &bare,
224 },
209225 )
210226 .await;
211227 }
Modified lib/git/src/smart/pre_receive.rs View lib/git/src/smart/pre_receive.rs
@@ -27,6 +27,23 @@
2727 pub per_ref: Vec<(RefUpdate, RefVerdict)>,
2828}
2929
30/// Everything the per-ref judges need, loaded once per push. The two policy
31/// lists and their whitelists always travel together, so they are passed as one
32/// value instead of four parallel arguments.
33struct ProtectionRules<'a> {
34 branch_policies: &'a [models::repo::repo_branch_protection_policy::Model],
35 tag_policies: &'a [models::repo::repo_tag_protection_policy::Model],
36 branch_whitelists: &'a WhitelistSet,
37 tag_whitelists: &'a WhitelistSet,
38}
39
40/// Who is pushing and how they are privileged on this repository.
41#[derive(Debug, Clone, Copy)]
42struct PushActor {
43 user_id: Uuid,
44 is_admin: bool,
45}
46
3047impl PreReceiveVerdict {
3148 pub fn accepted_only(self) -> SmartResult<Vec<RefUpdate>> {
3249 let mut accepted = Vec::with_capacity(self.per_ref.len());
@@ -96,20 +113,20 @@
96113 let oid_len = oid_len_of(refs);
97114 let zero_oid = "0".repeat(oid_len);
98115
116 let rules = ProtectionRules {
117 branch_policies: &branch_policies,
118 tag_policies: &tag_policies,
119 branch_whitelists: &branch_whitelists,
120 tag_whitelists: &tag_whitelists,
121 };
122 let actor = PushActor {
123 user_id: actor_user_id,
124 is_admin: actor_is_admin,
125 };
126
99127 let mut out = Vec::with_capacity(refs.len());
100128 for r in refs {
101 let verdict = judge(
102 baby,
103 r,
104 &branch_policies,
105 &tag_policies,
106 actor_user_id,
107 actor_is_admin,
108 &branch_whitelists,
109 &tag_whitelists,
110 &zero_oid,
111 )
112 .await?;
129 let verdict = judge(baby, r, &rules, actor, &zero_oid).await?;
113130 match &verdict {
114131 RefVerdict::Accept => {
115132 tracing::debug!(refname = %r.refname, "pre-receive ref accepted");
@@ -126,35 +143,15 @@
126143async fn judge(
127144 baby: &gitbaby::GitBaby,
128145 r: &RefUpdate,
129 branch_policies: &[models::repo::repo_branch_protection_policy::Model],
130 tag_policies: &[models::repo::repo_tag_protection_policy::Model],
131 actor_user_id: Uuid,
132 actor_is_admin: bool,
133 branch_whitelists: &WhitelistSet,
134 tag_whitelists: &WhitelistSet,
146 rules: &ProtectionRules<'_>,
147 actor: PushActor,
135148 zero_oid: &str,
136149) -> SmartResult<RefVerdict> {
137150 if let Some(branch) = r.refname.strip_prefix("refs/heads/") {
138 return judge_branch(
139 baby,
140 branch,
141 r,
142 branch_policies,
143 actor_user_id,
144 actor_is_admin,
145 branch_whitelists,
146 zero_oid,
147 )
148 .await;
151 return judge_branch(baby, branch, r, rules, actor, zero_oid).await;
149152 }
150153 if let Some(tag) = r.refname.strip_prefix("refs/tags/") {
151 return Ok(judge_tag(
152 tag,
153 tag_policies,
154 actor_user_id,
155 actor_is_admin,
156 tag_whitelists,
157 ));
154 return Ok(judge_tag(tag, rules, actor));
158155 }
159156 Ok(RefVerdict::Accept)
160157}
@@ -163,16 +160,15 @@
163160 baby: &gitbaby::GitBaby,
164161 branch: &str,
165162 r: &RefUpdate,
166 branch_policies: &[models::repo::repo_branch_protection_policy::Model],
167 actor_user_id: Uuid,
168 actor_is_admin: bool,
169 whitelists: &WhitelistSet,
163 rules: &ProtectionRules<'_>,
164 actor: PushActor,
170165 zero_oid: &str,
171166) -> SmartResult<RefVerdict> {
172167 let matchers: Vec<(
173168 &models::repo::repo_branch_protection_policy::Model,
174169 GlobMatcher,
175 )> = match branch_policies
170 )> = match rules
171 .branch_policies
176172 .iter()
177173 .filter_map(|m| {
178174 Glob::new(&m.branch_pattern)
@@ -201,7 +197,9 @@
201197
202198 if !policy.enable_push {
203199 if policy.enable_push_whitelist
204 && whitelists.push_actor_allowed(WhitelistSubject::User(actor_user_id))
200 && rules
201 .branch_whitelists
202 .push_actor_allowed(WhitelistSubject::User(actor.user_id))
205203 {
206204 return Ok(RefVerdict::Accept);
207205 }
@@ -210,8 +208,11 @@
210208 )));
211209 }
212210
213 if is_force && !policy.enable_force_push && !actor_is_admin {
214 if whitelists.push_actor_allowed(WhitelistSubject::User(actor_user_id)) {
211 if is_force && !policy.enable_force_push && !actor.is_admin {
212 if rules
213 .branch_whitelists
214 .push_actor_allowed(WhitelistSubject::User(actor.user_id))
215 {
215216 return Ok(RefVerdict::Accept);
216217 }
217218 return Ok(RefVerdict::Reject(format!(
@@ -222,21 +223,15 @@
222223 Ok(RefVerdict::Accept)
223224}
224225
225fn judge_tag(
226 tag: &str,
227 tag_policies: &[models::repo::repo_tag_protection_policy::Model],
228 actor_user_id: Uuid,
229 actor_is_admin: bool,
230 whitelists: &WhitelistSet,
231) -> RefVerdict {
232 for p in tag_policies {
226fn judge_tag(tag: &str, rules: &ProtectionRules<'_>, actor: PushActor) -> RefVerdict {
227 for p in rules.tag_policies {
233228 let Ok(glob) = Glob::new(&p.name_pattern) else {
234229 continue;
235230 };
236231 if !glob.compile_matcher().is_match(tag) {
237232 continue;
238233 }
239 if actor_is_admin || whitelists.tag_modifier_allowed(actor_user_id) {
234 if actor.is_admin || rules.tag_whitelists.tag_modifier_allowed(actor.user_id) {
240235 return RefVerdict::Accept;
241236 }
242237 return RefVerdict::Reject(format!(
Modified lib/git/src/ssh/handler/channel.rs View lib/git/src/ssh/handler/channel.rs
@@ -331,10 +331,12 @@
331331 &sync_settings,
332332 ci_topic.as_deref(),
333333 ai_ctx,
334 space_id,
335 repo_id,
336 actor_id,
337 &bare,
334 &crate::sync::PushTarget {
335 space_id,
336 repo_id,
337 actor_id,
338 bare: &bare,
339 },
338340 )
339341 .await;
340342 }
@@ -594,11 +596,11 @@
594596 let child_arc = child_arc.clone();
595597 let input = input.to_vec();
596598 tokio::spawn(async move {
597 if stdin.write_all(&input).await.is_err() {
598 if let Some(mut ch) = child_arc.lock().await.take() {
599 let _ = ch.start_kill();
600 let _ = ch.wait().await;
601 }
599 if stdin.write_all(&input).await.is_err()
600 && let Some(mut ch) = child_arc.lock().await.take()
601 {
602 let _ = ch.start_kill();
603 let _ = ch.wait().await;
602604 }
603605 let _ = stdin.shutdown().await;
604606 });
@@ -680,22 +682,22 @@
680682 stderr_bytes = stderr_buf.len(),
681683 "git process for ssh operation finished"
682684 );
683 if !stdout_buf.is_empty() {
684 if let Err(e) = handle.data(channel, Bytes::from(stdout_buf.clone())).await {
685 tracing::warn!(
686 ?e,
687 bytes = stdout_buf.len(),
688 "ssh: failed to send git stdout to client"
689 );
690 }
685 if !stdout_buf.is_empty()
686 && let Err(e) = handle.data(channel, Bytes::from(stdout_buf.clone())).await
687 {
688 tracing::warn!(
689 ?e,
690 bytes = stdout_buf.len(),
691 "ssh: failed to send git stdout to client"
692 );
691693 }
692 if !stderr_buf.is_empty() && !status.success() {
693 if let Err(e) = handle
694 if !stderr_buf.is_empty()
695 && !status.success()
696 && let Err(e) = handle
694697 .extended_data(channel, 1, Bytes::from(stderr_buf.clone()))
695698 .await
696 {
697 tracing::warn!(?e, "ssh: failed to send git stderr to client");
698 }
699 {
700 tracing::warn!(?e, "ssh: failed to send git stderr to client");
699701 }
700702 if status.success() {
701703 Ok(())
Modified lib/git/src/ssh/handler/command.rs View lib/git/src/ssh/handler/command.rs
@@ -237,8 +237,8 @@
237237 cmd.stdout(std::process::Stdio::piped())
238238 .stderr(std::process::Stdio::piped());
239239 let mut child = cmd.spawn().map_err(SshError::Io)?;
240 let stderr_task = match child.stderr.take() {
241 Some(stderr) => Some(tokio::spawn(async move {
240 let stderr_task = child.stderr.take().map(|stderr| {
241 tokio::spawn(async move {
242242 use tokio::io::AsyncReadExt;
243243 let s = stderr;
244244 let mut buf = Vec::new();
@@ -247,9 +247,8 @@
247247 .read_to_end(&mut buf)
248248 .await;
249249 buf
250 })),
251 None => None,
252 };
250 })
251 });
253252 let stdout = child
254253 .stdout
255254 .take()
Modified lib/git/src/sync/ai_task.rs View lib/git/src/sync/ai_task.rs
@@ -109,14 +109,14 @@
109109 let conn = db.write();
110110 // 仓库级开关是这一层的准入条件:平台开着、仓库不想用,就不该产生调用。
111111 // 读一次仓库行,两个开关都从它取,避免每条引用各查一次。
112 let (review_on_push, summary_on_push) =
113 match repos::Entity::find_by_id(repo_id).one(conn).await {
114 Ok(Some(row)) => (row.ai_review_on_push, row.ai_summary_on_push),
115 // 仓库行读不到(极少见:推送与同步竞态)时保持加列之前的行为:
116 // 评审照旧按平台开关走,新增的自动总结不触发。丢掉一次自动评审比
117 // 多发一次模型调用更难排查,所以这里偏向沿用旧行为。
118 Ok(None) | Err(_) => (true, false),
119 };
112 let (review_on_push, summary_on_push) = match repos::Entity::find_by_id(repo_id).one(conn).await
113 {
114 Ok(Some(row)) => (row.ai_review_on_push, row.ai_summary_on_push),
115 // 仓库行读不到(极少见:推送与同步竞态)时保持加列之前的行为:
116 // 评审照旧按平台开关走,新增的自动总结不触发。丢掉一次自动评审比
117 // 多发一次模型调用更难排查,所以这里偏向沿用旧行为。
118 Ok(None) | Err(_) => (true, false),
119 };
120120 let switches = push_ai_switches(cfg, review_on_push, summary_on_push);
121121 if !switches.any() {
122122 return;
Modified lib/git/src/sync/mod.rs View lib/git/src/sync/mod.rs
@@ -27,7 +27,7 @@
2727};
2828pub use lock::{SyncLock, acquire_lock};
2929pub use post_receive::{
30 AiPushCtx, enqueue_post_receive_push, enqueue_post_receive_push_full,
30 AiPushCtx, PushTarget, enqueue_post_receive_push, enqueue_post_receive_push_full,
3131 enqueue_post_receive_push_with_ci_logged, prepare_post_receive,
3232};
3333pub use queue::SyncQueue;
Modified lib/git/src/sync/post_receive.rs View lib/git/src/sync/post_receive.rs
@@ -18,6 +18,16 @@
1818 pub config: config::ai::AiConfig,
1919}
2020
21/// Who and where a push belongs to. Grouped into a struct because every
22/// post-receive entry point threads the same four values, and three of them are
23/// `Uuid`-shaped — positional arguments silently swap space and repo.
24pub struct PushTarget<'a> {
25 pub space_id: Uuid,
26 pub repo_id: Uuid,
27 pub actor_id: Option<Uuid>,
28 pub bare: &'a Path,
29}
30
2131pub const POST_RECEIVE_HOOK: &str = "post-receive";
2232pub const CAPTURE_FILE: &str = "_gitdata_post_receive";
2333
@@ -145,15 +155,9 @@
145155pub async fn enqueue_post_receive_push(
146156 queue: &SyncQueue,
147157 settings: &SyncSettings,
148 channel_id: Uuid,
149 repo_id: Uuid,
150 actor_id: Option<Uuid>,
151 bare: &Path,
158 target: &PushTarget<'_>,
152159) -> Result<usize, EnqueueError> {
153 enqueue_push_impl(
154 queue, settings, None, None, channel_id, repo_id, actor_id, bare,
155 )
156 .await
160 enqueue_push_impl(queue, settings, None, None, target).await
157161}
158162
159163/// Enqueues the sync task and (optionally) publishes a CI push event from the
@@ -162,15 +166,9 @@
162166 queue: &SyncQueue,
163167 settings: &SyncSettings,
164168 ci_topic: Option<&str>,
165 channel_id: Uuid,
166 repo_id: Uuid,
167 actor_id: Option<Uuid>,
168 bare: &Path,
169 target: &PushTarget<'_>,
169170) -> Result<usize, EnqueueError> {
170 enqueue_push_impl(
171 queue, settings, ci_topic, None, channel_id, repo_id, actor_id, bare,
172 )
173 .await
171 enqueue_push_impl(queue, settings, ci_topic, None, target).await
174172}
175173
176174/// 推送入口:同时镜像 CI 事件、投递 AI 任务并排入同步队列。
@@ -179,22 +177,9 @@
179177 settings: &SyncSettings,
180178 ci_topic: Option<&str>,
181179 ai_ctx: Option<AiPushCtx>,
182 channel_id: Uuid,
183 repo_id: Uuid,
184 actor_id: Option<Uuid>,
185 bare: &Path,
180 target: &PushTarget<'_>,
186181) -> Result<usize, EnqueueError> {
187 enqueue_push_impl(
188 queue,
189 settings,
190 ci_topic,
191 ai_ctx.as_ref(),
192 channel_id,
193 repo_id,
194 actor_id,
195 bare,
196 )
197 .await
182 enqueue_push_impl(queue, settings, ci_topic, ai_ctx.as_ref(), target).await
198183}
199184
200185async fn enqueue_push_impl(
@@ -202,11 +187,11 @@
202187 settings: &SyncSettings,
203188 ci_topic: Option<&str>,
204189 ai_ctx: Option<&AiPushCtx>,
205 channel_id: Uuid,
206 repo_id: Uuid,
207 actor_id: Option<Uuid>,
208 bare: &Path,
190 target: &PushTarget<'_>,
209191) -> Result<usize, EnqueueError> {
192 // Copied out so the borrow of `target` ends here; the fields are `Copy`.
193 let (channel_id, repo_id, actor_id) = (target.space_id, target.repo_id, target.actor_id);
194 let bare = target.bare;
210195 let refs = take_post_receive_capture(bare);
211196 if refs.is_empty() {
212197 return Ok(0);
@@ -257,12 +242,10 @@
257242pub async fn enqueue_post_receive_push_logged(
258243 queue: &SyncQueue,
259244 settings: &SyncSettings,
260 space_id: Uuid,
261 repo_id: Uuid,
262 actor_id: Option<Uuid>,
263 bare: &Path,
245 target: &PushTarget<'_>,
264246) {
265 match enqueue_post_receive_push(queue, settings, space_id, repo_id, actor_id, bare).await {
247 let (space_id, repo_id) = (target.space_id, target.repo_id);
248 match enqueue_post_receive_push(queue, settings, target).await {
266249 Ok(n) if n > 0 => {
267250 tracing::debug!(
268251 count = n,
@@ -286,16 +269,10 @@
286269 settings: &SyncSettings,
287270 ci_topic: Option<&str>,
288271 ai_ctx: Option<AiPushCtx>,
289 space_id: Uuid,
290 repo_id: Uuid,
291 actor_id: Option<Uuid>,
292 bare: &Path,
272 target: &PushTarget<'_>,
293273) {
294 match enqueue_post_receive_push_full(
295 queue, settings, ci_topic, ai_ctx, space_id, repo_id, actor_id, bare,
296 )
297 .await
298 {
274 let (space_id, repo_id) = (target.space_id, target.repo_id);
275 match enqueue_post_receive_push_full(queue, settings, ci_topic, ai_ctx, target).await {
299276 Ok(n) if n > 0 => tracing::debug!(
300277 count = n,
301278 space_id = %space_id,
Modified lib/git/src/sync/pulls.rs View lib/git/src/sync/pulls.rs
@@ -99,7 +99,6 @@
9999 commit_sha: Set(commit_sha.map(|sha| sha.to_string())),
100100 content: Set(None),
101101 created_at: Set(OffsetDateTime::now_utc()),
102 ..Default::default()
103102 };
104103 row.insert(db.write()).await?;
105104 Ok(())
@@ -116,6 +115,7 @@
116115 let model = repos::ActiveModel {
117116 id: Set(repo_id),
118117 nums_pr: Set(i32::try_from(open).unwrap_or(i32::MAX)),
118 // A partial update leaves every other column `NotSet`.
119119 ..Default::default()
120120 };
121121 model.update(db.write()).await?;
Modified lib/git/src/sync/worker.rs View lib/git/src/sync/worker.rs
@@ -210,7 +210,7 @@
210210 "sync task completed"
211211 );
212212 if let Err(e) =
213 crate::sync::pulls::refresh_pulls_after_sync(&db, rsync.repo_model(), &task)
213 crate::sync::pulls::refresh_pulls_after_sync(db, rsync.repo_model(), &task)
214214 .await
215215 {
216216 warn!(
Modified lib/i18n/src/lib.rs View lib/i18n/src/lib.rs
@@ -15,9 +15,7 @@
1515pub mod locale;
1616
1717pub use catalog::*;
18pub use locale::{
19 UiLocale, negotiate_accept_language, resolve_user_locale, resolve_user_locales,
20};
18pub use locale::{UiLocale, negotiate_accept_language, resolve_user_locale, resolve_user_locales};
2119
2220/// Normalize a stored user preference to a locale this build can render.
2321///
Modified lib/i18n/src/locale.rs View lib/i18n/src/locale.rs
@@ -90,7 +90,10 @@
9090 }
9191 // Stable by (descending q, ascending header position); header order wins ties.
9292 candidates.sort_by(|a, b| b.0.total_cmp(&a.0).then(a.1.cmp(&b.1)));
93 candidates.first().map(|(_, _, locale)| *locale).unwrap_or(fallback)
93 candidates
94 .first()
95 .map(|(_, _, locale)| *locale)
96 .unwrap_or(fallback)
9497}
9598
9699/// Resolve the language for a specific message recipient.
@@ -121,7 +124,12 @@
121124 use models::users::user;
122125 let row = user::Entity::find_by_id(uid).one(db).await.ok().flatten();
123126 match row {
124 Some(row) => resolve_locale(row.language.as_deref(), row.locale.as_deref(), None, fallback),
127 Some(row) => resolve_locale(
128 row.language.as_deref(),
129 row.locale.as_deref(),
130 None,
131 fallback,
132 ),
125133 None => fallback,
126134 }
127135}
@@ -147,7 +155,12 @@
147155 .await
148156 .unwrap_or_default();
149157 for row in rows {
150 let locale = resolve_locale(row.language.as_deref(), row.locale.as_deref(), None, fallback);
158 let locale = resolve_locale(
159 row.language.as_deref(),
160 row.locale.as_deref(),
161 None,
162 fallback,
163 );
151164 out.insert(row.uid, locale);
152165 }
153166 out
Modified lib/import/src/providers/mod.rs View lib/import/src/providers/mod.rs
@@ -114,10 +114,10 @@
114114}
115115
116116fn strip_brackets_and_port(host: &str) -> String {
117 if host.starts_with('[') {
118 if let Some(end) = host.find(']') {
119 return host[1..end].to_string();
120 }
117 if host.starts_with('[')
118 && let Some(end) = host.find(']')
119 {
120 return host[1..end].to_string();
121121 }
122122 if host.matches(':').count() == 1
123123 && let Some(idx) = host.rfind(':')
Modified lib/models/src/ai/ai_job.rs View lib/models/src/ai/ai_job.rs
@@ -19,7 +19,6 @@
1919)]
2020#[serde(rename_all = "snake_case")]
2121#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(24))")]
22
2322pub enum AiJobKind {
2423 #[default]
2524 #[sea_orm(string_value = "pull_review")]
Modified lib/pulls/src/handlers/pulls/delete_pull.rs View lib/pulls/src/handlers/pulls/delete_pull.rs
@@ -33,10 +33,7 @@
3333 )
3434 .await?;
3535 if !handle.admin {
36 return Err(ApiError::Forbidden(export::rest::AppResponseData::err(
37 403,
38 "repository administrator required",
39 )));
36 return Err(ApiError::forbidden());
4037 }
4138 let caller = handle
4239 .caller
Modified lib/pulls/src/handlers/pulls/merge_pull.rs View lib/pulls/src/handlers/pulls/merge_pull.rs
@@ -35,17 +35,11 @@
3535 true,
3636 )
3737 .await?;
38 let caller = handle
39 .caller
40 .ok_or(crate::errors::PullError::Unauthorized)?;
4138 let (merged, result, summary) = service
4239 .merge_pull(
4340 path.space_name.clone(),
4441 path.repo_name.clone(),
45 &handle.ctx,
46 &handle.pull,
47 caller,
48 handle.admin,
42 &handle,
4943 cmd,
5044 )
5145 .await?;
Modified lib/pulls/src/handlers/pulls/update_pull.rs View lib/pulls/src/handlers/pulls/update_pull.rs
@@ -6,7 +6,6 @@
66use crate::errors::ApiError;
77use crate::services::pulls::{PullDto, UpdatePullCmd};
88use crate::services::service::PullService;
9use crate::share::authz::require_can_manage;
109use crate::share::lookup::pull_handle;
1110use crate::share::path::PullPath;
1211
@@ -36,17 +35,8 @@
3635 true,
3736 )
3837 .await?;
39 let caller = require_can_manage(&handle.pull, handle.caller, handle.admin)?;
4038 let updated = service
41 .update_pull(
42 path.space_name,
43 path.repo_name,
44 &handle.ctx,
45 &handle.pull,
46 caller,
47 handle.admin,
48 cmd,
49 )
39 .update_pull(path.space_name, path.repo_name, &handle, cmd)
5040 .await?;
5141 let users = service.pull_users(std::slice::from_ref(&updated)).await?;
5242 Ok(AppResponseData::ok(PullService::dto(&updated, &users)))
Modified lib/pulls/src/handlers/relations/unlink_issue_pull.rs View lib/pulls/src/handlers/relations/unlink_issue_pull.rs
@@ -44,7 +44,7 @@
4444 .one(service.db_read())
4545 .await
4646 .map_err(PullError::from)?
47 .ok_or_else(|| PullError::NotFound)?;
47 .ok_or(PullError::NotFound)?;
4848 if relation.issue_id != issue_id {
4949 return Err(ApiError::from(PullError::NotFound));
5050 }
@@ -52,12 +52,12 @@
5252 .one(service.db_read())
5353 .await
5454 .map_err(PullError::from)?
55 .ok_or_else(|| PullError::NotFound)?;
55 .ok_or(PullError::NotFound)?;
5656 let repo = models::repo::repos::Entity::find_by_id(pull.repo_id)
5757 .one(service.db_read())
5858 .await
5959 .map_err(PullError::from)?
60 .ok_or_else(|| PullError::NotFound)?;
60 .ok_or(PullError::NotFound)?;
6161 let (ctx, _caller) = repo_write(
6262 &service,
6363 &session,
Modified lib/pulls/src/handlers/reviews/dismiss_review.rs View lib/pulls/src/handlers/reviews/dismiss_review.rs
@@ -32,10 +32,7 @@
3232 )
3333 .await?;
3434 if !handle.admin {
35 return Err(ApiError::Forbidden(export::rest::AppResponseData::err(
36 403,
37 "repository administrator required",
38 )));
35 return Err(ApiError::forbidden());
3936 }
4037 let caller = handle.caller.ok_or(PullError::Unauthorized)?;
4138 let review = service
Modified lib/pulls/src/services/approvals.rs View lib/pulls/src/services/approvals.rs
@@ -143,12 +143,7 @@
143143 .unwrap_or(0);
144144 let dismiss_stale = policies.iter().any(|policy| policy.dismiss_stale_approvals);
145145 let merge_whitelist_on = policies.iter().any(|policy| policy.enable_merge_whitelist);
146 let bypassed = admin
147 || (merge_whitelist_on
148 && whitelists
149 .merge_allowed
150 .iter()
151 .any(|entry| *entry == caller));
146 let bypassed = admin || (merge_whitelist_on && whitelists.merge_allowed.contains(&caller));
152147 let mut reasons: Vec<String> = Vec::new();
153148
154149 let verdicts = if required > 0
Modified lib/pulls/src/services/assignees.rs View lib/pulls/src/services/assignees.rs
@@ -66,7 +66,6 @@
6666 pull_request_id: Set(pull.id),
6767 user_id: Set(uid),
6868 created_at: Set(OffsetDateTime::now_utc()),
69 ..Default::default()
7069 }
7170 .insert(self.db_write())
7271 .await?;
Modified lib/pulls/src/services/comments.rs View lib/pulls/src/services/comments.rs
@@ -107,41 +107,40 @@
107107 if !inline && cmd.line.is_some() {
108108 return Err(PullError::BadRequest("line requires a path".to_string()));
109109 }
110 if let Some(line) = cmd.line {
111 if line <= 0 {
112 return Err(PullError::BadRequest("line must be positive".to_string()));
113 }
110 if let Some(line) = cmd.line
111 && line <= 0
112 {
113 return Err(PullError::BadRequest("line must be positive".to_string()));
114114 }
115 if let Some(side) = cmd.side.as_deref() {
116 if side != "left" && side != "right" {
117 return Err(PullError::BadRequest(
118 "side must be left or right".to_string(),
119 ));
120 }
115 if let Some(side) = cmd.side.as_deref()
116 && side != "left"
117 && side != "right"
118 {
119 return Err(PullError::BadRequest(
120 "side must be left or right".to_string(),
121 ));
121122 }
122 if let Some(parent) = cmd.parent_comment_id {
123 if !pull_request_comment::Entity::find_by_id(parent)
123 if let Some(parent) = cmd.parent_comment_id
124 && !pull_request_comment::Entity::find_by_id(parent)
124125 .filter(pull_request_comment::Column::PullRequestId.eq(pull.id))
125126 .one(self.db_read())
126127 .await?
127128 .is_some()
128 {
129 return Err(PullError::BadRequest(
130 "parent comment not found".to_string(),
131 ));
132 }
129 {
130 return Err(PullError::BadRequest(
131 "parent comment not found".to_string(),
132 ));
133133 }
134 if let Some(review_id) = cmd.review_id {
135 if !models::pull_request::pull_request_review::Entity::find_by_id(review_id)
134 if let Some(review_id) = cmd.review_id
135 && !models::pull_request::pull_request_review::Entity::find_by_id(review_id)
136136 .filter(
137137 models::pull_request::pull_request_review::Column::PullRequestId.eq(pull.id),
138138 )
139139 .one(self.db_read())
140140 .await?
141141 .is_some()
142 {
143 return Err(PullError::BadRequest("review not found".to_string()));
144 }
142 {
143 return Err(PullError::BadRequest("review not found".to_string()));
145144 }
146145 let row = pull_request_comment::ActiveModel {
147146 id: Set(Uuid::now_v7()),
@@ -164,7 +163,6 @@
164163 resolved_by: Set(None),
165164 created_at: Set(OffsetDateTime::now_utc()),
166165 updated_at: Set(None),
167 ..Default::default()
168166 }
169167 .insert(self.db_write())
170168 .await?;
Modified lib/pulls/src/services/events.rs View lib/pulls/src/services/events.rs
@@ -67,7 +67,6 @@
6767 commit_sha: Set(commit_sha),
6868 content: Set(content),
6969 created_at: Set(OffsetDateTime::now_utc()),
70 ..Default::default()
7170 };
7271 row.insert(db).await?;
7372 Ok(())
Modified lib/pulls/src/services/labels.rs View lib/pulls/src/services/labels.rs
@@ -83,7 +83,6 @@
8383 pull_request_id: Set(pull.id),
8484 label_id: Set(label.id),
8585 created_at: Set(OffsetDateTime::now_utc()),
86 ..Default::default()
8786 }
8887 .insert(self.db_write())
8988 .await?;
Modified lib/pulls/src/services/merge.rs View lib/pulls/src/services/merge.rs
@@ -38,29 +38,35 @@
3838}
3939
4040impl PullService {
41 /// `handle` carries the resolved repository, pull row, caller and admin bit
42 /// so the signature does not repeat four positionally-similar arguments.
4143 pub async fn merge_pull(
4244 &self,
4345 space_name: String,
4446 repo_name: String,
45 ctx: &GitContext,
46 pull: &pull_requests::Model,
47 caller: Uuid,
48 admin: bool,
47 handle: &crate::share::lookup::PullHandle,
4948 cmd: MergePullCmd,
5049 ) -> Result<(pull_requests::Model, MergeResultDto, PullSummaryDto), PullError> {
50 let crate::share::lookup::PullHandle {
51 ctx, pull, admin, ..
52 } = handle;
53 // Absorbed from the handler: merging always needs an authenticated
54 // caller, and the resolved handle is the single place it is known.
55 let caller = handle.caller.ok_or(PullError::Unauthorized)?;
56 let admin = *admin;
5157 require_open(pull)?;
5258 if pull.is_draft {
5359 return Err(PullError::Conflict(
5460 "pull request is still a draft".to_string(),
5561 ));
5662 }
57 if let Some(expected) = cmd.expect_head_sha.as_deref() {
58 if expected != pull.head_sha {
59 return Err(PullError::Conflict(format!(
60 "head moved: expected {expected}, found {}",
61 pull.head_sha
62 )));
63 }
63 if let Some(expected) = cmd.expect_head_sha.as_deref()
64 && expected != pull.head_sha
65 {
66 return Err(PullError::Conflict(format!(
67 "head moved: expected {expected}, found {}",
68 pull.head_sha
69 )));
6470 }
6571
6672 let (current, summary) = self
@@ -143,8 +149,7 @@
143149
144150 if cmd.delete_branch_after_merge.unwrap_or(false)
145151 && merged.head_branch != ctx.model.default_branch
146 {
147 if let Err(error) = self
152 && let Err(error) = self
148153 .git
149154 .delete_branch(
150155 space_name.clone(),
@@ -153,13 +158,12 @@
153158 false,
154159 )
155160 .await
156 {
157 tracing::warn!(
158 error = %error,
159 branch = %merged.head_branch,
160 "merged pull request head branch cleanup failed"
161 );
162 }
161 {
162 tracing::warn!(
163 error = %error,
164 branch = %merged.head_branch,
165 "merged pull request head branch cleanup failed"
166 );
163167 }
164168 Ok((merged, result, summary))
165169 }
Modified lib/pulls/src/services/pulls.rs View lib/pulls/src/services/pulls.rs
@@ -321,14 +321,13 @@
321321 "head and base branch must differ".to_string(),
322322 ));
323323 }
324 if let Some(issue_id) = cmd.issue_id {
325 if !models::issue::issue_item::Entity::find_by_id(issue_id)
324 if let Some(issue_id) = cmd.issue_id
325 && !models::issue::issue_item::Entity::find_by_id(issue_id)
326326 .one(self.db_read())
327327 .await?
328328 .is_some()
329 {
330 return Err(PullError::BadRequest("issue not found".to_string()));
331 }
329 {
330 return Err(PullError::BadRequest("issue not found".to_string()));
332331 }
333332
334333 let plan = self
@@ -377,7 +376,7 @@
377376 head_branch: Set(cmd.head_branch.clone()),
378377 head_sha: Set(plan.head_sha.clone()),
379378 head_repo_id: Set(None),
380 head_label: Set(Some(format!("{}", cmd.head_branch))),
379 head_label: Set(Some(cmd.head_branch.to_string())),
381380 base_branch: Set(base_branch.clone()),
382381 base_sha: Set(plan.base_sha.clone()),
383382 is_draft: Set(cmd.draft.unwrap_or(false)),
@@ -395,7 +394,6 @@
395394 created_at: Set(now),
396395 updated_at: Set(None),
397396 closed_at: Set(None),
398 ..Default::default()
399397 };
400398 let pull = created.insert(&txn).await?;
401399 write_event(
@@ -415,16 +413,20 @@
415413 Ok(pull)
416414 }
417415
416 /// `handle` carries the resolved repository, pull row, caller and admin bit
417 /// so the signature does not repeat four positionally-similar arguments.
418418 pub async fn update_pull(
419419 &self,
420420 space_name: String,
421421 repo_name: String,
422 ctx: &GitContext,
423 pull: &pull_requests::Model,
424 caller: Uuid,
425 admin: bool,
422 handle: &crate::share::lookup::PullHandle,
426423 cmd: UpdatePullCmd,
427424 ) -> Result<pull_requests::Model, PullError> {
425 let crate::share::lookup::PullHandle {
426 ctx, pull, admin, ..
427 } = handle;
428 let caller = handle.caller.ok_or(PullError::Unauthorized)?;
429 let admin = *admin;
428430 if pull.is_locked && !admin {
429431 return Err(PullError::Forbidden);
430432 }
@@ -460,14 +462,13 @@
460462 changed = true;
461463 }
462464 if let Some(issue_id) = cmd.issue_id {
463 if let Some(id) = issue_id {
464 if !models::issue::issue_item::Entity::find_by_id(id)
465 if let Some(id) = issue_id
466 && !models::issue::issue_item::Entity::find_by_id(id)
465467 .one(self.db_read())
466468 .await?
467469 .is_some()
468 {
469 return Err(PullError::BadRequest("issue not found".to_string()));
470 }
470 {
471 return Err(PullError::BadRequest("issue not found".to_string()));
471472 }
472473 model.issue_id = Set(issue_id);
473474 changed = true;
Modified lib/pulls/src/services/relations.rs View lib/pulls/src/services/relations.rs
@@ -50,7 +50,6 @@
5050 issue_id: Set(issue_id),
5151 pull_request_id: Set(pull.id),
5252 created_at: Set(OffsetDateTime::now_utc()),
53 ..Default::default()
5453 }
5554 .insert(self.db_write())
5655 .await?;
Modified lib/pulls/src/services/reviewers.rs View lib/pulls/src/services/reviewers.rs
@@ -121,7 +121,6 @@
121121 requested_by_id: Set(caller),
122122 created_at: Set(OffsetDateTime::now_utc()),
123123 updated_at: Set(None),
124 ..Default::default()
125124 }
126125 .insert(self.db_write())
127126 .await?;
@@ -239,7 +238,6 @@
239238 requested_by_id: Set(user_id),
240239 created_at: Set(OffsetDateTime::now_utc()),
241240 updated_at: Set(None),
242 ..Default::default()
243241 }
244242 .insert(self.db_write())
245243 .await?;
Modified lib/pulls/src/services/reviews.rs View lib/pulls/src/services/reviews.rs
@@ -104,7 +104,6 @@
104104 state: Set(state),
105105 created_at: Set(now),
106106 updated_at: Set(None),
107 ..Default::default()
108107 }
109108 .insert(&txn)
110109 .await?;
@@ -132,7 +131,6 @@
132131 resolved_by: Set(None),
133132 created_at: Set(now),
134133 updated_at: Set(None),
135 ..Default::default()
136134 }
137135 .insert(&txn)
138136 .await?;
Modified lib/pulls/src/share/path.rs View lib/pulls/src/share/path.rs
@@ -23,11 +23,6 @@
2323 Path::<PullPath>::from_request_parts(parts, state)
2424 .await
2525 .map(|Path(p)| p)
26 .map_err(|_| {
27 ApiError::BadRequest(export::rest::AppResponseData::err(
28 400,
29 "invalid request path",
30 ))
31 })
26 .map_err(|_| ApiError::bad_request("invalid request path"))
3227 }
3328}
Modified lib/search/src/parser.rs View lib/search/src/parser.rs
@@ -183,18 +183,17 @@
183183 is_definition: true,
184184 });
185185 }
186 } else if is_identifier_kind(kind) {
187 if let Ok(name) = node.utf8_text(source) {
188 if !name.is_empty() {
189 out.push(ParsedSymbol {
190 name: name.to_string(),
191 kind: "reference".to_string(),
192 line: node.start_position().row as u32 + 1,
193 end_line: node.end_position().row as u32 + 1,
194 is_definition: false,
195 });
196 }
197 }
186 } else if is_identifier_kind(kind)
187 && let Ok(name) = node.utf8_text(source)
188 && !name.is_empty()
189 {
190 out.push(ParsedSymbol {
191 name: name.to_string(),
192 kind: "reference".to_string(),
193 line: node.start_position().row as u32 + 1,
194 end_line: node.end_position().row as u32 + 1,
195 is_definition: false,
196 });
198197 }
199198 if cursor.goto_first_child() {
200199 loop {
Modified lib/search/src/service.rs View lib/search/src/service.rs
@@ -1424,7 +1424,10 @@
14241424 // 看板与频道是「当前所在位置」的默认约束,只在对应类型上生效:工单按 board_id,
14251425 // 消息按 room_id。其余类型的文档没有这两个字段,加了条件会命中零条。
14261426 if kind == SearchKind::Issue
1427 && let Some(board) = options.board_id.as_deref().filter(|value| !value.is_empty())
1427 && let Some(board) = options
1428 .board_id
1429 .as_deref()
1430 .filter(|value| !value.is_empty())
14281431 {
14291432 filters.push(eq_filter("board_id", board));
14301433 }
Modified lib/social/src/parse/mod.rs View lib/social/src/parse/mod.rs
@@ -196,7 +196,7 @@
196196 }
197197 }
198198 // 域名越长越优先;稳定排序保证同长度时先注册的平台先命中。
199 routes.sort_by(|left, right| right.domain_len.cmp(&left.domain_len));
199 routes.sort_by_key(|route| std::cmp::Reverse(route.domain_len));
200200 routes
201201 })
202202}
Modified lib/social/src/render.rs View lib/social/src/render.rs
@@ -3,7 +3,7 @@
33//! 生成的所有属性值都经过 HTML 转义;`src` 只可能是注册表模板拼接的结果,
44//! 或者经过校验的 http(s) 原始 URL,因此不会有脚本或事件属性注入。
55
6use crate::{Aspect, EmbedKind, Resolved};
6use crate::{EmbedKind, Resolved};
77
88/// HTML 属性/文本转义。
99pub fn escape_html(value: &str) -> String {
Modified lib/space/src/services/bans.rs View lib/space/src/services/bans.rs
@@ -38,7 +38,10 @@
3838 pub user_id: Uuid,
3939 #[serde(default)]
4040 pub reason: Option<String>,
41 #[serde(default, deserialize_with = "export::serde_datetime::deserialize_opt_datetime")]
41 #[serde(
42 default,
43 deserialize_with = "export::serde_datetime::deserialize_opt_datetime"
44 )]
4245 pub expires_at: Option<OffsetDateTime>,
4346 #[serde(default)]
4447 pub allow_agent_join: Option<bool>,
Modified lib/space/src/services/invites.rs View lib/space/src/services/invites.rs
@@ -48,7 +48,10 @@
4848 pub email: Option<String>,
4949 #[serde(default)]
5050 pub group_ids: Vec<Uuid>,
51 #[serde(default, deserialize_with = "export::serde_datetime::deserialize_opt_datetime")]
51 #[serde(
52 default,
53 deserialize_with = "export::serde_datetime::deserialize_opt_datetime"
54 )]
5255 pub expires_at: Option<OffsetDateTime>,
5356}
5457
Modified lib/space/src/services/join_requests.rs View lib/space/src/services/join_requests.rs
@@ -148,19 +148,19 @@
148148 }
149149 let now = OffsetDateTime::now_utc();
150150 match space.allow_join_level {
151 AllowJoinLevel::NeedMailActiveAndLastOneMid => {
152 if applicant.created_at > now - Duration::days(1) {
153 return Err(SpaceError::JoinRequirementNotMet(
154 "the account must be at least one day old".to_string(),
155 ));
156 }
151 AllowJoinLevel::NeedMailActiveAndLastOneMid
152 if applicant.created_at > now - Duration::days(1) =>
153 {
154 return Err(SpaceError::JoinRequirementNotMet(
155 "the account must be at least one day old".to_string(),
156 ));
157157 }
158 AllowJoinLevel::NeedMailActiveAndLastOneHours => {
159 if applicant.created_at > now - Duration::hours(1) {
160 return Err(SpaceError::JoinRequirementNotMet(
161 "the account must be at least one hour old".to_string(),
162 ));
163 }
158 AllowJoinLevel::NeedMailActiveAndLastOneHours
159 if applicant.created_at > now - Duration::hours(1) =>
160 {
161 return Err(SpaceError::JoinRequirementNotMet(
162 "the account must be at least one hour old".to_string(),
163 ));
164164 }
165165 _ => {}
166166 }
Modified lib/space/src/services/members.rs View lib/space/src/services/members.rs
@@ -52,7 +52,10 @@
5252 pub message_notifications: Option<MessageNotifications>,
5353 #[serde(default)]
5454 pub muted: Option<bool>,
55 #[serde(default, deserialize_with = "export::serde_datetime::deserialize_opt_datetime")]
55 #[serde(
56 default,
57 deserialize_with = "export::serde_datetime::deserialize_opt_datetime"
58 )]
5659 pub mute_until: Option<OffsetDateTime>,
5760 #[serde(default)]
5861 pub mobile_push: Option<bool>,
Modified lib/storage/src/s3.rs View lib/storage/src/s3.rs
@@ -69,11 +69,7 @@
6969 }
7070
7171 let mut presign_bucket = bucket.clone();
72 if let Some(public_endpoint) = config
73 .public_endpoint
74 .as_deref()
75 .filter(|s| !s.is_empty())
76 {
72 if let Some(public_endpoint) = config.public_endpoint.as_deref().filter(|s| !s.is_empty()) {
7773 presign_bucket.region = Region::Custom {
7874 region: config.region().to_string(),
7975 endpoint: public_endpoint.to_string(),
@@ -411,7 +407,9 @@
411407 post_policy: String,
412408 ) -> Result<String, StorageError> {
413409 check_expiry(expiry_secs)?;
414 Ok(self.presign_bucket.presign_post(key, expiry_secs, post_policy)?)
410 Ok(self
411 .presign_bucket
412 .presign_post(key, expiry_secs, post_policy)?)
415413 }
416414}
417415
Modified lib/user/src/services/profile.rs View lib/user/src/services/profile.rs
@@ -341,14 +341,11 @@
341341 let can_view = match user.visibility {
342342 ProfileVisibility::Public => true,
343343 ProfileVisibility::RegisteredOnly => viewer.is_some(),
344 ProfileVisibility::MembersOnly => match viewer {
344 ProfileVisibility::MembersOnly => matches!(
345 viewer,
345346 Some(viewer_uid)
346 if is_owner || shares_space_with(self, viewer_uid, target_uid).await? =>
347 {
348 true
349 }
350 _ => false,
351 },
347 if is_owner || shares_space_with(self, viewer_uid, target_uid).await?
348 ),
352349 ProfileVisibility::Private => is_owner,
353350 };
354351 if !can_view {
Modified web/lib.rs View web/lib.rs
@@ -141,13 +141,13 @@
141141 .iter()
142142 .find(|p| p.ends_with(".css"))
143143 .expect("css in dist");
144 assert_eq!(file(*css).unwrap().mime, "text/css; charset=utf-8");
144 assert_eq!(file(css).unwrap().mime, "text/css; charset=utf-8");
145145 }
146146
147147 #[test]
148148 fn wasm_mime() {
149149 if let Some(w) = paths().iter().find(|p| p.ends_with(".wasm")) {
150 assert_eq!(file(*w).unwrap().mime, "application/wasm");
150 assert_eq!(file(w).unwrap().mime, "application/wasm");
151151 }
152152 }
153153