1use super::blob_batch::load_blob_batch;
17use super::replay_containment::contains_landed_change;
19use super::replay_patch::{PatchHunk, parse_indexed_patch, split_keeping_newlines};
20use super::replay_reverse_apply::reverse_applies;
21use super::replay_shadow::run_shadow_checked;
22use super::replay_shared_paths::{SharedPath, TreeEntry};
23use super::transaction_git::{GitContext, GitFailure};
24use std::collections::{BTreeMap, HashMap};
26use std::path::Path;
28
29const BINARY_PROBE_BYTES: usize = 8_000;
31
32const STRICT_CONTEXT: u32 = 3;
34
35#[derive(Clone, Debug, Eq, PartialEq)]
46pub enum TextSubsumptionError {
47 Git(GitFailure),
49 Blob(String),
51 Format(String),
53}
54
55fn is_binary(bytes: &[u8]) -> bool {
66 return bytes[..bytes.len().min(BINARY_PROBE_BYTES)].contains(&0);
67}
68
69fn empty_blob_for(
80 context: &GitContext,
81 shadow: &Path,
82 cwd: &Path,
83 candidates: &[SharedPath],
84) -> Result<String, TextSubsumptionError> {
85 if !candidates.iter().any(|shared| { return shared.base.is_none(); }) {
86 return Ok(String::new());
87 }
88 let output = run_shadow_checked(
89 context,
90 shadow,
91 cwd,
92 &["hash-object", "-w", "-t", "blob", "--stdin"],
93 Some(&[]),
94 None,
95 &[],
96 )
97 .map_err(TextSubsumptionError::Git)?;
98 return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
99}
100
101fn side_oid(entry: &Option<TreeEntry>, empty_blob: &str) -> String {
112 return match entry {
113 Some(present) => present.oid.clone(),
114 None => String::from(empty_blob),
115 };
116}
117
118fn indexed_tree(
128 context: &GitContext,
129 shadow: &Path,
130 cwd: &Path,
131 oids: &[String],
132) -> Result<String, TextSubsumptionError> {
133 let mut input: Vec<u8> = Vec::new();
135 for (index, oid) in oids.iter().enumerate() {
136 input.extend_from_slice(format!("100644 blob {oid}\t{index}\0").as_bytes());
137 }
138 let output = run_shadow_checked(context, shadow, cwd, &["mktree", "-z"], Some(&input), None, &[])
139 .map_err(TextSubsumptionError::Git)?;
140 return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
141}
142
143fn indexed_hunks(
153 context: &GitContext,
154 shadow: &Path,
155 cwd: &Path,
156 from: &str,
157 to: &str,
158 lines_of_context: u32,
159) -> Result<BTreeMap<Vec<u8>, Vec<PatchHunk>>, TextSubsumptionError> {
160 let unified: String = format!("--unified={lines_of_context}");
161 let output = run_shadow_checked(
162 context,
163 shadow,
164 cwd,
165 &[
166 "diff-tree",
167 "-p",
168 "--text",
169 unified.as_str(),
170 "--no-color",
171 "--no-ext-diff",
172 "--no-textconv",
173 "--no-renames",
174 "--src-prefix=a/",
175 "--dst-prefix=b/",
176 from,
177 to,
178 ],
179 None,
180 None,
181 &[],
182 )
183 .map_err(TextSubsumptionError::Git)?;
184 return parse_indexed_patch(&output.stdout)
185 .map_err(|error| { return TextSubsumptionError::Format(error.0); });
186}
187
188pub fn subsumed_text_paths(
199 context: &GitContext,
200 shadow: &Path,
201 cwd: &Path,
202 object_directory: &Path,
203 candidates: &[SharedPath],
204) -> Result<Vec<String>, TextSubsumptionError> {
205 if candidates.is_empty() {
206 return Ok(Vec::new());
207 }
208 let empty_blob: String = empty_blob_for(context, shadow, cwd, candidates)?;
209 let oids: Vec<String> = candidates
211 .iter()
212 .flat_map(|shared| { return [&shared.base, &shared.landed, &shared.prepared]; })
213 .filter_map(|entry| { return entry.as_ref().map(|present| { return present.oid.clone(); }); })
214 .collect();
215 let blobs: HashMap<String, Vec<u8>> = load_blob_batch(context, cwd, &oids, Some(object_directory))
216 .map_err(TextSubsumptionError::Blob)?;
217 let blob_of = |entry: &Option<TreeEntry>| -> Option<Vec<u8>> {
218 return entry.as_ref().map(|present| { return blobs.get(&present.oid).cloned().unwrap_or_default(); });
219 };
220 let text: Vec<&SharedPath> = candidates
222 .iter()
223 .filter(|shared| {
224 return [&shared.base, &shared.landed, &shared.prepared].iter().all(|entry| {
225 let blob = entry.as_ref().map(|present| { return blobs.get(&present.oid); });
226 return match blob {
227 None => true,
228 Some(Some(bytes)) => !is_binary(bytes),
229 Some(None) => false,
230 };
231 });
232 })
233 .collect();
234 if text.is_empty() {
235 return Ok(Vec::new());
236 }
237 let owned: Vec<SharedPath> = text.iter().map(|shared| { return (*shared).clone(); }).collect();
238 let base_tree: String = indexed_tree(
239 context,
240 shadow,
241 cwd,
242 &owned.iter().map(|shared| { return side_oid(&shared.base, &empty_blob); }).collect::<Vec<String>>(),
243 )?;
244 let landed_tree: String = indexed_tree(
245 context,
246 shadow,
247 cwd,
248 &owned.iter().map(|shared| { return side_oid(&shared.landed, &empty_blob); }).collect::<Vec<String>>(),
249 )?;
250 let prepared_tree: String = indexed_tree(
251 context,
252 shadow,
253 cwd,
254 &owned.iter().map(|shared| { return side_oid(&shared.prepared, &empty_blob); }).collect::<Vec<String>>(),
255 )?;
256 let strict: BTreeMap<Vec<u8>, Vec<PatchHunk>> =
257 indexed_hunks(context, shadow, cwd, &base_tree, &landed_tree, STRICT_CONTEXT)?;
258 let landed_zero: BTreeMap<Vec<u8>, Vec<PatchHunk>> =
259 indexed_hunks(context, shadow, cwd, &base_tree, &landed_tree, 0)?;
260 let prepared_zero: BTreeMap<Vec<u8>, Vec<PatchHunk>> =
261 indexed_hunks(context, shadow, cwd, &base_tree, &prepared_tree, 0)?;
262 let mut subsumed: Vec<String> = Vec::new();
264 for (index, shared) in owned.iter().enumerate() {
265 let name: Vec<u8> = index.to_string().into_bytes();
266 let prepared_bytes: Vec<u8> = blob_of(&shared.prepared).unwrap_or_default();
267 let base_bytes: Vec<u8> = blob_of(&shared.base).unwrap_or_default();
268 let strict_hunks: &[PatchHunk] = strict.get(&name).map(Vec::as_slice).unwrap_or_default();
269 let landed_hunks: &[PatchHunk] = landed_zero.get(&name).map(Vec::as_slice).unwrap_or_default();
270 let prepared_hunks: &[PatchHunk] = prepared_zero.get(&name).map(Vec::as_slice).unwrap_or_default();
271 let base_lines: Vec<Vec<u8>> = split_keeping_newlines(&base_bytes);
272 if reverse_applies(strict_hunks, &prepared_bytes)
273 || contains_landed_change(landed_hunks, prepared_hunks, &base_lines)
274 {
275 subsumed.push(shared.path.clone());
276 }
277 }
278 return Ok(subsumed);
279}
280
281#[cfg(test)]
282#[path = "replay_subsumption_text_tests.rs"]
283mod tests;