gnostr_asyncgit/sync/
commits_info.rs1use std::fmt::Display;
2
3use git2::{Commit, Error, Oid};
4use scopetime::scope_time;
5use unicode_truncate::UnicodeTruncateStr;
6
7use super::RepoPath;
8use crate::{error::Result, sync::repository::repo};
9
10#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
12pub struct CommitId(Oid);
13
14impl Default for CommitId {
15 fn default() -> Self {
16 Self(Oid::zero())
17 }
18}
19
20impl CommitId {
21 pub const fn new(id: Oid) -> Self {
23 Self(id)
24 }
25
26 pub(crate) const fn get_oid(self) -> Oid {
28 self.0
29 }
30
31 pub fn get_short_string(&self) -> String {
33 self.to_string().chars().take(7).collect()
34 }
35 pub fn get_padded_hash_string(&self) -> String {
37 format!("{:0>64}", self.to_string())
38 }
40 pub fn get_padded_short_hash_string(&self) -> String {
42 format!("{:0>64}", self.get_short_string())
43 }
45 pub fn get_keys(&self) -> String {
47 format!("{:0>64}", self.get_short_string())
48 }
50
51 pub fn from_revision(repo_path: &RepoPath, revision: &str) -> Result<Self> {
54 scope_time!("CommitId::from_revision");
55
56 let repo = repo(repo_path)?;
57
58 let commit_obj = repo.revparse_single(revision)?;
59 Ok(commit_obj.id().into())
60 }
61}
62
63impl Display for CommitId {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 write!(f, "{}", self.0)
66 }
67}
68
69impl From<CommitId> for Oid {
70 fn from(id: CommitId) -> Self {
71 id.0
72 }
73}
74
75impl From<Oid> for CommitId {
76 fn from(id: Oid) -> Self {
77 Self::new(id)
78 }
79}
80
81#[derive(Debug)]
83pub struct CommitInfo {
84 pub message: String,
86 pub time: i64,
88 pub author: String,
90 pub id: CommitId,
92}
93
94pub fn get_commits_info(
96 repo_path: &RepoPath,
97 ids: &[CommitId],
98 message_length_limit: usize,
99) -> Result<Vec<CommitInfo>> {
100 scope_time!("get_commits_info");
101
102 let repo = repo(repo_path)?;
103
104 let commits = ids
105 .iter()
106 .map(|id| repo.find_commit((*id).into()))
107 .collect::<std::result::Result<Vec<Commit>, Error>>()?
108 .into_iter();
109
110 let res = commits
111 .map(|c: Commit| {
112 let message = get_message(&c, Some(message_length_limit));
113 let author = c
114 .author()
115 .name()
116 .map_or_else(|| String::from("<unknown>"), String::from);
117 CommitInfo {
118 message,
119 author,
120 time: c.time().seconds(),
121 id: CommitId(c.id()),
122 }
123 })
124 .collect::<Vec<_>>();
125
126 Ok(res)
127}
128
129pub fn get_commit_info(repo_path: &RepoPath, commit_id: &CommitId) -> Result<CommitInfo> {
131 scope_time!("get_commit_info");
132
133 let repo = repo(repo_path)?;
134
135 let commit = repo.find_commit((*commit_id).into())?;
136 let author = commit.author();
137
138 Ok(CommitInfo {
139 message: commit.message().unwrap_or("").into(),
140 author: author.name().unwrap_or("<unknown>").into(),
141 time: commit.time().seconds(),
142 id: CommitId(commit.id()),
143 })
144}
145
146pub fn get_message(c: &Commit, message_limit: Option<usize>) -> String {
149 let msg = String::from_utf8_lossy(c.message_bytes());
150 let msg = msg.trim();
151
152 message_limit.map_or_else(
153 || msg.to_string(),
154 |limit| {
155 let msg = msg.lines().next().unwrap_or_default();
156 msg.unicode_truncate(limit).0.to_string()
157 },
158 )
159}
160
161#[cfg(test)]
162mod tests {
163 use std::{fs::File, io::Write, path::Path};
164
165 use super::get_commits_info;
166 use crate::{
167 error::Result,
168 sync::{
169 commit, stage_add_file, tests::repo_init_empty, utils::get_head_repo, CommitId,
170 RepoPath,
171 },
172 };
173
174 #[test]
175 fn test_log() -> Result<()> {
176 let file_path = Path::new("foo");
177 let (_td, repo) = repo_init_empty().unwrap();
178 let root = repo.path().parent().unwrap();
179 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
180
181 File::create(root.join(file_path))?.write_all(b"a")?;
182 stage_add_file(repo_path, file_path).unwrap();
183 let c1 = commit(repo_path, "commit1").unwrap();
184 File::create(root.join(file_path))?.write_all(b"a")?;
185 stage_add_file(repo_path, file_path).unwrap();
186 let c2 = commit(repo_path, "commit2").unwrap();
187
188 let res = get_commits_info(repo_path, &[c2, c1], 50).unwrap();
189
190 assert_eq!(res.len(), 2);
191 assert_eq!(res[0].message.as_str(), "commit2");
192 assert_eq!(res[0].author.as_str(), "name");
193 assert_eq!(res[1].message.as_str(), "commit1");
194
195 Ok(())
196 }
197
198 #[test]
199 fn test_log_first_msg_line() -> Result<()> {
200 let file_path = Path::new("foo");
201 let (_td, repo) = repo_init_empty().unwrap();
202 let root = repo.path().parent().unwrap();
203 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
204
205 File::create(root.join(file_path))?.write_all(b"a")?;
206 stage_add_file(repo_path, file_path).unwrap();
207 let c1 = commit(repo_path, "subject\nbody").unwrap();
208
209 let res = get_commits_info(repo_path, &[c1], 50).unwrap();
210
211 assert_eq!(res.len(), 1);
212 assert_eq!(res[0].message.as_str(), "subject");
213
214 Ok(())
215 }
216
217 #[test]
218 fn test_invalid_utf8() -> Result<()> {
219 let file_path = Path::new("foo");
220 let (_td, repo) = repo_init_empty().unwrap();
221 let root = repo.path().parent().unwrap();
222 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
223
224 File::create(root.join(file_path))?.write_all(b"a")?;
225 stage_add_file(repo_path, file_path).unwrap();
226
227 let msg = invalidstring::invalid_utf8("test msg");
228 commit(repo_path, msg.as_str()).unwrap();
229
230 let res = get_commits_info(repo_path, &[get_head_repo(&repo).unwrap()], 50).unwrap();
231
232 assert_eq!(res.len(), 1);
233 dbg!(&res[0].message);
234 assert_eq!(res[0].message.starts_with("test msg"), true);
235
236 Ok(())
237 }
238
239 #[test]
240 fn test_get_commit_from_revision() -> Result<()> {
241 let (_td, repo) = repo_init_empty().unwrap();
242 let root = repo.path().parent().unwrap();
243 let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
244
245 let foo_file = Path::new("foo");
246 File::create(root.join(foo_file))?.write_all(b"a")?;
247 stage_add_file(repo_path, foo_file).unwrap();
248 let c1 = commit(repo_path, "subject: foo\nbody").unwrap();
249 let c1_rev = c1.get_short_string();
250
251 assert_eq!(
252 CommitId::from_revision(repo_path, c1_rev.as_str()).unwrap(),
253 c1
254 );
255
256 const FOREIGN_HASH: &str = "d6d7d55cb6e4ba7301d6a11a657aab4211e5777e";
257 assert!(CommitId::from_revision(repo_path, FOREIGN_HASH).is_err());
258
259 Ok(())
260 }
261}