Skip to main content

specfold_core/workspace/vcs/
history.rs

1//! History: commits in a range, a file's revisions and when governed code last changed.
2
3use super::repo::{git, nearest_repo};
4use std::path::Path;
5
6pub struct CommitText {
7    pub hash: String,
8    pub author: String,
9    pub message: String,
10}
11
12/// Commits in a range with their author and message.
13pub fn commits(root: &Path, range: &str) -> Option<Vec<CommitText>> {
14    let out = git(root, &["log", "--format=%H%x00%an <%ae>%x00%B%x1e", range])?;
15    let text = String::from_utf8_lossy(&out).into_owned();
16    Some(
17        text.split('\u{1e}')
18            .filter_map(|rec| {
19                let rec = rec.trim_start_matches('\n');
20                let mut parts = rec.splitn(3, '\0');
21                let hash = parts.next()?.to_string();
22                if hash.is_empty() {
23                    return None;
24                }
25                Some(CommitText {
26                    hash,
27                    author: parts.next()?.to_string(),
28                    message: parts.next().unwrap_or("").to_string(),
29                })
30            })
31            .collect(),
32    )
33}
34
35/// The date (YYYY-MM-DD) of the last commit touching any of `paths`, which may be globs.
36pub fn last_change(root: &Path, paths: &[String]) -> Option<String> {
37    let mut args: Vec<String> = vec!["log".into(), "-1".into(), "--format=%cs".into(), "--".into()];
38    args.extend(paths.iter().map(|p| if p.contains(['*', '?', '[']) { format!(":(glob){p}") } else { p.clone() }));
39    let refs: Vec<&str> = args.iter().map(String::as_str).collect();
40    let out = git(root, &refs)?;
41    let s = String::from_utf8_lossy(&out).trim().to_string();
42    if s.is_empty() { None } else { Some(s) }
43}
44
45pub struct Revision {
46    pub hash: String,
47    pub date: String,
48    pub author: String,
49    /// The file's path in that commit (it may have been renamed since).
50    pub path: String,
51}
52
53/// Every commit that touched `path`, oldest first, following renames.
54pub fn history(root: &Path, path: &str) -> Option<Vec<Revision>> {
55    let (repo, rel, prefix) = nearest_repo(root, path);
56    let out = git(&repo, &["log", "--follow", "--format=%x1e%H%x00%cs%x00%an", "--name-only", "--", &rel])?;
57    let text = String::from_utf8_lossy(&out).into_owned();
58    let mut revs: Vec<Revision> = text
59        .split('\u{1e}')
60        .filter_map(|rec| {
61            let mut lines = rec.lines();
62            let head = lines.next()?;
63            let mut parts = head.split('\0');
64            let hash = parts.next()?.to_string();
65            let date = parts.next()?.to_string();
66            let author = parts.next().unwrap_or("").to_string();
67            let path = format!("{prefix}{}", lines.find(|l| !l.trim().is_empty())?.trim());
68            Some(Revision { hash, date, author, path })
69        })
70        .collect();
71    revs.reverse();
72    Some(revs)
73}