1use gix::ObjectId;
5
6use crate::error::{Error, Result};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub(crate) struct CommitInfo {
11 pub(crate) hash: String,
13 pub(crate) subject: String,
15 pub(crate) author: String,
17 pub(crate) timestamp_unix: i64,
19}
20
21pub(crate) fn abbrev_len(repo: &gix::Repository) -> usize {
24 repo.config_snapshot()
25 .integer("core.abbrev")
26 .and_then(|n| usize::try_from(n).ok())
27 .filter(|n| (4..=64).contains(n))
28 .unwrap_or(7)
29}
30
31pub(crate) fn commit_info(
33 repo: &gix::Repository,
34 oid_hex: &str,
35 abbrev: usize,
36) -> Result<CommitInfo> {
37 let id = ObjectId::from_hex(oid_hex.as_bytes())
38 .map_err(|e| Error::operation(format!("invalid object id {oid_hex:?}: {e}")))?;
39 let commit = repo
40 .find_object(id)
41 .map_err(|e| Error::operation(format!("cannot read object {oid_hex}: {e}")))?
42 .try_into_commit()
43 .map_err(|e| Error::operation(format!("object {oid_hex} is not a commit: {e}")))?;
44
45 let message = commit
46 .message()
47 .map_err(|e| Error::operation(format!("cannot decode commit message: {e}")))?;
48 let subject = message.summary().to_string();
49
50 let author = commit
51 .author()
52 .map_err(|e| Error::operation(format!("cannot decode commit author: {e}")))?;
53 let name = author.name.to_string();
54 let timestamp_unix = author.seconds();
55
56 let len = abbrev.clamp(4, oid_hex.len());
57 Ok(CommitInfo {
58 hash: oid_hex[..len].to_string(),
59 subject,
60 author: name,
61 timestamp_unix,
62 })
63}
64
65pub(crate) fn recent_commits(
69 repo: &gix::Repository,
70 start_hex: &str,
71 abbrev: usize,
72 max: usize,
73) -> Vec<CommitInfo> {
74 let Ok(id) = ObjectId::from_hex(start_hex.as_bytes()) else {
75 return Vec::new();
76 };
77 let Ok(walk) = repo.rev_walk([id]).all() else {
78 return Vec::new();
79 };
80 walk.take(max)
81 .filter_map(std::result::Result::ok)
82 .filter_map(|info| commit_info(repo, &info.id.to_string(), abbrev).ok())
83 .collect()
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use crate::git::discover::Repo;
90 use crate::testutil::TestRepo;
91
92 fn head_oid(repo: &TestRepo) -> String {
93 repo.git(&["rev-parse", "HEAD"]).trim().to_string()
94 }
95
96 #[test]
97 fn reads_subject_author_and_short_hash() {
98 let repo = TestRepo::init();
99 let oid = head_oid(&repo);
100 let r = Repo::discover(repo.root()).unwrap();
101 let info = commit_info(r.gix(), &oid, 7).unwrap();
102 assert_eq!(info.hash.len(), 7);
103 assert!(oid.starts_with(&info.hash));
104 assert_eq!(info.subject, "init");
105 assert_eq!(info.author, "wt Test");
106 assert!(info.timestamp_unix > 1_600_000_000);
107 }
108
109 #[test]
110 fn subject_is_first_line_only() {
111 let repo = TestRepo::init();
112 repo.write("f.txt", "x\n");
113 repo.git(&["add", "-A"]);
114 repo.git(&["commit", "-q", "-m", "summary line\n\nbody text"]);
115 let oid = head_oid(&repo);
116 let r = Repo::discover(repo.root()).unwrap();
117 let info = commit_info(r.gix(), &oid, 10).unwrap();
118 assert_eq!(info.subject, "summary line");
119 assert_eq!(info.hash.len(), 10);
120 }
121
122 #[test]
123 fn abbrev_len_defaults_to_seven() {
124 let repo = TestRepo::init();
125 repo.git(&["config", "core.abbrev", "auto"]);
128 let r = Repo::discover(repo.root()).unwrap();
129 assert_eq!(abbrev_len(r.gix()), 7);
130 repo.git(&["config", "core.abbrev", "12"]);
131 let r2 = Repo::discover(repo.root()).unwrap();
132 assert_eq!(abbrev_len(r2.gix()), 12);
133 }
134
135 #[test]
136 fn invalid_oid_errors() {
137 let repo = TestRepo::init();
138 let r = Repo::discover(repo.root()).unwrap();
139 assert!(commit_info(r.gix(), "not-hex", 7).is_err());
140 }
141
142 #[test]
143 fn recent_commits_walks_newest_first_and_caps() {
144 let repo = TestRepo::init(); repo.write("a.txt", "1\n");
146 repo.commit_all("second");
147 repo.write("b.txt", "2\n");
148 repo.commit_all("third");
149 let oid = head_oid(&repo);
150 let r = Repo::discover(repo.root()).unwrap();
151 let commits = recent_commits(r.gix(), &oid, 7, 5);
152 assert_eq!(commits.len(), 3);
153 assert_eq!(commits[0].subject, "third"); assert_eq!(commits[2].subject, "init");
155 assert_eq!(recent_commits(r.gix(), &oid, 7, 2).len(), 2);
157 assert!(recent_commits(r.gix(), "not-hex", 7, 5).is_empty());
159 }
160}