1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// This file is part of radicle-surf
// <https://github.com/radicle-dev/radicle-git>
//
// Copyright (C) 2022 The Radicle Team <dev@radicle.xyz>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 3 or
// later as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

use std::{
    convert::TryFrom,
    path::{Path, PathBuf},
};

use crate::{Commit, Error, Repository, ToCommit};

/// An iterator that produces the history of commits for a given `head`.
///
/// The lifetime of this struct is attached to the underlying [`Repository`].
pub struct History<'a> {
    repo: &'a Repository,
    head: Commit,
    revwalk: git2::Revwalk<'a>,
    filter_by: Option<FilterBy>,
}

/// Internal implementation, subject to refactoring.
enum FilterBy {
    File { path: PathBuf },
}

impl<'a> History<'a> {
    /// Creates a new history starting from `head`, in `repo`.
    pub(crate) fn new<C: ToCommit>(repo: &'a Repository, head: C) -> Result<Self, Error> {
        let head = head
            .to_commit(repo)
            .map_err(|err| Error::ToCommit(err.into()))?;
        let mut revwalk = repo.revwalk()?;
        revwalk.push(head.id.into())?;
        let history = Self {
            repo,
            head,
            revwalk,
            filter_by: None,
        };
        Ok(history)
    }

    /// Returns the first commit (i.e. the head) in the history.
    pub fn head(&self) -> &Commit {
        &self.head
    }

    /// Returns a modified `History` filtered by `path`.
    ///
    /// Note that it is possible that a filtered History becomes empty,
    /// even though calling `.head()` still returns the original head.
    pub fn by_path<P>(mut self, path: &P) -> Self
    where
        P: AsRef<Path>,
    {
        self.filter_by = Some(FilterBy::File {
            path: path.as_ref().to_path_buf(),
        });
        self
    }
}

impl<'a> Iterator for History<'a> {
    type Item = Result<Commit, Error>;

    fn next(&mut self) -> Option<Self::Item> {
        // Loop through the commits with the optional filtering.
        while let Some(oid) = self.revwalk.next() {
            let found = oid
                .map_err(Error::Git)
                .and_then(|oid| {
                    let commit = self.repo.find_commit(oid.into())?;

                    // Handles the optional filter_by.
                    if let Some(FilterBy::File { path }) = &self.filter_by {
                        // Only check the commit diff if the path is not empty.
                        if !path.as_os_str().is_empty() {
                            let path_opt = self.repo.diff_commit_and_parents(path, &commit)?;
                            if path_opt.is_none() {
                                return Ok(None); // Filter out this commit.
                            }
                        }
                    }

                    let commit = Commit::try_from(commit)?;
                    Ok(Some(commit))
                })
                .transpose();
            if found.is_some() {
                return found;
            }
        }
        None
    }
}

impl<'a> std::fmt::Debug for History<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "History of {}", self.head.id)
    }
}