use crate::command_push::{parse_push_region, push_shape};
use crate::lfs_upload_settings::{current_branch, default_remote, reads_as_true, setting, values};
pub use crate::lfs_upload_settings::{PlanError, Query, Settings, read_settings, resolve_push_remote, settings_from_snapshot};
use crate::lfs_pre_push::{hook_switched_off, skip_requested};
use std::ffi::{OsStr, OsString};
use std::path::Path;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UploadPlan {
pub remotes: Vec<OsString>,
pub refs: Vec<OsString>,
pub everything: bool,
pub submodules: bool,
pub full_scan: bool,
pub objects: Option<Vec<String>>,
}
pub const OBJECTS_PER_RUN: usize = 500;
impl UploadPlan {
pub fn runs(&self, remote: &OsStr) -> Vec<Vec<OsString>> {
let Some(objects) = self.objects.as_ref() else {
return vec![self.arguments(remote)];
};
return objects
.chunks(OBJECTS_PER_RUN)
.map(|chunk| {
let mut arguments: Vec<OsString> = vec![OsString::from("push"), OsString::from("--object-id"), remote.to_owned()];
arguments.extend(chunk.iter().map(OsString::from));
return arguments;
})
.collect();
}
pub fn arguments(&self, remote: &OsStr) -> Vec<OsString> {
let mut arguments: Vec<OsString> = vec![OsString::from("push")];
if self.full_scan {
arguments.push(OsString::from("--all"));
}
arguments.push(remote.to_owned());
if !self.everything {
arguments.extend(self.refs.iter().cloned());
}
return arguments;
}
}
enum Source {
Deletion,
Matching,
Named(OsString),
}
fn refspec_source(spec: &OsStr) -> Source {
let bytes: &[u8] = spec.as_encoded_bytes();
let unforced: &[u8] = bytes.strip_prefix(b"+").unwrap_or(bytes);
if unforced == b":" {
return Source::Matching;
}
let source: &[u8] = match unforced.iter().rposition(|byte| {
return *byte == b':';
}) {
Some(position) => &unforced[..position],
None => unforced,
};
if source.is_empty() {
return Source::Deletion;
}
return Source::Named(<OsString as std::os::unix::ffi::OsStringExt>::from_vec(source.to_vec()));
}
fn keep_all(_name: &str) -> bool {
return true;
}
fn list(query: Query<'_>, prefix: &str, keep: &dyn Fn(&str) -> bool) -> Result<Vec<OsString>, PlanError> {
let listing: String = query(&["for-each-ref", "--format=%(refname)", prefix]).map_err(|why| {
return PlanError(format!("git for-each-ref {prefix}: {why}"));
})?;
return Ok(listing
.lines()
.filter(|name| {
return keep(name);
})
.map(OsString::from)
.collect());
}
fn expand_pattern(query: Query<'_>, pattern: &str) -> Result<Vec<OsString>, PlanError> {
let (front, back) = pattern.split_once('*').unwrap_or((pattern, ""));
let directory: &str = front.rsplit_once('/').map_or("refs", |(head, _)| {
return head;
});
return list(query, directory, &|name| {
return name.len() >= front.len() + back.len() && name.starts_with(front) && name.ends_with(back);
});
}
fn file_url(absolute: &Path) -> OsString {
let mut url: Vec<u8> = b"file://".to_vec();
for byte in absolute.as_os_str().as_encoded_bytes() {
if byte.is_ascii_alphanumeric() || b"/-._~".contains(byte) {
url.push(*byte);
} else {
url.extend_from_slice(format!("%{byte:02X}").as_bytes());
}
}
return <OsString as std::os::unix::ffi::OsStringExt>::from_vec(url);
}
fn lfs_destination(remote: &OsStr, directory: &Path, configured: bool) -> OsString {
let bytes: &[u8] = remote.as_encoded_bytes();
let has_scheme: bool = bytes.windows(3).any(|window| {
return window == b"://";
});
let scp_like: bool = bytes.iter().position(|byte| {
return *byte == b':';
}).is_some_and(|colon| {
return !bytes[..colon].contains(&b'/');
});
let explicit_path: bool = bytes.starts_with(b"/") || bytes.starts_with(b"./") || bytes.starts_with(b"../");
if configured || has_scheme || (scp_like && !explicit_path) {
return remote.to_owned();
}
let candidate = directory.join(Path::new(remote));
if !explicit_path && !candidate.exists() {
return remote.to_owned();
}
return match std::path::absolute(&candidate) {
Ok(absolute) => file_url(absolute.as_path()),
Err(_) => remote.to_owned(),
};
}
fn configured_remotes(all: &[(String, String)]) -> Vec<&str> {
return all
.iter()
.filter_map(|(key, _)| {
let rest: &str = key.strip_prefix("remote.")?;
let (name, variable) = rest.rsplit_once('.')?;
return (variable == "url" || variable == "pushurl").then_some(name);
})
.collect();
}
fn has_push_rewrite(all: &[(String, String)]) -> bool {
return all.iter().any(|(key, _)| {
return key.starts_with("url.") && key.ends_with(".pushinsteadof");
});
}
pub fn plan_upload(
global_prefix: &[OsString],
region: &[OsString],
environment: &[(OsString, OsString)],
directory: &Path,
all: &[(String, String)],
query: Query<'_>,
) -> Result<Option<UploadPlan>, PlanError> {
if hook_switched_off(global_prefix, environment) || skip_requested(environment) {
return Ok(None);
}
let Some((region_facts, shape)) = parse_push_region(region, &[]).ok().zip(push_shape(region)) else {
return Ok(None);
};
if region_facts.no_verify || region_facts.dry_run || shape.deletes {
return Ok(None);
}
let submodules: bool = shape.recurse_submodules.as_ref().is_some_and(|value| {
return matches!(value.to_str(), Some("on-demand" | "only"));
});
let branch_needed: bool = shape.repository.is_none() || shape.refspecs.is_empty();
let branch: Option<String> = if branch_needed { current_branch(query)? } else { None };
let remote_name: OsString = match shape.repository.clone() {
Some(typed) => typed,
None => OsString::from(default_remote(branch.as_deref(), all)),
};
let remote_text: String = remote_name.to_string_lossy().into_owned();
let mut everything: bool = shape.mirror;
let mut specs: Vec<OsString> = shape.refspecs.clone();
let bare: bool = specs.is_empty() && !shape.branches && !shape.tags && !everything;
if bare {
let configured: Vec<&str> = values(all, &format!("remote.{remote_text}.push"));
if setting(all, &format!("remote.{remote_text}.mirror")).is_some_and(reads_as_true) {
everything = true;
} else if !configured.is_empty() {
specs.extend(configured.into_iter().map(OsString::from));
} else if setting(all, "push.default") == Some("matching") {
specs.push(OsString::from(":"));
} else {
specs.push(OsString::from(match branch.as_deref() {
Some(name) => format!("refs/heads/{name}"),
None => String::from("HEAD"),
}));
}
}
let mut refs: Vec<OsString> = Vec::new();
for spec in &specs {
match refspec_source(spec.as_os_str()) {
Source::Deletion => {}
Source::Matching => refs.extend(list(query, "refs/heads", &keep_all)?),
Source::Named(source) => match source.to_str() {
Some(text) if text.contains('*') => refs.extend(expand_pattern(query, text)?),
_ => refs.push(source),
},
}
}
if shape.branches {
refs.extend(list(query, "refs/heads", &keep_all)?);
}
if shape.tags {
refs.extend(list(query, "refs/tags", &keep_all)?);
}
let mut unique: Vec<OsString> = Vec::new();
for found in refs {
if !unique.contains(&found) {
unique.push(found);
}
}
if unique.is_empty() && !everything {
return Ok(None);
}
let configured: bool = configured_remotes(all).contains(&remote_text.as_str());
let push_urls: Vec<&str> = if configured { values(all, &format!("remote.{remote_text}.pushurl")) } else { Vec::new() };
let push_url_count: usize = push_urls.len();
let remotes: Vec<OsString> = if push_url_count > 1 {
push_urls
.iter()
.copied()
.map(|url| {
return lfs_destination(OsStr::new(url), directory, false);
})
.collect()
} else {
vec![lfs_destination(remote_name.as_os_str(), directory, configured)]
};
let tracking_trusted: bool = configured && push_urls.is_empty() && !has_push_rewrite(all);
let full_scan: bool = everything || !tracking_trusted;
return Ok(Some(UploadPlan { remotes, refs: unique, everything, submodules, full_scan, objects: None }));
}
#[cfg(test)]
#[path = "lfs_upload_plan_tests.rs"]
mod tests;