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::{GitQuery, 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)];
};
let mut runs: Vec<Vec<OsString>> = Vec::new();
for chunk in objects.chunks(OBJECTS_PER_RUN) {
let mut arguments: Vec<OsString> = vec![OsString::from("push"), OsString::from("--object-id"), remote.to_owned()];
arguments.extend(chunk.iter().map(OsString::from));
runs.push(arguments);
}
return runs;
}
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(is_colon_byte) {
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 is_colon_byte(byte: &u8) -> bool {
return *byte == b':';
}
fn list(query: Query<'_>, prefix: &str, pattern: Option<(&str, &str)>) -> Result<Vec<OsString>, PlanError> {
let listing: String = match query.ask(&["for-each-ref", "--format=%(refname)", prefix]) {
Ok(text) => text,
Err(why) => return Err(PlanError(format!("git for-each-ref {prefix}: {why}"))),
};
let mut names: Vec<OsString> = Vec::new();
for name in listing.lines() {
let kept: bool = match pattern {
Some((front, back)) => {
name.len() >= front.len() + back.len() && name.starts_with(front) && name.ends_with(back)
}
None => true,
};
if kept {
names.push(OsString::from(name));
}
}
return Ok(names);
}
fn expand_pattern(query: Query<'_>, pattern: &str) -> Result<Vec<OsString>, PlanError> {
let (front, back) = pattern.split_once('*').unwrap_or((pattern, ""));
let directory: &str = match front.rsplit_once('/') {
Some((head, _)) => head,
None => "refs",
};
return list(query, directory, Some((front, 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 mut has_scheme: bool = false;
for window in bytes.windows(3) {
if window == b"://" {
has_scheme = true;
break;
}
}
let scp_like: bool = match bytes.iter().position(is_colon_byte) {
Some(colon) => !bytes[..colon].contains(&b'/'),
None => false,
};
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> {
let mut names: Vec<&str> = Vec::new();
for (key, _) in all {
let Some(rest) = key.strip_prefix("remote.") else {
continue;
};
let Some((name, variable)) = rest.rsplit_once('.') else {
continue;
};
if variable == "url" || variable == "pushurl" {
names.push(name);
}
}
return names;
}
fn has_push_rewrite(all: &[(String, String)]) -> bool {
for (key, _) in all {
if key.starts_with("url.") && key.ends_with(".pushinsteadof") {
return true;
}
}
return false;
}
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 = match shape.recurse_submodules.as_ref() {
Some(value) => matches!(value.to_str(), Some("on-demand" | "only")),
None => false,
};
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", None)?),
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", None)?);
}
if shape.tags {
refs.extend(list(query, "refs/tags", None)?);
}
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 {
let mut destinations: Vec<OsString> = Vec::new();
for url in &push_urls {
destinations.push(lfs_destination(OsStr::new(url), directory, false));
}
destinations
} 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;