1use std::collections::BTreeSet;
8use std::path::Path;
9use std::process::Command;
10
11#[derive(Debug, Clone)]
15pub struct ExtensionConfig {
16 pub name: String,
17 pub clean: String,
18 pub smudge: String,
19 pub priority: i64,
20}
21
22pub fn list_extensions(cwd: &Path) -> Vec<ExtensionConfig> {
27 let mut extensions: Vec<ExtensionConfig> = list_extension_names(cwd)
28 .into_iter()
29 .map(|name| read_extension(cwd, &name))
30 .collect();
31 extensions.sort_by(|a, b| a.priority.cmp(&b.priority).then(a.name.cmp(&b.name)));
32 extensions
33}
34
35pub fn list_extension_names(cwd: &Path) -> BTreeSet<String> {
41 let mut names = BTreeSet::new();
42
43 if let Ok(out) = Command::new("git")
44 .arg("-C")
45 .arg(cwd)
46 .args([
47 "config",
48 "--name-only",
49 "--get-regexp",
50 r"^lfs\.extension\..*\.(clean|smudge|priority)$",
51 ])
52 .output()
53 && out.status.success()
54 {
55 for line in String::from_utf8_lossy(&out.stdout).lines() {
56 if let Some(name) = extension_name_from_key(line) {
57 names.insert(name);
58 }
59 }
60 }
61
62 if let Some(root) = repo_root(cwd) {
63 let lfsconfig = root.join(".lfsconfig");
64 if lfsconfig.is_file()
65 && let Ok(out) = Command::new("git")
66 .arg("-C")
67 .arg(&root)
68 .args([
69 "config",
70 "--file=.lfsconfig",
71 "--name-only",
72 "--get-regexp",
73 r"^lfs\.extension\..*\.(clean|smudge|priority)$",
74 ])
75 .output()
76 && out.status.success()
77 {
78 for line in String::from_utf8_lossy(&out.stdout).lines() {
79 if let Some(name) = extension_name_from_key(line) {
80 names.insert(name);
81 }
82 }
83 }
84 }
85
86 names
87}
88
89fn repo_root(cwd: &Path) -> Option<std::path::PathBuf> {
90 let out = Command::new("git")
91 .arg("-C")
92 .arg(cwd)
93 .args(["rev-parse", "--show-toplevel"])
94 .output()
95 .ok()?;
96 if !out.status.success() {
97 return None;
98 }
99 let s = String::from_utf8_lossy(&out.stdout).trim().to_owned();
100 if s.is_empty() {
101 None
102 } else {
103 Some(std::path::PathBuf::from(s))
104 }
105}
106
107fn extension_name_from_key(key: &str) -> Option<String> {
111 let rest = key.strip_prefix("lfs.extension.")?;
112 for suffix in [".clean", ".smudge", ".priority"] {
113 if let Some(name) = rest.strip_suffix(suffix) {
114 return Some(name.to_owned());
115 }
116 }
117 None
118}
119
120fn read_extension(cwd: &Path, name: &str) -> ExtensionConfig {
121 let lookup = |suffix: &str| -> String {
122 crate::config::get_effective(cwd, &format!("lfs.extension.{name}.{suffix}"))
123 .ok()
124 .flatten()
125 .unwrap_or_default()
126 };
127 let clean = lookup("clean");
128 let smudge = lookup("smudge");
129 let priority = lookup("priority").parse::<i64>().unwrap_or(0);
130 ExtensionConfig {
131 name: name.to_owned(),
132 clean,
133 smudge,
134 priority,
135 }
136}