git_policy_cli/
lfs_object_plan.rs1use super::lfs_upload_plan::UploadPlan;
18use super::repo_read_layout::RepoLayout;
19
20pub fn narrow_plan(plan: UploadPlan, layout: Option<&RepoLayout>) -> Option<UploadPlan> {
31 #[cfg(unix)]
32 {
33 let Some(found) = layout else {
34 return Some(plan);
35 };
36 return match unix::scan(&plan, found) {
37 None => Some(plan),
38 Some(objects) if objects.is_empty() => None,
39 Some(objects) => Some(UploadPlan { objects: Some(objects), ..plan }),
40 };
41 }
42 #[cfg(not(unix))]
43 {
44 let _unused = layout;
45 return Some(plan);
46 }
47}
48
49#[cfg(unix)]
51mod unix {
52 use super::super::diagnostic_log::debug;
53 use super::super::lfs_pointer_scan::{SCAN_BUDGET, new_pointer_objects};
54 use super::super::lfs_upload_plan::UploadPlan;
55 use super::super::repo_read_layout::RepoLayout;
56 use super::super::repo_read_object::{ObjectKind, from_hex, name_length, to_hex};
57 use super::super::repo_read_odb::ObjectDatabase;
58 use super::super::repo_read_refs::{Lookup, RefValue, Refs, is_plain_ref_name};
59
60 const TAG: &str = "cli-git lfs-object-plan";
62
63 const PEEL_LIMIT: usize = 5;
65
66 fn peel(database: &ObjectDatabase, layout: &RepoLayout, hex: &str) -> Option<Vec<u8>> {
68 let mut name: Vec<u8> = from_hex(hex.as_bytes(), layout.object_format)?;
69 for _ in 0..PEEL_LIMIT {
70 let object = database.read(name.as_slice())?;
71 match object.kind {
72 ObjectKind::Commit => return Some(name),
73 ObjectKind::Tag => {
74 let first: &[u8] = object.bytes.split(|byte| { return *byte == b'\n'; }).next()?;
75 name = from_hex(first.strip_prefix(b"object ")?, layout.object_format)?;
76 }
77 ObjectKind::Tree | ObjectKind::Blob => return None,
78 }
79 }
80 return None;
81 }
82
83 fn tip_of(database: &ObjectDatabase, layout: &RepoLayout, refs: &Refs, typed: &str) -> Option<String> {
85 if typed == "HEAD" {
86 return match refs.lookup_head(layout.git_dir.as_path())? {
87 RefValue::Direct(hex) => Some(hex),
88 RefValue::Symbolic(target) => refs.resolve(target.as_str()),
89 };
90 }
91 let plain: bool = !typed.is_empty()
92 && typed.split('/').all(|part| {
93 return !part.is_empty() && part.bytes().all(|byte| { return byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'); });
94 });
95 if !plain {
96 return None;
97 }
98 let candidates: [String; 6] = [
99 typed.to_owned(),
100 format!("refs/{typed}"),
101 format!("refs/tags/{typed}"),
102 format!("refs/heads/{typed}"),
103 format!("refs/remotes/{typed}"),
104 format!("refs/remotes/{typed}/HEAD"),
105 ];
106 for candidate in &candidates {
107 if !is_plain_ref_name(candidate) {
108 continue;
109 }
110 match refs.lookup(candidate) {
111 Lookup::Found(_) => return refs.resolve(candidate),
112 Lookup::Missing => {}
113 Lookup::Unsure => return None,
114 }
115 }
116 let as_name: Vec<u8> = from_hex(typed.as_bytes(), layout.object_format)?;
117 return database.contains(as_name.as_slice()).then(|| { return to_hex(as_name.as_slice()); });
118 }
119
120 pub(super) fn scan(plan: &UploadPlan, layout: &RepoLayout) -> Option<Vec<String>> {
122 let database: ObjectDatabase = ObjectDatabase::open(layout.common_dir.as_path(), None, layout.object_format)?;
123 let refs: Refs = Refs::new(layout.common_dir.as_path(), layout.object_format.hex_length());
124 let listing = refs.list()?;
125 let mut tips: Vec<String> = Vec::new();
126 if plan.everything {
127 for (_, value) in &listing {
128 if let RefValue::Direct(hex) = value {
129 tips.push(hex.clone());
130 }
131 }
132 } else {
133 for typed in &plan.refs {
134 tips.push(tip_of(&database, layout, &refs, typed.to_str()?)?);
135 }
136 }
137 let mut include: Vec<Vec<u8>> = Vec::new();
138 for hex in &tips {
139 include.push(peel(&database, layout, hex)?);
140 }
141 let mut stop: Vec<Vec<u8>> = Vec::new();
142 if !plan.full_scan {
143 let remote: &str = plan.remotes.first()?.to_str()?;
144 let prefix: String = format!("refs/remotes/{remote}/");
145 for (name, value) in &listing {
146 if let (true, RefValue::Direct(hex)) = (name.starts_with(prefix.as_str()), value) {
147 stop.push(from_hex(hex.as_bytes(), layout.object_format)?);
148 }
149 }
150 }
151 let objects: Vec<String> = new_pointer_objects(&database, layout.object_format, include.as_slice(), stop.as_slice(), SCAN_BUDGET)?;
152 for name in &objects {
153 let stored = layout.common_dir.join("lfs/objects").join(&name[..2]).join(&name[2..4]).join(name);
154 if !stored.is_file() {
155 debug(TAG, &format!("object {name} is not in the local LFS storage; asking git-lfs"));
156 return None;
157 }
158 }
159 debug(
160 TAG,
161 &format!(
162 "{} tips, {} known tips, full scan {}: {} objects to upload (hash length {})",
163 include.len(),
164 stop.len(),
165 plan.full_scan,
166 objects.len(),
167 name_length(layout.object_format)
168 ),
169 );
170 return Some(objects);
171 }
172}
173
174#[cfg(test)]
176#[path = "lfs_object_plan_tests.rs"]
177mod tests;