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}