Skip to content
This repository was archived by the owner on Oct 19, 2026. It is now read-only.

Commit e8898f7

Browse files
authored
fix: add gpg_check_status for commit detail (#1697)
cargo fmt cargo fmt fix: mv gpg_status into CommitSummary struct fix: code review fix: code review fix: code review Signed-off-by: Ruizhi Huang <231220075@smail.nju.edu.cn>
1 parent 8004b6f commit e8898f7

4 files changed

Lines changed: 100 additions & 4 deletions

File tree

‎ceres/src/api_service/commit_ops.rs‎

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,91 @@ use git_internal::{
1010
};
1111

1212
use crate::api_service::{ApiHandler, history, tree_ops};
13-
use crate::model::commit::{CommitDetail, CommitSummary};
13+
use crate::model::commit::{CommitDetail, CommitSummary, GpgStatus};
1414
use crate::model::git::{CommitBindingInfo, LatestCommitInfo};
1515
use common::model::{DiffItem, Pagination};
1616
use git_internal::hash::SHA1;
1717
use redis::AsyncCommands;
1818
use std::collections::{HashMap, HashSet, VecDeque};
1919

20+
/// Compute GPG signature verification status for a commit.
21+
///
22+
/// This function checks whether a commit has a valid GPG signature by:
23+
/// 1. Checking Redis cache for previously computed status
24+
/// 2. Extracting the GPG signature from commit content
25+
/// 3. Verifying the signature against stored GPG keys for the committer
26+
///
27+
/// # Arguments
28+
/// * `handler` - API handler providing storage and cache access
29+
/// * `commit` - The commit to verify
30+
/// * `committer` - Username of the committer for key lookup
31+
///
32+
/// # Returns
33+
/// * `GpgStatus::Verified` - Commit has a valid signature
34+
/// * `GpgStatus::Unverified` - Commit has a signature but verification failed
35+
/// * `GpgStatus::NoSignature` - Commit is not signed
36+
///
37+
/// # Cache
38+
/// Results are cached in Redis with a 600-second TTL to avoid redundant verification.
39+
async fn compute_gpg_status_for_commit<T: ApiHandler + ?Sized>(
40+
handler: &T,
41+
commit: &Commit,
42+
committer: &str,
43+
) -> GpgStatus {
44+
// Cache key per commit
45+
let cache_key = format!(
46+
"{}:gpg_status:v1:sha={}",
47+
handler.object_cache().prefix,
48+
commit.id
49+
);
50+
51+
// Try cache first
52+
let mut conn = handler.object_cache().connection.clone();
53+
if let Ok(Some(json)) = conn.get::<_, Option<String>>(&cache_key).await
54+
&& let Ok(status) = serde_json::from_str::<GpgStatus>(&json)
55+
{
56+
let _ = conn.expire::<_, ()>(&cache_key, 600).await;
57+
return status;
58+
}
59+
60+
let content = &commit.message;
61+
let status = {
62+
let (_commit_msg, sig_opt) =
63+
crate::merge_checker::gpg_signature_checker::extract_from_commit_content(content);
64+
if sig_opt.is_none() {
65+
GpgStatus::NoSignature
66+
} else {
67+
let storage = handler.get_context();
68+
let checker = crate::merge_checker::gpg_signature_checker::GpgSignatureChecker {
69+
storage: Arc::new(storage),
70+
};
71+
match checker
72+
.verify_commit_gpg_signature(content, committer.to_string())
73+
.await
74+
{
75+
Ok(()) => GpgStatus::Verified,
76+
Err(e) => {
77+
tracing::debug!("GPG verification failed for commit {}: {}", commit.id, e);
78+
GpgStatus::Unverified
79+
}
80+
}
81+
}
82+
};
83+
84+
// Save to cache
85+
match serde_json::to_string(&status) {
86+
Ok(json) => {
87+
if let Err(e) = conn.set_ex::<_, _, ()>(&cache_key, json, 600).await {
88+
tracing::warn!("Failed to cache GPG status for {}: {}", cache_key, e);
89+
}
90+
}
91+
Err(e) => {
92+
tracing::warn!("Failed to serialize GPG status for {}: {}", cache_key, e);
93+
}
94+
}
95+
status
96+
}
97+
2098
/// Get the latest commit that modified a file or directory.
2199
///
22100
/// This unified function handles both tag-based and commit-based browsing through
@@ -426,13 +504,15 @@ pub async fn list_commit_history<T: ApiHandler + ?Sized>(
426504
let c = handler.get_commit_by_hash(sha).await?;
427505
let mut info: LatestCommitInfo = c.clone().into();
428506
apply_username_binding(handler, &c.id.to_string(), &mut info).await;
507+
let gpg_status = compute_gpg_status_for_commit(handler, &c, &info.committer).await;
429508
res.push(CommitSummary {
430509
sha: c.id.to_string(),
431510
short_message: info.short_message,
432511
author: info.author,
433512
committer: info.committer,
434513
date: info.date,
435514
parents: c.parent_commit_ids.iter().map(|p| p.to_string()).collect(),
515+
gpg_status,
436516
});
437517
}
438518
return Ok((res, total));
@@ -468,13 +548,15 @@ pub async fn list_commit_history<T: ApiHandler + ?Sized>(
468548
for c in slice {
469549
let mut info: LatestCommitInfo = c.clone().into();
470550
apply_username_binding(handler, &c.id.to_string(), &mut info).await;
551+
let gpg_status = compute_gpg_status_for_commit(handler, c, &info.committer).await;
471552
res.push(CommitSummary {
472553
sha: c.id.to_string(),
473554
short_message: info.short_message,
474555
author: info.author,
475556
committer: info.committer,
476557
date: info.date,
477558
parents: c.parent_commit_ids.iter().map(|p| p.to_string()).collect(),
559+
gpg_status,
478560
});
479561
}
480562

@@ -511,6 +593,7 @@ pub async fn build_commit_detail<T: ApiHandler + ?Sized>(
511593
// Summary
512594
let mut info: LatestCommitInfo = commit.clone().into();
513595
apply_username_binding(handler, &commit.id.to_string(), &mut info).await;
596+
let gpg_status = compute_gpg_status_for_commit(handler, &commit, &info.committer).await;
514597
let summary = CommitSummary {
515598
sha: commit.id.to_string(),
516599
short_message: info.short_message,
@@ -522,6 +605,7 @@ pub async fn build_commit_detail<T: ApiHandler + ?Sized>(
522605
.iter()
523606
.map(|p| p.to_string())
524607
.collect(),
608+
gpg_status,
525609
};
526610

527611
// Collect diffs vs each parent and merge by path

‎ceres/src/merge_checker/gpg_signature_checker.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ impl GpgSignatureChecker {
7474
Ok(())
7575
}
7676

77-
async fn verify_commit_gpg_signature(
77+
pub(crate) async fn verify_commit_gpg_signature(
7878
&self,
7979
commit_content: &str,
8080
assignee: String,
@@ -138,7 +138,7 @@ fn normalize_signature_block(sig_block: &str) -> String {
138138
lines.join("\n") + "\n"
139139
}
140140

141-
fn extract_from_commit_content(msg_gpg: &str) -> (String, Option<String>) {
141+
pub(crate) fn extract_from_commit_content(msg_gpg: &str) -> (String, Option<String>) {
142142
const SIG_PATTERN: &str = r"gpgsig (-----BEGIN (?:PGP|SSH) SIGNATURE-----[\s\S]*?-----END (?:PGP|SSH) SIGNATURE-----)";
143143
let sig_regex = Regex::new(SIG_PATTERN).unwrap();
144144

‎ceres/src/merge_checker/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use jupiter::{model::cl_dto::ClInfoDto, storage::Storage};
1414
pub mod cl_sync_checker;
1515
mod code_review_checker;
1616
mod commit_message_checker;
17-
mod gpg_signature_checker;
17+
pub(crate) mod gpg_signature_checker;
1818

1919
#[async_trait]
2020
pub trait Checker: Send + Sync {

‎ceres/src/model/commit.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
use serde::{Deserialize, Serialize};
22
use utoipa::ToSchema;
33

4+
#[derive(Debug, Serialize, Deserialize, ToSchema, Clone, Default)]
5+
#[serde(rename_all = "PascalCase")]
6+
pub enum GpgStatus {
7+
Verified,
8+
Unverified,
9+
#[default]
10+
NoSignature,
11+
}
12+
413
#[derive(Debug, Serialize, Deserialize, ToSchema, Clone)]
514
pub struct CommitSummary {
615
pub sha: String,
@@ -9,6 +18,9 @@ pub struct CommitSummary {
918
pub committer: String,
1019
pub date: String,
1120
pub parents: Vec<String>,
21+
/// GPG verification status for this commit.
22+
#[serde(default)]
23+
pub gpg_status: GpgStatus,
1224
}
1325

1426
#[derive(Debug, Serialize, Deserialize, ToSchema, Clone, Default)]

0 commit comments

Comments
 (0)