git_hist/app/
commit.rs

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                    // use https://doc.rust-lang.org/std/option/enum.Option.html#method.contains in the future
100                    .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}