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