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(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    /// create new `CommitId`
22    pub const fn new(id: Oid) -> Self {
23        Self(id)
24    }
25
26    ///
27    pub(crate) const fn get_oid(self) -> Oid {
28        self.0
29    }
30
31    /// 7 chars short hash
32    pub fn get_short_string(&self) -> String {
33        self.to_string().chars().take(7).collect()
34    }
35    ///
36    pub fn get_padded_hash_string(&self) -> String {
37        format!("{:0>64}", self.to_string())
38        //self.to_string().chars().take(7).collect()
39    }
40    ///
41    pub fn get_padded_short_hash_string(&self) -> String {
42        format!("{:0>64}", self.get_short_string())
43        //self.to_string().chars().take(7).collect()
44    }
45    ///
46    pub fn get_keys(&self) -> String {
47        format!("{:0>64}", self.get_short_string())
48        //self.to_string().chars().take(7).collect()
49    }
50
51    /// Tries to retrieve the `CommitId` form the revision if exists
52    /// in the given repository
53    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///
82#[derive(Debug)]
83pub struct CommitInfo {
84    ///
85    pub message: String,
86    ///
87    pub time: i64,
88    ///
89    pub author: String,
90    ///
91    pub id: CommitId,
92}
93
94///
95pub 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
129///
130pub 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
146/// if `message_limit` is set the message will be
147/// limited to the first line and truncated to fit
148pub 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}