use super::lfs_upload_plan::UploadPlan;
use super::repo_read_layout::RepoLayout;
pub fn narrow_plan(plan: UploadPlan, layout: Option<&RepoLayout>) -> Option<UploadPlan> {
#[cfg(unix)]
{
let Some(found) = layout else {
return Some(plan);
};
return match unix::scan(&plan, found) {
None => Some(plan),
Some(objects) if objects.is_empty() => None,
Some(objects) => Some(UploadPlan { objects: Some(objects), ..plan }),
};
}
#[cfg(not(unix))]
{
let _unused = layout;
return Some(plan);
}
}
#[cfg(unix)]
mod unix {
use super::super::diagnostic_log::debug;
use super::super::lfs_pointer_scan::{SCAN_BUDGET, new_pointer_objects};
use super::super::lfs_upload_plan::UploadPlan;
use super::super::repo_read_layout::RepoLayout;
use super::super::repo_read_object::{ObjectKind, from_hex, name_length, to_hex};
use super::super::repo_read_odb::ObjectDatabase;
use super::super::repo_read_refs::{Lookup, RefValue, Refs, is_plain_ref_name};
const TAG: &str = "cli-git lfs-object-plan";
const PEEL_LIMIT: usize = 5;
fn peel(database: &ObjectDatabase, layout: &RepoLayout, hex: &str) -> Option<Vec<u8>> {
let mut name: Vec<u8> = from_hex(hex.as_bytes(), layout.object_format)?;
for _ in 0..PEEL_LIMIT {
let object = database.read(name.as_slice())?;
match object.kind {
ObjectKind::Commit => return Some(name),
ObjectKind::Tag => {
let first: &[u8] = object.bytes.split(|byte| { return *byte == b'\n'; }).next()?;
name = from_hex(first.strip_prefix(b"object ")?, layout.object_format)?;
}
ObjectKind::Tree | ObjectKind::Blob => return None,
}
}
return None;
}
fn tip_of(database: &ObjectDatabase, layout: &RepoLayout, refs: &Refs, typed: &str) -> Option<String> {
if typed == "HEAD" {
return match refs.lookup_head(layout.git_dir.as_path())? {
RefValue::Direct(hex) => Some(hex),
RefValue::Symbolic(target) => refs.resolve(target.as_str()),
};
}
let plain: bool = !typed.is_empty()
&& typed.split('/').all(|part| {
return !part.is_empty() && part.bytes().all(|byte| { return byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'); });
});
if !plain {
return None;
}
let candidates: [String; 6] = [
typed.to_owned(),
format!("refs/{typed}"),
format!("refs/tags/{typed}"),
format!("refs/heads/{typed}"),
format!("refs/remotes/{typed}"),
format!("refs/remotes/{typed}/HEAD"),
];
for candidate in &candidates {
if !is_plain_ref_name(candidate) {
continue;
}
match refs.lookup(candidate) {
Lookup::Found(_) => return refs.resolve(candidate),
Lookup::Missing => {}
Lookup::Unsure => return None,
}
}
let as_name: Vec<u8> = from_hex(typed.as_bytes(), layout.object_format)?;
return database.contains(as_name.as_slice()).then(|| { return to_hex(as_name.as_slice()); });
}
pub(super) fn scan(plan: &UploadPlan, layout: &RepoLayout) -> Option<Vec<String>> {
let database: ObjectDatabase = ObjectDatabase::open(layout.common_dir.as_path(), None, layout.object_format)?;
let refs: Refs = Refs::new(layout.common_dir.as_path(), layout.object_format.hex_length());
let listing = refs.list()?;
let mut tips: Vec<String> = Vec::new();
if plan.everything {
for (_, value) in &listing {
if let RefValue::Direct(hex) = value {
tips.push(hex.clone());
}
}
} else {
for typed in &plan.refs {
tips.push(tip_of(&database, layout, &refs, typed.to_str()?)?);
}
}
let mut include: Vec<Vec<u8>> = Vec::new();
for hex in &tips {
include.push(peel(&database, layout, hex)?);
}
let mut stop: Vec<Vec<u8>> = Vec::new();
if !plan.full_scan {
let remote: &str = plan.remotes.first()?.to_str()?;
let prefix: String = format!("refs/remotes/{remote}/");
for (name, value) in &listing {
if let (true, RefValue::Direct(hex)) = (name.starts_with(prefix.as_str()), value) {
stop.push(from_hex(hex.as_bytes(), layout.object_format)?);
}
}
}
let objects: Vec<String> = new_pointer_objects(&database, layout.object_format, include.as_slice(), stop.as_slice(), SCAN_BUDGET)?;
for name in &objects {
let stored = layout.common_dir.join("lfs/objects").join(&name[..2]).join(&name[2..4]).join(name);
if !stored.is_file() {
debug(TAG, &format!("object {name} is not in the local LFS storage; asking git-lfs"));
return None;
}
}
debug(
TAG,
&format!(
"{} tips, {} known tips, full scan {}: {} objects to upload (hash length {})",
include.len(),
stop.len(),
plan.full_scan,
objects.len(),
name_length(layout.object_format)
),
);
return Some(objects);
}
}
#[cfg(test)]
#[path = "lfs_object_plan_tests.rs"]
mod tests;