git_policy_cli/
lfs_upload_plan.rs1use super::command_push::{parse_push_region, push_shape};
17use super::lfs_pre_push::{hook_switched_off, skip_requested};
19use std::ffi::{OsStr, OsString};
21use std::path::Path;
23
24pub type Query<'a> = &'a mut dyn FnMut(&[&str]) -> Option<String>;
26
27#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct UploadPlan {
35 pub remote: OsString,
37 pub refs: Vec<OsString>,
39 pub everything: bool,
41 pub submodules: bool,
43}
44
45impl UploadPlan {
46 pub fn arguments(&self) -> Vec<OsString> {
48 let mut arguments: Vec<OsString> = vec![OsString::from("push")];
49 if self.everything {
50 arguments.push(OsString::from("--all"));
51 }
52 arguments.push(self.remote.clone());
53 if !self.everything {
54 arguments.extend(self.refs.iter().cloned());
55 }
56 return arguments;
57 }
58}
59
60fn refspec_source(spec: &OsStr) -> Option<OsString> {
62 let bytes: &[u8] = spec.as_encoded_bytes();
63 let unforced: &[u8] = bytes.strip_prefix(b"+").unwrap_or(bytes);
64 let source: &[u8] = unforced
65 .split(|byte| {
66 return *byte == b':';
67 })
68 .next()
69 .unwrap_or_default();
70 if source.is_empty() {
71 return None;
72 }
73 return Some(<OsString as std::os::unix::ffi::OsStringExt>::from_vec(source.to_vec()));
74}
75
76fn settings(query: Query<'_>) -> Vec<(String, String)> {
78 let listing: String = query(&[
79 "config",
80 "--get-regexp",
81 "^(branch\\..*\\.(pushremote|remote)|remote\\.pushdefault|remote\\..*\\.push|push\\.default)$",
82 ])
83 .unwrap_or_default();
84 return listing
85 .lines()
86 .filter_map(|line| {
87 return line.split_once(' ').map(|(key, value)| {
88 return (key.to_owned(), value.trim().to_owned());
89 });
90 })
91 .collect();
92}
93
94fn setting<'a>(all: &'a [(String, String)], key: &str) -> Option<&'a str> {
96 return all
97 .iter()
98 .rev()
99 .find(|(name, _)| {
100 return name == key;
101 })
102 .map(|(_, value)| {
103 return value.as_str();
104 });
105}
106
107fn current_branch(query: Query<'_>) -> Option<String> {
109 let full: String = query(&["symbolic-ref", "--quiet", "HEAD"])?;
110 return full.strip_prefix("refs/heads/").map(str::to_owned);
111}
112
113fn default_remote(branch: Option<&str>, all: &[(String, String)]) -> String {
116 let named = |variable: &str| -> Option<String> {
117 return branch.and_then(|name| {
118 return setting(all, &format!("branch.{name}.{variable}")).map(str::to_owned);
119 });
120 };
121 return named("pushremote")
122 .or_else(|| {
123 return setting(all, "remote.pushdefault").map(str::to_owned);
124 })
125 .or_else(|| {
126 return named("remote");
127 })
128 .unwrap_or_else(|| {
129 return String::from("origin");
130 });
131}
132
133pub fn resolve_push_remote(named: Option<OsString>, query: Query<'_>) -> OsString {
143 if let Some(found) = named {
144 return found;
145 }
146 let branch: Option<String> = current_branch(query);
147 let all: Vec<(String, String)> = settings(query);
148 return OsString::from(default_remote(branch.as_deref(), all.as_slice()));
149}
150
151fn expand(query: Query<'_>, patterns: &[&str]) -> Vec<OsString> {
153 let mut arguments: Vec<&str> = vec!["for-each-ref", "--format=%(refname)"];
154 arguments.extend(patterns);
155 return query(arguments.as_slice())
156 .unwrap_or_default()
157 .lines()
158 .map(OsString::from)
159 .collect();
160}
161
162fn lfs_destination(remote: OsString, directory: &Path) -> OsString {
173 let bytes: &[u8] = remote.as_encoded_bytes();
174 let path_like: bool = bytes.starts_with(b"/") || bytes.starts_with(b"./") || bytes.starts_with(b"../");
175 if !path_like {
176 return remote;
177 }
178 let Ok(absolute) = std::path::absolute(directory.join(Path::new(&remote))) else {
179 return remote;
180 };
181 let mut url: OsString = OsString::from("file://");
182 url.push(absolute.as_os_str());
183 return url;
184}
185
186pub fn plan_upload(
199 global_prefix: &[OsString],
200 region: &[OsString],
201 environment: &[(OsString, OsString)],
202 directory: &Path,
203 query: Query<'_>,
204) -> Option<UploadPlan> {
205 if hook_switched_off(global_prefix, environment) || skip_requested(environment) {
206 return None;
207 }
208 let region_facts = parse_push_region(region, &[]).ok()?;
209 if region_facts.no_verify || region_facts.dry_run {
210 return None;
211 }
212 let shape = push_shape(region)?;
213 if shape.deletes {
214 return None;
215 }
216 let submodules: bool = shape.recurse_submodules.as_ref().is_some_and(|value| {
217 return matches!(value.to_str(), Some("on-demand" | "only"));
218 });
219 let mut everything: bool = shape.mirror;
220 let mut specs: Vec<OsString> = shape.refspecs.clone();
221 let mut patterns: Vec<String> = Vec::new();
222 if shape.branches {
223 patterns.push(String::from("refs/heads"));
224 }
225 if shape.tags {
226 patterns.push(String::from("refs/tags"));
227 }
228 let bare: bool = specs.is_empty() && patterns.is_empty() && !everything;
229 let mut remote: Option<OsString> = shape.repository.clone();
230 if remote.is_none() || bare {
231 let branch: Option<String> = current_branch(query);
232 let all: Vec<(String, String)> = settings(query);
233 if remote.is_none() {
234 remote = Some(OsString::from(default_remote(branch.as_deref(), all.as_slice())));
235 }
236 if bare {
237 let key: String = format!("remote.{}.push", remote.as_ref()?.to_string_lossy());
238 let configured: Vec<&str> = all
239 .iter()
240 .filter(|(name, _)| {
241 return *name == key;
242 })
243 .map(|(_, value)| {
244 return value.as_str();
245 })
246 .collect();
247 if !configured.is_empty() {
248 specs.extend(configured.into_iter().map(OsString::from));
249 } else if setting(all.as_slice(), "push.default") == Some("matching") {
250 everything = true;
251 } else {
252 let head: String = match branch.as_deref() {
253 Some(name) => format!("refs/heads/{name}"),
254 None => String::from("HEAD"),
255 };
256 specs.push(OsString::from(head));
257 }
258 }
259 }
260 let mut refs: Vec<OsString> = Vec::new();
261 for spec in &specs {
262 let Some(source) = refspec_source(spec.as_os_str()) else {
263 continue;
264 };
265 match source.to_str() {
266 Some(text) if text.contains('*') => refs.extend(expand(query, &[text])),
267 _ => refs.push(source),
268 }
269 }
270 if !patterns.is_empty() {
271 let borrowed: Vec<&str> = patterns.iter().map(String::as_str).collect();
272 refs.extend(expand(query, borrowed.as_slice()));
273 }
274 if refs.is_empty() && !everything {
275 return None;
276 }
277 let destination: OsString = lfs_destination(remote?, directory);
278 return Some(UploadPlan { remote: destination, refs, everything, submodules });
279}
280
281#[cfg(test)]
283#[path = "lfs_upload_plan_tests.rs"]
284mod tests;