Skip to main content

git_spawn/
history.rs

1//! Walk a repository's commit history into typed [`Commit`] structs.
2//!
3//! Reached through [`Repository::history`], which returns a [`HistoryWalk`]
4//! builder. Configure with `revision`, `max_count`, `since`, `author`, etc.,
5//! then call [`HistoryWalk::execute`] to spawn `git log` with a stable
6//! `--format` and parse the output into `Vec<Commit>`.
7//!
8//! ```no_run
9//! # async fn ex() -> git_spawn::Result<()> {
10//! use git_spawn::Repository;
11//!
12//! let repo = Repository::open("/path/to/repo")?;
13//!
14//! // Last 20 commits authored by Alice.
15//! let commits = repo
16//!     .history()
17//!     .max_count(20)
18//!     .author("Alice")
19//!     .execute()
20//!     .await?;
21//! for c in commits {
22//!     println!("{} {} {}", c.short_sha, c.author_name, c.subject);
23//! }
24//! # Ok(())
25//! # }
26//! ```
27//!
28//! Parsing reuses the [`crate::parse::parse_log`] machinery and the
29//! [`crate::parse::LOG_FORMAT`] token string. [`Commit`] is a re-export of
30//! [`crate::parse::CommitEntry`] under a friendlier name for the workflow API.
31
32use crate::command::GitCommand;
33use crate::command::log::LogCommand;
34use crate::error::Result;
35use crate::parse::{LOG_FORMAT, parse_log};
36use crate::repo::Repository;
37
38pub use crate::parse::CommitEntry as Commit;
39
40/// Builder for one history walk. Configure with the chained setters, then
41/// call [`execute`](Self::execute).
42#[derive(Debug)]
43pub struct HistoryWalk<'a> {
44    repo: &'a Repository,
45    revisions: Vec<String>,
46    paths: Vec<String>,
47    max_count: Option<u32>,
48    skip: Option<u32>,
49    since: Option<String>,
50    until: Option<String>,
51    author: Option<String>,
52    grep: Option<String>,
53    reverse: bool,
54}
55
56impl<'a> HistoryWalk<'a> {
57    fn new(repo: &'a Repository) -> Self {
58        Self {
59            repo,
60            revisions: Vec::new(),
61            paths: Vec::new(),
62            max_count: None,
63            skip: None,
64            since: None,
65            until: None,
66            author: None,
67            grep: None,
68            reverse: false,
69        }
70    }
71
72    /// Limit results to the most recent `n` commits.
73    #[must_use]
74    pub fn max_count(mut self, n: u32) -> Self {
75        self.max_count = Some(n);
76        self
77    }
78
79    /// Skip the first `n` commits before collecting.
80    #[must_use]
81    pub fn skip(mut self, n: u32) -> Self {
82        self.skip = Some(n);
83        self
84    }
85
86    /// Filter by `--since` (any value `git log` accepts, e.g. `"2.weeks.ago"`).
87    #[must_use]
88    pub fn since(mut self, s: impl Into<String>) -> Self {
89        self.since = Some(s.into());
90        self
91    }
92
93    /// Filter by `--until`.
94    #[must_use]
95    pub fn until(mut self, s: impl Into<String>) -> Self {
96        self.until = Some(s.into());
97        self
98    }
99
100    /// Filter by author (`--author`).
101    #[must_use]
102    pub fn author(mut self, s: impl Into<String>) -> Self {
103        self.author = Some(s.into());
104        self
105    }
106
107    /// Filter by commit-message grep (`--grep`).
108    #[must_use]
109    pub fn grep(mut self, s: impl Into<String>) -> Self {
110        self.grep = Some(s.into());
111        self
112    }
113
114    /// Add a revision, range, or ref (e.g. `"HEAD~10..HEAD"`).
115    /// Multiple calls accumulate.
116    #[must_use]
117    pub fn revision(mut self, r: impl Into<String>) -> Self {
118        self.revisions.push(r.into());
119        self
120    }
121
122    /// Restrict to commits touching `path`. Multiple calls accumulate.
123    #[must_use]
124    pub fn path(mut self, p: impl Into<String>) -> Self {
125        self.paths.push(p.into());
126        self
127    }
128
129    /// Reverse the output order (`--reverse`).
130    #[must_use]
131    pub fn reverse(mut self) -> Self {
132        self.reverse = true;
133        self
134    }
135
136    /// Spawn `git log` with the configured filters and parse the result.
137    pub async fn execute(self) -> Result<Vec<Commit>> {
138        let mut cmd = LogCommand::new();
139        cmd.format(LOG_FORMAT);
140        if let Some(n) = self.max_count {
141            cmd.max_count(n);
142        }
143        if let Some(n) = self.skip {
144            cmd.skip(n);
145        }
146        if let Some(s) = self.since {
147            cmd.since(s);
148        }
149        if let Some(s) = self.until {
150            cmd.until(s);
151        }
152        if let Some(s) = self.author {
153            cmd.author(s);
154        }
155        if let Some(s) = self.grep {
156            cmd.grep(s);
157        }
158        if self.reverse {
159            cmd.reverse();
160        }
161        for r in self.revisions {
162            cmd.revision(r);
163        }
164        for p in self.paths {
165            cmd.path(p);
166        }
167        cmd.current_dir(self.repo.path());
168        let out = cmd.execute().await?;
169        parse_log(&out.stdout)
170    }
171}
172
173impl Repository {
174    /// Walk commit history with a chained-builder filter.
175    #[must_use]
176    pub fn history(&self) -> HistoryWalk<'_> {
177        HistoryWalk::new(self)
178    }
179}