gnostr_asyncgit/sync/
commits_info.rs

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/// identifies a single commit
11#[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	/// create new `CommitId`
24	pub const fn new(id: Oid) -> Self {
25		Self(id)
26	}
27
28	///
29	pub(crate) const fn get_oid(self) -> Oid {
30		self.0
31	}
32
33	/// 7 chars short hash
34	pub fn get_short_string(&self) -> String {
35		self.to_string().chars().take(7).collect()
36	}
37	///
38	pub fn get_padded_hash_string(&self) -> String {
39		format!("{:0>64}",self.to_string())
40		//self.to_string().chars().take(7).collect()
41	}
42	///
43	pub fn get_padded_short_hash_string(&self) -> String {
44		format!("{:0>64}", self.get_short_string())
45		//self.to_string().chars().take(7).collect()
46	}
47	///
48	pub fn get_keys(&self) -> String {
49		format!("{:0>64}", self.get_short_string())
50		//self.to_string().chars().take(7).collect()
51	}
52
53	/// Tries to retrieve the `CommitId` form the revision if exists
54	/// in the given repository
55	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///
90#[derive(Debug)]
91pub struct CommitInfo {
92	///
93	pub message: String,
94	///
95	pub time: i64,
96	///
97	pub author: String,
98	///
99	pub id: CommitId,
100}
101
102///
103pub 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
137///
138pub 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
157/// if `message_limit` is set the message will be
158/// limited to the first line and truncated to fit
159pub 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}