gitcc_git/
commit.rs

1//! Commit
2
3use git2::StatusOptions;
4use time::OffsetDateTime;
5
6use crate::{error::Error, util::convert_git2_time, GitRepository};
7
8/// A commit
9#[derive(Debug)]
10pub struct Commit {
11    /// ID (hash)
12    pub id: String,
13    /// Date
14    pub date: OffsetDateTime,
15    /// Author name
16    pub author_name: String,
17    /// Author email
18    pub author_email: String,
19    /// Committer name
20    pub committer_name: String,
21    /// Committer email
22    pub committer_email: String,
23    /// Message
24    pub message: String,
25}
26
27impl Commit {
28    /// Returns the commit subject (1st line)
29    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
72/// Returns the complete commit history
73///
74/// The returned list is ordered with the last commit first (revwalk order).
75pub fn commit_log(repo: &GitRepository) -> Result<Vec<Commit>, Error> {
76    // NB: an explanation can be found here (for Go)
77    // https://stackoverflow.com/questions/37289674/how-to-run-git-log-commands-using-libgit2-in-go
78    let mut revwalk = repo.revwalk()?;
79    revwalk.push_head()?;
80
81    // NB: revwalk starts with the last commit first
82    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                //eprintln!("{:?}", obj);
88                if let Some(c) = obj.as_commit() {
89                    // eprintln!("{:#?}", c);
90                    // NB: raw text values can be invalid if not UTF8
91                    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
100/// Performs a commit to the head
101pub fn commit_to_head(repo: &GitRepository, message: &str) -> Result<Commit, Error> {
102    // check for nothing to commit
103    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    // go
111    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        // NB: OK, here it is weird, the loaded index contains a bunch of stuff,
118        // but i cannot see the staged changes. I just load the index and writes the tree
119        // and it seems to work fine.
120        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    // Note this useful idiom: importing names from outer (for mod tests) scope.
135    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        // USE WITH CAUTION
157        // commit_to_head(&repo, "test commit").unwrap();
158    }
159}