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}