@@ -10,13 +10,91 @@ use git_internal::{
1010} ;
1111
1212use crate :: api_service:: { ApiHandler , history, tree_ops} ;
13- use crate :: model:: commit:: { CommitDetail , CommitSummary } ;
13+ use crate :: model:: commit:: { CommitDetail , CommitSummary , GpgStatus } ;
1414use crate :: model:: git:: { CommitBindingInfo , LatestCommitInfo } ;
1515use common:: model:: { DiffItem , Pagination } ;
1616use git_internal:: hash:: SHA1 ;
1717use redis:: AsyncCommands ;
1818use 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
0 commit comments