1use std::path::Path;
4use std::process::Command;
5
6use crate::Error;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ConfigScope {
11 Global,
14 Local,
16 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
30pub 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 Some(1) => Ok(None),
44 _ => Err(Error::Failed(
45 String::from_utf8_lossy(&out.stderr).trim().to_owned(),
46 )),
47 }
48}
49
50pub fn get_from_file(cwd: &Path, file: &Path, key: &str) -> Result<Option<String>, Error> {
53 if !cwd.join(file).is_file() {
54 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
75pub 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
95pub 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
111pub 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 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}