1use super::command_push::{parse_push_region, push_shape};
21use super::lfs_upload_settings::{current_branch, default_remote, reads_as_true, setting, values};
23pub use super::lfs_upload_settings::{PlanError, Query, Settings, read_settings, resolve_push_remote, settings_from_snapshot};
24use super::lfs_pre_push::{hook_switched_off, skip_requested};
26use std::ffi::{OsStr, OsString};
28use std::path::Path;
30
31#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct UploadPlan {
39 pub remotes: Vec<OsString>,
42 pub refs: Vec<OsString>,
44 pub everything: bool,
46 pub submodules: bool,
48 pub full_scan: bool,
51 pub objects: Option<Vec<String>>,
54}
55
56pub const OBJECTS_PER_RUN: usize = 500;
58
59impl UploadPlan {
60 pub fn runs(&self, remote: &OsStr) -> Vec<Vec<OsString>> {
63 let Some(objects) = self.objects.as_ref() else {
64 return vec![self.arguments(remote)];
65 };
66 return objects
67 .chunks(OBJECTS_PER_RUN)
68 .map(|chunk| {
69 let mut arguments: Vec<OsString> = vec![OsString::from("push"), OsString::from("--object-id"), remote.to_owned()];
70 arguments.extend(chunk.iter().map(OsString::from));
71 return arguments;
72 })
73 .collect();
74 }
75
76 pub fn arguments(&self, remote: &OsStr) -> Vec<OsString> {
80 let mut arguments: Vec<OsString> = vec![OsString::from("push")];
81 if self.full_scan {
82 arguments.push(OsString::from("--all"));
83 }
84 arguments.push(remote.to_owned());
85 if !self.everything {
86 arguments.extend(self.refs.iter().cloned());
87 }
88 return arguments;
89 }
90}
91
92enum Source {
95 Deletion,
97 Matching,
99 Named(OsString),
101}
102
103fn refspec_source(spec: &OsStr) -> Source {
105 let bytes: &[u8] = spec.as_encoded_bytes();
106 let unforced: &[u8] = bytes.strip_prefix(b"+").unwrap_or(bytes);
107 if unforced == b":" {
108 return Source::Matching;
109 }
110 let source: &[u8] = match unforced.iter().rposition(|byte| {
111 return *byte == b':';
112 }) {
113 Some(position) => &unforced[..position],
114 None => unforced,
115 };
116 if source.is_empty() {
117 return Source::Deletion;
118 }
119 return Source::Named(<OsString as std::os::unix::ffi::OsStringExt>::from_vec(source.to_vec()));
120}
121
122fn keep_all(_name: &str) -> bool {
124 return true;
125}
126
127fn list(query: Query<'_>, prefix: &str, keep: &dyn Fn(&str) -> bool) -> Result<Vec<OsString>, PlanError> {
129 let listing: String = query(&["for-each-ref", "--format=%(refname)", prefix]).map_err(|why| {
130 return PlanError(format!("git for-each-ref {prefix}: {why}"));
131 })?;
132 return Ok(listing
133 .lines()
134 .filter(|name| {
135 return keep(name);
136 })
137 .map(OsString::from)
138 .collect());
139}
140
141fn expand_pattern(query: Query<'_>, pattern: &str) -> Result<Vec<OsString>, PlanError> {
144 let (front, back) = pattern.split_once('*').unwrap_or((pattern, ""));
145 let directory: &str = front.rsplit_once('/').map_or("refs", |(head, _)| {
146 return head;
147 });
148 return list(query, directory, &|name| {
149 return name.len() >= front.len() + back.len() && name.starts_with(front) && name.ends_with(back);
150 });
151}
152
153fn file_url(absolute: &Path) -> OsString {
155 let mut url: Vec<u8> = b"file://".to_vec();
156 for byte in absolute.as_os_str().as_encoded_bytes() {
157 if byte.is_ascii_alphanumeric() || b"/-._~".contains(byte) {
158 url.push(*byte);
159 } else {
160 url.extend_from_slice(format!("%{byte:02X}").as_bytes());
161 }
162 }
163 return <OsString as std::os::unix::ffi::OsStringExt>::from_vec(url);
164}
165
166fn lfs_destination(remote: &OsStr, directory: &Path, configured: bool) -> OsString {
178 let bytes: &[u8] = remote.as_encoded_bytes();
179 let has_scheme: bool = bytes.windows(3).any(|window| {
180 return window == b"://";
181 });
182 let scp_like: bool = bytes.iter().position(|byte| {
183 return *byte == b':';
184 }).is_some_and(|colon| {
185 return !bytes[..colon].contains(&b'/');
186 });
187 let explicit_path: bool = bytes.starts_with(b"/") || bytes.starts_with(b"./") || bytes.starts_with(b"../");
188 if configured || has_scheme || (scp_like && !explicit_path) {
189 return remote.to_owned();
190 }
191 let candidate = directory.join(Path::new(remote));
192 if !explicit_path && !candidate.exists() {
193 return remote.to_owned();
194 }
195 return match std::path::absolute(&candidate) {
196 Ok(absolute) => file_url(absolute.as_path()),
197 Err(_) => remote.to_owned(),
198 };
199}
200
201fn configured_remotes(all: &[(String, String)]) -> Vec<&str> {
203 return all
204 .iter()
205 .filter_map(|(key, _)| {
206 let rest: &str = key.strip_prefix("remote.")?;
207 let (name, variable) = rest.rsplit_once('.')?;
208 return (variable == "url" || variable == "pushurl").then_some(name);
209 })
210 .collect();
211}
212
213fn has_push_rewrite(all: &[(String, String)]) -> bool {
215 return all.iter().any(|(key, _)| {
216 return key.starts_with("url.") && key.ends_with(".pushinsteadof");
217 });
218}
219
220pub fn plan_upload(
235 global_prefix: &[OsString],
236 region: &[OsString],
237 environment: &[(OsString, OsString)],
238 directory: &Path,
239 all: &[(String, String)],
240 query: Query<'_>,
241) -> Result<Option<UploadPlan>, PlanError> {
242 if hook_switched_off(global_prefix, environment) || skip_requested(environment) {
243 return Ok(None);
244 }
245 let Some((region_facts, shape)) = parse_push_region(region, &[]).ok().zip(push_shape(region)) else {
246 return Ok(None);
247 };
248 if region_facts.no_verify || region_facts.dry_run || shape.deletes {
249 return Ok(None);
250 }
251 let submodules: bool = shape.recurse_submodules.as_ref().is_some_and(|value| {
252 return matches!(value.to_str(), Some("on-demand" | "only"));
253 });
254 let branch_needed: bool = shape.repository.is_none() || shape.refspecs.is_empty();
255 let branch: Option<String> = if branch_needed { current_branch(query)? } else { None };
256 let remote_name: OsString = match shape.repository.clone() {
257 Some(typed) => typed,
258 None => OsString::from(default_remote(branch.as_deref(), all)),
259 };
260 let remote_text: String = remote_name.to_string_lossy().into_owned();
261 let mut everything: bool = shape.mirror;
262 let mut specs: Vec<OsString> = shape.refspecs.clone();
263 let bare: bool = specs.is_empty() && !shape.branches && !shape.tags && !everything;
264 if bare {
265 let configured: Vec<&str> = values(all, &format!("remote.{remote_text}.push"));
266 if setting(all, &format!("remote.{remote_text}.mirror")).is_some_and(reads_as_true) {
267 everything = true;
268 } else if !configured.is_empty() {
269 specs.extend(configured.into_iter().map(OsString::from));
270 } else if setting(all, "push.default") == Some("matching") {
271 specs.push(OsString::from(":"));
272 } else {
273 specs.push(OsString::from(match branch.as_deref() {
274 Some(name) => format!("refs/heads/{name}"),
275 None => String::from("HEAD"),
276 }));
277 }
278 }
279 let mut refs: Vec<OsString> = Vec::new();
280 for spec in &specs {
281 match refspec_source(spec.as_os_str()) {
282 Source::Deletion => {}
283 Source::Matching => refs.extend(list(query, "refs/heads", &keep_all)?),
284 Source::Named(source) => match source.to_str() {
285 Some(text) if text.contains('*') => refs.extend(expand_pattern(query, text)?),
286 _ => refs.push(source),
287 },
288 }
289 }
290 if shape.branches {
291 refs.extend(list(query, "refs/heads", &keep_all)?);
292 }
293 if shape.tags {
294 refs.extend(list(query, "refs/tags", &keep_all)?);
295 }
296 let mut unique: Vec<OsString> = Vec::new();
297 for found in refs {
298 if !unique.contains(&found) {
299 unique.push(found);
300 }
301 }
302 if unique.is_empty() && !everything {
303 return Ok(None);
304 }
305 let configured: bool = configured_remotes(all).contains(&remote_text.as_str());
306 let push_urls: Vec<&str> = if configured { values(all, &format!("remote.{remote_text}.pushurl")) } else { Vec::new() };
307 let push_url_count: usize = push_urls.len();
308 let remotes: Vec<OsString> = if push_url_count > 1 {
309 push_urls
310 .iter()
311 .copied()
312 .map(|url| {
313 return lfs_destination(OsStr::new(url), directory, false);
314 })
315 .collect()
316 } else {
317 vec![lfs_destination(remote_name.as_os_str(), directory, configured)]
318 };
319 let tracking_trusted: bool = configured && push_urls.is_empty() && !has_push_rewrite(all);
322 let full_scan: bool = everything || !tracking_trusted;
323 return Ok(Some(UploadPlan { remotes, refs: unique, everything, submodules, full_scan, objects: None }));
324}
325
326#[cfg(test)]
328#[path = "lfs_upload_plan_tests.rs"]
329mod tests;