Skip to main content

git_lfs_git/
config.rs

1//! Git config get/set/unset, scoped to one of git's config files.
2
3use std::path::Path;
4use std::process::Command;
5
6use crate::Error;
7
8/// Which config file `git config` operates on.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ConfigScope {
11    /// `~/.gitconfig` (or `~/.config/git/config`). The default for upstream
12    /// `git lfs install`.
13    Global,
14    /// The current repository's `.git/config`.
15    Local,
16    /// `/etc/gitconfig`. Usually requires root.
17    System,
18}
19
20impl ConfigScope {
21    fn flag(self) -> &'static str {
22        match self {
23            Self::Global => "--global",
24            Self::Local => "--local",
25            Self::System => "--system",
26        }
27    }
28}
29
30/// Read a single config value from the given scope. Returns `Ok(None)` if
31/// the key isn't set.
32pub fn get(cwd: &Path, scope: ConfigScope, key: &str) -> Result<Option<String>, Error> {
33    let out = Command::new("git")
34        .arg("-C")
35        .arg(cwd)
36        .args(["config", scope.flag(), "--get", key])
37        .output()?;
38    match out.status.code() {
39        Some(0) => Ok(Some(
40            String::from_utf8_lossy(&out.stdout).trim().to_owned(),
41        )),
42        // `git config --get` exits 1 when the key isn't set.
43        Some(1) => Ok(None),
44        _ => Err(Error::Failed(
45            String::from_utf8_lossy(&out.stderr).trim().to_owned(),
46        )),
47    }
48}
49
50/// Read a single config value from a specific file (e.g. `.lfsconfig`).
51/// Returns `Ok(None)` if the file doesn't exist or the key isn't set.
52pub fn get_from_file(cwd: &Path, file: &Path, key: &str) -> Result<Option<String>, Error> {
53    if !cwd.join(file).is_file() {
54        // `git config --file` errors loudly on a missing file. The common
55        // case for `.lfsconfig` is "no file" — treat that as "no value".
56        return Ok(None);
57    }
58    let file_arg = format!("--file={}", file.display());
59    let out = Command::new("git")
60        .arg("-C")
61        .arg(cwd)
62        .args(["config", &file_arg, "--get", key])
63        .output()?;
64    match out.status.code() {
65        Some(0) => Ok(Some(
66            String::from_utf8_lossy(&out.stdout).trim().to_owned(),
67        )),
68        Some(1) => Ok(None),
69        _ => Err(Error::Failed(
70            String::from_utf8_lossy(&out.stderr).trim().to_owned(),
71        )),
72    }
73}
74
75/// Look up `key` across `.lfsconfig` (committed; lowest priority) and
76/// the standard git config scopes (local → global → system). Returns the
77/// first match.
78///
79/// Mirrors upstream's effective config: settings written to `.lfsconfig`
80/// at the repo root are visible without overriding anything explicitly
81/// set in the user's git config.
82pub fn get_effective(cwd: &Path, key: &str) -> Result<Option<String>, Error> {
83    if let Some(v) = get(cwd, ConfigScope::Local, key)? {
84        return Ok(Some(v));
85    }
86    if let Some(v) = get(cwd, ConfigScope::Global, key)? {
87        return Ok(Some(v));
88    }
89    if let Some(v) = get(cwd, ConfigScope::System, key)? {
90        return Ok(Some(v));
91    }
92    get_from_file(cwd, std::path::Path::new(".lfsconfig"), key)
93}
94
95/// Set `key = value` in the given scope.
96pub fn set(cwd: &Path, scope: ConfigScope, key: &str, value: &str) -> Result<(), Error> {
97    let out = Command::new("git")
98        .arg("-C")
99        .arg(cwd)
100        .args(["config", scope.flag(), key, value])
101        .output()?;
102    if out.status.success() {
103        Ok(())
104    } else {
105        Err(Error::Failed(
106            String::from_utf8_lossy(&out.stderr).trim().to_owned(),
107        ))
108    }
109}
110
111/// Unset `key` in the given scope. Idempotent: if the key isn't there,
112/// returns `Ok(())` rather than erroring.
113pub fn unset(cwd: &Path, scope: ConfigScope, key: &str) -> Result<(), Error> {
114    let out = Command::new("git")
115        .arg("-C")
116        .arg(cwd)
117        .args(["config", scope.flag(), "--unset", key])
118        .output()?;
119    match out.status.code() {
120        Some(0) => Ok(()),
121        // git config --unset exits 5 when the key isn't set.
122        Some(5) => Ok(()),
123        _ => Err(Error::Failed(
124            String::from_utf8_lossy(&out.stderr).trim().to_owned(),
125        )),
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use tempfile::TempDir;
133
134    fn init_repo() -> TempDir {
135        let tmp = TempDir::new().unwrap();
136        let status = Command::new("git")
137            .args(["init", "--quiet"])
138            .arg(tmp.path())
139            .status()
140            .unwrap();
141        assert!(status.success());
142        tmp
143    }
144
145    #[test]
146    fn get_unset_key_returns_none() {
147        let tmp = init_repo();
148        let v = get(tmp.path(), ConfigScope::Local, "filter.lfs.clean").unwrap();
149        assert_eq!(v, None);
150    }
151
152    #[test]
153    fn set_then_get_round_trips() {
154        let tmp = init_repo();
155        set(tmp.path(), ConfigScope::Local, "filter.lfs.clean", "git-lfs clean -- %f").unwrap();
156        let v = get(tmp.path(), ConfigScope::Local, "filter.lfs.clean").unwrap();
157        assert_eq!(v.as_deref(), Some("git-lfs clean -- %f"));
158    }
159
160    #[test]
161    fn unset_removes_key() {
162        let tmp = init_repo();
163        set(tmp.path(), ConfigScope::Local, "filter.lfs.required", "true").unwrap();
164        unset(tmp.path(), ConfigScope::Local, "filter.lfs.required").unwrap();
165        let v = get(tmp.path(), ConfigScope::Local, "filter.lfs.required").unwrap();
166        assert_eq!(v, None);
167    }
168
169    #[test]
170    fn unset_missing_key_is_ok() {
171        let tmp = init_repo();
172        unset(tmp.path(), ConfigScope::Local, "never.was.set").unwrap();
173    }
174}