gnostr_asyncgit/sync/
commit_details.rs1use git2::ObjectType;
2use git2::Signature;
3
4use nostr_sdk::prelude::*;
6use scopetime::scope_time;
7
8use super::{commits_info::get_message, CommitId, RepoPath};
9use crate::{error::Result, sync::repository::repo};
10
11#[derive(Debug, PartialEq, Eq, Default, Clone)]
13pub struct CommitSignature {
14 pub name: String,
16 pub email: String,
18 pub time: i64,
20}
21
22impl CommitSignature {
23 pub fn from(s: &Signature<'_>) -> Self {
25 Self {
26 name: s.name().unwrap_or("").to_string(),
27 email: s.email().unwrap_or("").to_string(),
28
29 time: s.when().seconds(),
30 }
31 }
32}
33
34#[derive(Default, Clone)]
36pub struct CommitMessage {
37 pub subject: String,
39 pub body: Option<String>,
41}
42
43impl CommitMessage {
44 pub fn from(s: &str) -> Self {
46 let mut lines = s.lines();
47 let subject = lines
48 .next()
49 .map_or_else(String::new, std::string::ToString::to_string);
50
51 let body: Vec<String> = lines.map(std::string::ToString::to_string).collect();
52
53 Self {
54 subject,
55 body: if body.is_empty() {
56 None
57 } else {
58 Some(body.join("\n"))
59 },
60 }
61 }
62
63 pub fn combine(self) -> String {
65 if let Some(body) = self.body {
66 format!("{}\n{body}", self.subject)
67 } else {
68 self.subject
69 }
70 }
71}
72
73#[derive(Default, Clone)]
75pub struct CommitDetails {
76 pub author: CommitSignature,
78 pub committer: Option<CommitSignature>,
80 pub message: Option<CommitMessage>,
82 pub hash: String,
84 pub keys: Option<Keys>,
86}
87
88impl CommitDetails {
89 pub fn short_hash(&self) -> &str {
91 &self.hash[0..7]
92 }
93 pub(crate) fn pad_commit_hash(input: &str, padding_char: char) -> String {
95 let target_length = 64;
96 let current_length = input.len();
97 if current_length >= target_length {
98 return input.to_string(); }
100 let padding_needed = target_length - current_length;
101 let padding = padding_char.to_string().repeat(padding_needed);
102 format!("{}{}", input, padding)
103 }
104 pub fn padded_hash(&self) -> String {
106 Self::pad_commit_hash(&self.hash, '0')
107 }
109 pub fn padded_short_hash(&self) -> String {
111 Self::pad_commit_hash(&self.hash[0..7], '0')
112 }
113 pub fn keys(&self) -> Result<Keys> {
115 Ok(Keys::parse(Self::pad_commit_hash(&self.hash, '0')).unwrap())
116 }
117}
118
119pub fn get_commit_details(repo_path: &RepoPath, id: CommitId) -> Result<CommitDetails> {
121 scope_time!("get_commit_details");
122
123 let repo = repo(repo_path)?;
124 let head = repo.head()?;
125 let obj = head.resolve()?.peel(ObjectType::Commit)?;
126
127 let commit = obj.peel_to_commit()?;
129 let commit_id = commit.id().to_string();
130 let padded_commit_id = format!("{:0>64}", commit_id);
133
134 let keys = Keys::parse(padded_commit_id).unwrap();
141
142 let commit = repo.find_commit(id.into())?;
143
144 let author = CommitSignature::from(&commit.author());
145 let committer = CommitSignature::from(&commit.committer());
146 let committer = if author == committer {
147 None
148 } else {
149 Some(committer)
150 };
151
152 let msg = CommitMessage::from(get_message(&commit, None).as_str());
153
154 let details = CommitDetails {
155 author,
156 committer,
157 message: Some(msg),
158 hash: id.to_string(),
159 keys: Some(keys),
160 };
161
162 Ok(details)
163}
164
165#[cfg(test)]
166mod tests {
167 use std::{fs::File, io::Write, path::Path};
168
169 use super::{get_commit_details, CommitMessage};
170 use crate::{
171 error::Result,
172 sync::{commit, stage_add_file, tests::repo_init_empty, RepoPath},
173 };
174
175 #[test]
176 fn test_msg_invalid_utf8() -> Result<()> {
177 let file_path = Path::new("foo");
178 let (_td, repo) = repo_init_empty().unwrap();
179 let root = repo.path().parent().unwrap();
180 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
181
182 File::create(root.join(file_path))?.write_all(b"a")?;
183 stage_add_file(repo_path, file_path).unwrap();
184
185 let msg = invalidstring::invalid_utf8("test msg");
186 let id = commit(repo_path, msg.as_str()).unwrap();
187
188 let res = get_commit_details(repo_path, id).unwrap();
189
190 assert_eq!(
191 res.message
192 .as_ref()
193 .unwrap()
194 .subject
195 .starts_with("test msg"),
196 true
197 );
198
199 Ok(())
200 }
201
202 #[test]
203 fn test_msg_linefeeds() -> Result<()> {
204 let msg = CommitMessage::from("foo\nbar\r\ntest");
205
206 assert_eq!(msg.subject, String::from("foo"),);
207 assert_eq!(msg.body, Some(String::from("bar\ntest")),);
208
209 Ok(())
210 }
211
212 #[test]
213 fn test_commit_message_combine() -> Result<()> {
214 let msg = CommitMessage::from("foo\nbar\r\ntest");
215
216 assert_eq!(msg.combine(), String::from("foo\nbar\ntest"));
217
218 Ok(())
219 }
220}