Skip to main content

specfold_core/workspace/vcs/
first_add.rs

1//! When each path was first added, carried across renames, from one pass over the history and
2//! cached per repository. Asking git per file with --follow costs seconds each in a long history.
3
4use super::repo::{git, nearest_repo};
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use std::sync::{Mutex, OnceLock};
8
9fn first_adds(repo: &Path) -> HashMap<String, i64> {
10    let mut seen = HashMap::new();
11    let Some(out) =
12        git(repo, &["log", "--reverse", "-M", "--diff-filter=AR", "--format=%x1e%at", "--name-status", "-z"])
13    else {
14        return seen;
15    };
16    let text = String::from_utf8_lossy(&out);
17    for rec in text.split('\u{1e}').filter(|r| !r.is_empty()) {
18        let mut fields = rec.split('\0').map(|f| f.trim_start_matches('\n'));
19        let Some(secs) = fields.next().and_then(|h| h.trim().parse::<i64>().ok()) else { continue };
20        while let Some(status) = fields.next().filter(|s| !s.is_empty()) {
21            if status.starts_with('R') {
22                let (Some(from), Some(to)) = (fields.next(), fields.next()) else { break };
23                let first = seen.get(from).copied().unwrap_or(secs);
24                seen.entry(to.to_string()).or_insert(first);
25            } else if let Some(p) = fields.next() {
26                seen.entry(p.to_string()).or_insert(secs);
27            }
28        }
29    }
30    seen
31}
32
33fn first_add_secs(repo: &Path, rel: &str) -> Option<i64> {
34    static CACHE: OnceLock<Mutex<HashMap<PathBuf, HashMap<String, i64>>>> = OnceLock::new();
35    let mut cache = CACHE.get_or_init(Default::default).lock().ok()?;
36    cache.entry(repo.to_path_buf()).or_insert_with(|| first_adds(repo)).get(rel).copied()
37}
38
39/// The date of the first commit that added `path`, as `YYYYMMDDHHMMSS` UTC.
40pub fn first_commit_stamp(root: &Path, path: &str) -> Option<String> {
41    let (repo, rel, _) = nearest_repo(root, path);
42    let secs = first_add_secs(&repo, &rel)?;
43    let t = time::OffsetDateTime::from_unix_timestamp(secs).ok()?;
44    let f = time::macros::format_description!("[year][month][day][hour][minute][second]");
45    t.format(f).ok()
46}