radicle_surf/history.rs
1// This file is part of radicle-surf
2// <https://github.com/radicle-dev/radicle-git>
3//
4// Copyright (C) 2022 The Radicle Team <dev@radicle.xyz>
5//
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License version 3 or
8// later as published by the Free Software Foundation.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18use std::{
19 convert::TryFrom,
20 path::{Path, PathBuf},
21};
22
23use crate::{Commit, Error, Repository, ToCommit};
24
25/// An iterator that produces the history of commits for a given `head`.
26///
27/// The lifetime of this struct is attached to the underlying [`Repository`].
28pub struct History<'a> {
29 repo: &'a Repository,
30 head: Commit,
31 revwalk: git2::Revwalk<'a>,
32 filter_by: Option<FilterBy>,
33}
34
35/// Internal implementation, subject to refactoring.
36enum FilterBy {
37 File { path: PathBuf },
38}
39
40impl<'a> History<'a> {
41 /// Creates a new history starting from `head`, in `repo`.
42 pub(crate) fn new<C: ToCommit>(repo: &'a Repository, head: C) -> Result<Self, Error> {
43 let head = head
44 .to_commit(repo)
45 .map_err(|err| Error::ToCommit(err.into()))?;
46 let mut revwalk = repo.revwalk()?;
47 revwalk.push(head.id.into())?;
48 let history = Self {
49 repo,
50 head,
51 revwalk,
52 filter_by: None,
53 };
54 Ok(history)
55 }
56
57 /// Returns the first commit (i.e. the head) in the history.
58 pub fn head(&self) -> &Commit {
59 &self.head
60 }
61
62 /// Returns a modified `History` filtered by `path`.
63 ///
64 /// Note that it is possible that a filtered History becomes empty,
65 /// even though calling `.head()` still returns the original head.
66 pub fn by_path<P>(mut self, path: &P) -> Self
67 where
68 P: AsRef<Path>,
69 {
70 self.filter_by = Some(FilterBy::File {
71 path: path.as_ref().to_path_buf(),
72 });
73 self
74 }
75}
76
77impl Iterator for History<'_> {
78 type Item = Result<Commit, Error>;
79
80 fn next(&mut self) -> Option<Self::Item> {
81 // Loop through the commits with the optional filtering.
82 while let Some(oid) = self.revwalk.next() {
83 let found = oid
84 .map_err(Error::Git)
85 .and_then(|oid| {
86 let commit = self.repo.find_commit(oid.into())?;
87
88 // Handles the optional filter_by.
89 if let Some(FilterBy::File { path }) = &self.filter_by {
90 // Only check the commit diff if the path is not empty.
91 if !path.as_os_str().is_empty() {
92 let path_opt = self.repo.diff_commit_and_parents(path, &commit)?;
93 if path_opt.is_none() {
94 return Ok(None); // Filter out this commit.
95 }
96 }
97 }
98
99 let commit = Commit::try_from(commit)?;
100 Ok(Some(commit))
101 })
102 .transpose();
103 if found.is_some() {
104 return found;
105 }
106 }
107 None
108 }
109}
110
111impl std::fmt::Debug for History<'_> {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 write!(f, "History of {}", self.head.id)
114 }
115}