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