1use git2::StatusOptions;
4use time::OffsetDateTime;
5
6use crate::{error::Error, util::convert_git2_time, GitRepository};
7
8#[derive(Debug)]
10pub struct Commit {
11 pub id: String,
13 pub date: OffsetDateTime,
15 pub author_name: String,
17 pub author_email: String,
19 pub committer_name: String,
21 pub committer_email: String,
23 pub message: String,
25}
26
27impl Commit {
28 pub fn subject(&self) -> String {
30 if let Some(line) = self.message.lines().next() {
31 return line.to_string();
32 }
33 unreachable!()
34 }
35}
36
37impl<'repo> TryFrom<&'repo git2::Commit<'repo>> for Commit {
38 type Error = Error;
39
40 fn try_from(c: &'repo git2::Commit) -> Result<Self, Self::Error> {
41 Ok(Commit {
42 id: c.id().to_string(),
43 date: convert_git2_time(c.time())?,
44 author_name: c
45 .author()
46 .name()
47 .ok_or(Error::msg("non UTF8 author name"))?
48 .to_string(),
49 author_email: c
50 .author()
51 .email()
52 .ok_or(Error::msg("non UTF8 author email"))?
53 .to_string(),
54 committer_name: c
55 .committer()
56 .name()
57 .ok_or(Error::msg("non UTF8 committer name"))?
58 .to_string(),
59 committer_email: c
60 .committer()
61 .email()
62 .ok_or(Error::msg("non UTF8 committer email"))?
63 .to_string(),
64 message: c
65 .message()
66 .ok_or(Error::msg("non UTF8 message"))?
67 .to_string(),
68 })
69 }
70}
71
72pub fn commit_log(repo: &GitRepository) -> Result<Vec<Commit>, Error> {
76 let mut revwalk = repo.revwalk()?;
79 revwalk.push_head()?;
80
81 let mut commits: Vec<_> = vec![];
83 for oid_res in revwalk {
84 match oid_res {
85 Ok(oid) => {
86 let obj = repo.find_object(oid, None).unwrap();
87 if let Some(c) = obj.as_commit() {
89 commits.push(c.try_into()?);
92 }
93 }
94 Err(err) => return Err(Error::msg(format!("{err} (revwalk)").as_str())),
95 }
96 }
97 Ok(commits)
98}
99
100pub fn commit_to_head(repo: &GitRepository, message: &str) -> Result<Commit, Error> {
102 let mut status_opts = StatusOptions::new();
104 status_opts.show(git2::StatusShow::Index);
105 let has_no_changes = repo.statuses(Some(&mut status_opts))?.is_empty();
106 if has_no_changes {
107 return Err(Error::msg("nothing to commit"));
108 }
109
110 let sig = repo.signature()?;
112 let update_ref = Some("HEAD");
113 let head = repo.head()?;
114 let head_commit = head.peel_to_commit()?;
115
116 let tree = {
117 let mut index = repo.index()?;
121 let tree_id = index.write_tree()?;
122 repo.find_tree(tree_id)?
123 };
124 let commit_id = repo.commit(update_ref, &sig, &sig, message, &tree, &[&head_commit])?;
125 let commit_obj = repo.find_object(commit_id, None)?;
126 let commit = commit_obj.as_commit().unwrap();
127 commit.try_into()
128}
129
130#[cfg(test)]
131mod tests {
132 use crate::repo::discover_repo;
133
134 use super::*;
136
137 #[test]
138 fn test_commit_log() {
139 let cwd = std::env::current_dir().unwrap();
140 let repo = discover_repo(&cwd).unwrap();
141 let commits = commit_log(&repo).unwrap();
142 for c in commits {
143 eprintln!("commit {}", c.id);
144 eprintln!("Date: {}", c.date);
145 eprintln!("Author: {} <{}>", c.author_name, c.author_email);
146 eprintln!("Committer: {} <{}>", c.committer_name, c.committer_email);
147 eprintln!("{}", c.message);
148 eprintln!();
149 }
150 }
151
152 #[test]
153 fn test_commit_do() {
154 let cwd = std::env::current_dir().unwrap();
155 let _repo = discover_repo(&cwd).unwrap();
156 }
159}