1use chrono::TimeZone;
2use git2::{Commit as GitCommit, Oid, Repository};
3use itertools::Itertools;
4use once_cell::sync::OnceCell;
5use std::{collections::HashMap, fmt};
6
7const HEAD_NAME: &str = "HEAD";
8
9pub struct Commit<'a> {
10 oid: Oid,
11 short_id: String,
12 long_id: String,
13 author_name: String,
14 author_date: chrono::DateTime<chrono::Local>,
15 committer_name: String,
16 committer_date: chrono::DateTime<chrono::Local>,
17 summary: String,
18 references: OnceCell<References>,
19 repo: &'a Repository,
20}
21
22impl<'a> Commit<'a> {
23 pub fn new(commit: &GitCommit, repo: &'a Repository) -> Self {
24 let oid = commit.id();
25 let short_id = commit
26 .as_object()
27 .short_id()
28 .unwrap()
29 .as_str()
30 .unwrap_or_default()
31 .to_string();
32 let long_id = format!("{}", oid);
33 let author = commit.author().name().unwrap_or_default().to_string();
34 let author_date = chrono::DateTime::<chrono::Local>::from(
35 chrono::Utc.timestamp(commit.author().when().seconds(), 0),
36 );
37 let committer = commit.committer().name().unwrap_or_default().to_string();
38 let committer_date = chrono::DateTime::<chrono::Local>::from(
39 chrono::Utc.timestamp(commit.committer().when().seconds(), 0),
40 );
41 let summary = commit.summary().unwrap_or_default().to_string();
42
43 Self {
44 oid,
45 short_id,
46 long_id,
47 author_name: author,
48 author_date,
49 committer_name: committer,
50 committer_date,
51 summary,
52 references: OnceCell::new(),
53 repo,
54 }
55 }
56
57 pub fn short_id(&self) -> &str {
58 &self.short_id
59 }
60
61 pub fn long_id(&self) -> &str {
62 &self.long_id
63 }
64
65 pub fn author_name(&self) -> &str {
66 &self.author_name
67 }
68
69 pub fn author_date(&self) -> &chrono::DateTime<chrono::Local> {
70 &self.author_date
71 }
72
73 pub fn committer_name(&self) -> &str {
74 &self.committer_name
75 }
76
77 pub fn committer_date(&self) -> &chrono::DateTime<chrono::Local> {
78 &self.committer_date
79 }
80
81 pub fn summary(&self) -> &str {
82 &self.summary
83 }
84
85 pub fn references(&self) -> &References {
86 self.references.get_or_init(|| self.calc_references())
87 }
88
89 fn calc_references(&self) -> References {
90 let head = self.repo.head().unwrap();
91
92 let references = self
93 .repo
94 .references()
95 .unwrap()
96 .filter_map(|r| r.ok())
97 .filter(|r| {
98 r.target()
99 .filter(|oid| *oid == self.oid)
101 .is_some()
102 });
103 let reference_groups: HashMap<ReferenceType, Vec<_>> =
104 references.into_group_map_by(|r| match r {
105 _ if r.is_branch() => ReferenceType::LocalBranch,
106 _ if r.is_remote() => ReferenceType::RemoteBranch,
107 _ if r.is_tag() => ReferenceType::Tag,
108 _ => unreachable!(),
109 });
110
111 let local_branches: Vec<LocalBranch> = reference_groups
112 .get(&ReferenceType::LocalBranch)
113 .map(|rs| rs.iter().collect::<Vec<_>>())
114 .unwrap_or_else(Vec::new)
115 .iter()
116 .filter_map(|r| {
117 r.shorthand()
118 .map(|name| LocalBranch::new(name, r.name() == head.name()))
119 })
120 .collect();
121
122 let remote_branches: Vec<RemoteBranch> = reference_groups
123 .get(&ReferenceType::RemoteBranch)
124 .map(|rs| rs.iter().collect::<Vec<_>>())
125 .unwrap_or_else(Vec::new)
126 .iter()
127 .filter_map(|r| r.shorthand().map(RemoteBranch::new))
128 .collect();
129
130 let tags: Vec<Tag> = reference_groups
131 .get(&ReferenceType::Tag)
132 .map(|rs| rs.iter().collect::<Vec<_>>())
133 .unwrap_or_else(Vec::new)
134 .iter()
135 .filter_map(|r| r.shorthand().map(Tag::new))
136 .collect();
137
138 let is_head = head.target().unwrap() == self.oid && head.name() == Some(HEAD_NAME);
139
140 References::new(local_branches, remote_branches, tags, is_head)
141 }
142}
143
144#[derive(Debug)]
145pub struct References {
146 local_branches: Vec<LocalBranch>,
147 remote_branches: Vec<RemoteBranch>,
148 tags: Vec<Tag>,
149 is_head: bool,
150}
151
152impl References {
153 pub fn new(
154 local_branches: Vec<LocalBranch>,
155 remote_branches: Vec<RemoteBranch>,
156 tags: Vec<Tag>,
157 is_head: bool,
158 ) -> Self {
159 Self {
160 local_branches,
161 remote_branches,
162 tags,
163 is_head,
164 }
165 }
166
167 pub fn is_empty(&self) -> bool {
168 self.local_branches.is_empty()
169 && self.remote_branches.is_empty()
170 && self.tags.is_empty()
171 && !self.is_head
172 }
173
174 pub fn head_names(&self) -> Vec<String> {
175 if self.is_head {
176 vec![String::from(HEAD_NAME)]
177 } else {
178 vec![]
179 }
180 }
181
182 pub fn local_branch_names(&self) -> Vec<String> {
183 self.local_branches
184 .iter()
185 .map(|x| format!("{}", x))
186 .collect()
187 }
188
189 pub fn remote_branch_names(&self) -> Vec<String> {
190 self.remote_branches
191 .iter()
192 .map(|x| format!("{}", x))
193 .collect()
194 }
195
196 pub fn tag_names(&self) -> Vec<String> {
197 self.tags.iter().map(|x| format!("{}", x)).collect()
198 }
199}
200
201#[derive(Debug, PartialEq, Eq, Hash)]
202enum ReferenceType {
203 LocalBranch,
204 RemoteBranch,
205 Tag,
206}
207
208#[derive(Debug)]
209pub struct LocalBranch {
210 name: String,
211 is_head: bool,
212}
213
214impl LocalBranch {
215 pub fn new(name: impl Into<String>, is_head: bool) -> Self {
216 Self {
217 name: name.into(),
218 is_head,
219 }
220 }
221}
222
223impl fmt::Display for LocalBranch {
224 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
225 if self.is_head {
226 write!(f, "{} -> {}", HEAD_NAME, self.name)
227 } else {
228 write!(f, "{}", self.name)
229 }
230 }
231}
232
233#[derive(Debug)]
234pub struct RemoteBranch {
235 name: String,
236}
237
238impl RemoteBranch {
239 pub fn new(name: impl Into<String>) -> Self {
240 Self { name: name.into() }
241 }
242}
243
244impl fmt::Display for RemoteBranch {
245 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
246 write!(f, "{}", self.name)
247 }
248}
249
250#[derive(Debug)]
251pub struct Tag {
252 name: String,
253}
254
255impl Tag {
256 pub fn new(name: impl Into<String>) -> Self {
257 Self { name: name.into() }
258 }
259}
260
261impl fmt::Display for Tag {
262 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
263 write!(f, "tag: {}", self.name)
264 }
265}