Skip to main content

git_lfs_git/
fetch_prune.rs

1//! Typed view of the `lfs.fetchrecent*` and `lfs.prune*` config knobs
2//! that govern fetch-recent and prune retention. Mirrors upstream's
3//! `lfs/config.go::FetchPruneConfig` field-for-field so the same
4//! defaults apply.
5
6use std::path::Path;
7
8use crate::config;
9
10/// Configuration for fetch-recent and prune retention. Built once per
11/// command via [`FetchPruneConfig::from_repo`]; pass by reference into
12/// the scanners + retention logic that consumes it.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct FetchPruneConfig {
15    /// Days prior to current date for which (local) refs other than
16    /// HEAD will be fetched with `--recent` (default 7, 0 = HEAD only).
17    pub fetch_recent_refs_days: i64,
18    /// Apply [`Self::fetch_recent_refs_days`] to remote-tracking refs
19    /// from the fetch source as well (default true).
20    pub fetch_recent_refs_include_remotes: bool,
21    /// Days prior to the latest commit on each kept ref to also fetch
22    /// previous LFS pre-images (default 0 = at-ref only).
23    pub fetch_recent_commits_days: i64,
24    /// If true, fetch acts as if `--recent` were always passed.
25    pub fetch_recent_always: bool,
26    /// Days added to the fetch-recent windows when computing prune
27    /// retention. Data outside the combined window can be pruned
28    /// (default 3).
29    pub prune_offset_days: i64,
30    /// Always verify with the remote before pruning reachable objects.
31    pub prune_verify_remote_always: bool,
32    /// When verifying, also verify unreachable objects (default false).
33    pub prune_verify_unreachable_always: bool,
34    /// Remote name used for unpushed checks and verify queries.
35    /// Defaults to `origin` if `lfs.pruneremotetocheck` isn't set.
36    pub prune_remote_name: String,
37}
38
39impl FetchPruneConfig {
40    /// Read every knob from git config under `cwd`, applying upstream's
41    /// defaults where unset. Reads via the effective git config (so
42    /// `.lfsconfig` overlays apply).
43    pub fn from_repo(cwd: &Path) -> Self {
44        Self {
45            fetch_recent_refs_days: get_int(cwd, "lfs.fetchrecentrefsdays", 7),
46            fetch_recent_refs_include_remotes: get_bool(cwd, "lfs.fetchrecentremoterefs", true),
47            fetch_recent_commits_days: get_int(cwd, "lfs.fetchrecentcommitsdays", 0),
48            fetch_recent_always: get_bool(cwd, "lfs.fetchrecentalways", false),
49            prune_offset_days: get_int(cwd, "lfs.pruneoffsetdays", 3),
50            prune_verify_remote_always: get_bool(cwd, "lfs.pruneverifyremotealways", false),
51            prune_verify_unreachable_always: get_bool(
52                cwd,
53                "lfs.pruneverifyunreachablealways",
54                false,
55            ),
56            prune_remote_name: get_string(cwd, "lfs.pruneremotetocheck", "origin"),
57        }
58    }
59}
60
61fn get_int(cwd: &Path, key: &str, default: i64) -> i64 {
62    config::get_effective(cwd, key)
63        .ok()
64        .flatten()
65        .and_then(|s| s.trim().parse().ok())
66        .unwrap_or(default)
67}
68
69/// Git-style boolean parsing: `true/yes/on/1` and `false/no/off/0` are
70/// recognized case-insensitively. Anything else falls back to `default`
71/// — upstream errors on garbage, we silently ignore (mirrors
72/// `lfs.skipdownloaderrors`-style read paths elsewhere).
73fn get_bool(cwd: &Path, key: &str, default: bool) -> bool {
74    let Ok(Some(raw)) = config::get_effective(cwd, key) else {
75        return default;
76    };
77    match raw.trim().to_ascii_lowercase().as_str() {
78        "true" | "yes" | "on" | "1" => true,
79        "false" | "no" | "off" | "0" | "" => false,
80        _ => default,
81    }
82}
83
84fn get_string(cwd: &Path, key: &str, default: &str) -> String {
85    let raw = config::get_effective(cwd, key).ok().flatten();
86    match raw {
87        Some(s) if !s.trim().is_empty() => s.trim().to_owned(),
88        _ => default.to_owned(),
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::tests::commit_helper;
96
97    fn set(cwd: &Path, key: &str, value: &str) {
98        std::process::Command::new("git")
99            .arg("-C")
100            .arg(cwd)
101            .args(["config", key, value])
102            .status()
103            .unwrap();
104    }
105
106    #[test]
107    fn defaults_match_upstream() {
108        let tmp = commit_helper::init_repo();
109        let cfg = FetchPruneConfig::from_repo(tmp.path());
110        assert_eq!(cfg.fetch_recent_refs_days, 7);
111        assert!(cfg.fetch_recent_refs_include_remotes);
112        assert_eq!(cfg.fetch_recent_commits_days, 0);
113        assert!(!cfg.fetch_recent_always);
114        assert_eq!(cfg.prune_offset_days, 3);
115        assert!(!cfg.prune_verify_remote_always);
116        assert!(!cfg.prune_verify_unreachable_always);
117        assert_eq!(cfg.prune_remote_name, "origin");
118    }
119
120    #[test]
121    fn reads_overrides() {
122        let tmp = commit_helper::init_repo();
123        set(tmp.path(), "lfs.fetchrecentrefsdays", "14");
124        set(tmp.path(), "lfs.fetchrecentcommitsdays", "30");
125        set(tmp.path(), "lfs.fetchrecentremoterefs", "false");
126        set(tmp.path(), "lfs.fetchrecentalways", "yes");
127        set(tmp.path(), "lfs.pruneoffsetdays", "0");
128        set(tmp.path(), "lfs.pruneremotetocheck", "upstream");
129        let cfg = FetchPruneConfig::from_repo(tmp.path());
130        assert_eq!(cfg.fetch_recent_refs_days, 14);
131        assert_eq!(cfg.fetch_recent_commits_days, 30);
132        assert!(!cfg.fetch_recent_refs_include_remotes);
133        assert!(cfg.fetch_recent_always);
134        assert_eq!(cfg.prune_offset_days, 0);
135        assert_eq!(cfg.prune_remote_name, "upstream");
136    }
137
138    #[test]
139    fn bool_accepts_git_styles() {
140        let tmp = commit_helper::init_repo();
141        for (raw, expected) in [
142            ("true", true),
143            ("TRUE", true),
144            ("yes", true),
145            ("On", true),
146            ("1", true),
147            ("false", false),
148            ("no", false),
149            ("OFF", false),
150            ("0", false),
151        ] {
152            set(tmp.path(), "lfs.fetchrecentalways", raw);
153            let cfg = FetchPruneConfig::from_repo(tmp.path());
154            assert_eq!(cfg.fetch_recent_always, expected, "raw={raw:?}");
155        }
156    }
157}