git_commits/
lib.rs

1//! Abstraction of [`git2`] providing a simple interface
2//! for easily iterating over commits and changes in a
3//! Git repository.
4//!
5//! In short, both `git log --name-status` and
6//! `git log --stat --format=fuller` can be
7//! implemented with just a handful of lines.
8//!
9//! [`git2`]: https://crates.io/crates/git2
10//!
11//! # Example
12//!
13//! ```no_run
14//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
15//! let repo = git_commits::open("path-to-repo")?;
16//!
17//! for commit in repo.commits()? {
18//!     // The `commit` contains the message, author, committer, time, etc
19//!     let commit = commit?;
20//!     println!("\n{}", commit);
21//!
22//!     for change in commit.changes()? {
23//!         // The `change` contains change kind, old/new path, old/new sizes, etc
24//!         let change = change?;
25//!         println!("  {}", change);
26//!
27//!         // match change {
28//!         //     Change::Added(change) => {}
29//!         //     Change::Modified(change) => {}
30//!         //     Change::Deleted(change) => {}
31//!         //     Change::Renamed(change) => {}
32//!         // }
33//!     }
34//! }
35//! #     Ok(())
36//! # }
37//! ```
38
39#![forbid(unsafe_code, elided_lifetimes_in_paths)]
40
41mod change;
42mod changes;
43mod commit;
44
45pub use git2::Error as GitError;
46pub use git2::Sort;
47
48pub use crate::change::{Added, Change, ChangeKind, Deleted, Modified, Renamed};
49pub use crate::changes::Changes;
50pub use crate::commit::{Commit, Signature};
51
52use std::iter::FusedIterator;
53use std::path::Path;
54
55use git2::{Repository, Revwalk};
56
57#[inline]
58pub fn open(path: impl AsRef<Path>) -> Result<Repo, GitError> {
59    Repo::open(path)
60}
61
62pub struct Repo(Repository);
63
64impl Repo {
65    /// Attempt to open an already-existing repository at `path`.
66    ///
67    /// The path can point to either a normal or bare repository.
68    #[inline]
69    pub fn open(path: impl AsRef<Path>) -> Result<Self, GitError> {
70        let repo = Repository::open(path)?;
71        Ok(Self(repo))
72    }
73
74    /// Attempt to open an already-existing repository at or above `path`.
75    ///
76    /// This starts at `path` and looks up the filesystem hierarchy
77    /// until it finds a repository.
78    #[inline]
79    pub fn discover(path: impl AsRef<Path>) -> Result<Self, GitError> {
80        let repo = Repository::discover(path)?;
81        Ok(Self(repo))
82    }
83
84    /// Returns an iterator that produces all commits
85    /// in the repo.
86    ///
87    /// _See [`.commits_ext()`](Repo::commits_ext) to be
88    /// able to specify the order._
89    #[inline]
90    pub fn commits(&self) -> Result<Commits<'_>, GitError> {
91        self.commits_ext(Sort::NONE)
92    }
93
94    /// Returns an iterator that produces all commits
95    /// in the repo.
96    #[inline]
97    pub fn commits_ext(&self, sort: Sort) -> Result<Commits<'_>, GitError> {
98        Commits::new(&self.0, sort)
99    }
100}
101
102pub struct Commits<'repo> {
103    repo: &'repo Repository,
104    revwalk: Revwalk<'repo>,
105}
106
107impl<'repo> Commits<'repo> {
108    fn new(repo: &'repo Repository, sort: Sort) -> Result<Self, GitError> {
109        let mut revwalk = repo.revwalk()?;
110        revwalk.push_head()?;
111        revwalk.set_sorting(sort)?;
112
113        Ok(Self { repo, revwalk })
114    }
115}
116
117impl<'repo> Iterator for Commits<'repo> {
118    type Item = Result<Commit<'repo>, GitError>;
119
120    fn next(&mut self) -> Option<Self::Item> {
121        let oid = self.revwalk.next()?;
122        let oid = match oid {
123            Ok(oid) => oid,
124            Err(err) => return Some(Err(err)),
125        };
126
127        let commit = match self.repo.find_commit(oid) {
128            Ok(commit) => commit,
129            Err(err) => return Some(Err(err)),
130        };
131
132        let commit = Commit::new(self.repo, commit);
133
134        Some(Ok(commit))
135    }
136}
137
138impl FusedIterator for Commits<'_> {}