1use auths_sdk::ports::git::{CommitRecord, GitLogProvider, GitProviderError, SignatureStatus};
6use std::path::Path;
7use std::sync::Mutex;
8
9pub struct Git2LogProvider {
22 repo: Mutex<git2::Repository>,
23}
24
25impl Git2LogProvider {
26 pub fn open(path: &Path) -> Result<Self, GitProviderError> {
31 let repo =
32 git2::Repository::open(path).map_err(|e| GitProviderError::Open(e.to_string()))?;
33 Ok(Self {
34 repo: Mutex::new(repo),
35 })
36 }
37}
38
39impl GitLogProvider for Git2LogProvider {
40 fn walk_commits(
41 &self,
42 range: Option<&str>,
43 limit: Option<usize>,
44 ) -> Result<Vec<CommitRecord>, GitProviderError> {
45 let repo = self
46 .repo
47 .lock()
48 .map_err(|_| GitProviderError::LockPoisoned)?;
49
50 let mut revwalk = repo
51 .revwalk()
52 .map_err(|e| GitProviderError::Walk(e.to_string()))?;
53 revwalk
54 .set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::TIME)
55 .map_err(|e| GitProviderError::Walk(e.to_string()))?;
56
57 if let Some(range_spec) = range {
58 revwalk
59 .push_range(range_spec)
60 .map_err(|e| GitProviderError::Walk(e.to_string()))?;
61 } else {
62 revwalk
63 .push_head()
64 .map_err(|e| GitProviderError::Walk(e.to_string()))?;
65 }
66
67 let mut records = Vec::new();
68 let max = limit.unwrap_or(usize::MAX);
69
70 for oid_result in revwalk {
71 if records.len() >= max {
72 break;
73 }
74
75 let oid = oid_result.map_err(|e| GitProviderError::Walk(e.to_string()))?;
76 let commit = repo
77 .find_commit(oid)
78 .map_err(|e| GitProviderError::NotFound(e.to_string()))?;
79
80 let signature_status = classify_signature(&repo, &commit);
81
82 let author = commit.author();
83 records.push(CommitRecord {
84 hash: oid.to_string()[..7].to_string(),
85 author_name: author.name().unwrap_or("unknown").to_string(),
86 author_email: author.email().unwrap_or("").to_string(),
87 timestamp: format_commit_time(&commit),
88 message: commit.summary().unwrap_or("").to_string(),
89 signature_status,
90 });
91 }
92
93 Ok(records)
94 }
95}
96
97fn classify_signature(repo: &git2::Repository, commit: &git2::Commit) -> SignatureStatus {
98 match repo.extract_signature(&commit.id(), None) {
99 Ok(sig_buf) => {
100 let sig_bytes = sig_buf.0;
101 let sig_str = String::from_utf8_lossy(sig_bytes.as_ref());
102
103 if sig_str.contains("-----BEGIN SSH SIGNATURE-----") {
104 if sig_str.contains("auths") {
106 SignatureStatus::AuthsSigned {
107 signer_did: String::new(),
108 }
109 } else {
110 SignatureStatus::SshSigned
111 }
112 } else if sig_str.contains("-----BEGIN PGP SIGNATURE-----") {
113 SignatureStatus::GpgSigned { verified: false }
114 } else {
115 SignatureStatus::InvalidSignature {
116 reason: "unknown signature format".to_string(),
117 }
118 }
119 }
120 Err(_) => SignatureStatus::Unsigned,
121 }
122}
123
124fn format_commit_time(commit: &git2::Commit) -> String {
125 let time = commit.time();
126 let secs = time.seconds();
127 let offset_minutes = time.offset_minutes();
128 let offset_hours = offset_minutes / 60;
129 let offset_mins = (offset_minutes % 60).abs();
130 let sign = if offset_minutes >= 0 { '+' } else { '-' };
131
132 format!(
135 "{}{}{}:{:02}",
136 chrono::DateTime::from_timestamp(secs, 0)
137 .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S").to_string())
138 .unwrap_or_else(|| secs.to_string()),
139 sign,
140 offset_hours.abs(),
141 offset_mins
142 )
143}