1use std::collections::HashMap;
9use std::sync::Arc;
10
11use crate::config::ConfigSet;
12use crate::diff::core::diff_trees_for_pathspecs;
13use crate::diff::posix_ere::PosixEre;
14use crate::diff::structured::SideBytes;
15use crate::diff::{
16 detect_renames_with_policy, renames_from_config, DiffEntry, DiffStatus, RenameDetection,
17};
18use crate::error::{Error, Result};
19use crate::objects::{parse_commit, FileMode, ObjectId};
20use crate::parallel::ThreadPolicy;
21use crate::repo::Repository;
22
23#[derive(Debug, Clone, Copy)]
25pub(crate) struct PickaxeRenames<'a> {
26 threshold: Option<u32>,
28 config: &'a ConfigSet,
30}
31
32impl<'a> PickaxeRenames<'a> {
33 pub(crate) fn new(detection: RenameDetection, config: &'a ConfigSet) -> Self {
34 Self {
35 threshold: detection.threshold_percent(),
36 config,
37 }
38 }
39
40 pub(crate) fn from_config(config: &'a ConfigSet) -> Self {
42 Self::new(renames_from_config(config), config)
43 }
44
45 pub(crate) fn config(&self) -> &'a ConfigSet {
46 self.config
47 }
48}
49
50#[derive(Debug, Clone)]
52pub enum PickaxeKind {
53 Occurrences {
55 needle: String,
57 },
58 LineGrep,
60}
61
62#[derive(Debug, Clone)]
64pub struct Pickaxe {
65 kind: PickaxeKind,
66 ignore_case: bool,
67 pickaxe_regex: bool,
69 ere: Option<PosixEre>,
71}
72
73impl Pickaxe {
74 #[must_use]
76 pub fn occurrences(needle: impl Into<String>) -> Self {
77 Self {
78 kind: PickaxeKind::Occurrences {
79 needle: needle.into(),
80 },
81 ignore_case: false,
82 pickaxe_regex: false,
83 ere: None,
84 }
85 }
86
87 pub fn line_grep(pattern: &str, ignore_case: bool) -> std::result::Result<Self, PickaxeError> {
93 let ere = compile_pickaxe_ere(pattern, ignore_case)?;
94 Ok(Self {
95 kind: PickaxeKind::LineGrep,
96 ignore_case,
97 pickaxe_regex: false,
98 ere: Some(ere),
99 })
100 }
101
102 #[must_use]
104 pub fn ignore_case(mut self, on: bool) -> Self {
105 self.ignore_case = on;
106 self
107 }
108
109 #[must_use]
111 pub fn pickaxe_regex(mut self, on: bool) -> Self {
112 self.pickaxe_regex = on;
113 self
114 }
115
116 pub fn build(mut self) -> std::result::Result<Self, PickaxeError> {
123 if self.pickaxe_regex {
124 let PickaxeKind::Occurrences { ref needle } = self.kind else {
125 return Err(PickaxeError::InvalidPattern {
126 pattern: String::new(),
127 detail: "--pickaxe-regex cannot be used with -G".into(),
128 });
129 };
130 self.ere = Some(compile_pickaxe_ere(needle, self.ignore_case)?);
131 }
132 Ok(self)
133 }
134
135 #[must_use]
137 pub fn is_active(&self) -> bool {
138 true
139 }
140
141 #[must_use]
143 pub fn into_shared(self) -> Arc<Self> {
144 Arc::new(self)
145 }
146
147 pub fn matches_commit(
170 &self,
171 repo: &Repository,
172 commit_oid: ObjectId,
173 paths: &[String],
174 renames: RenameDetection,
175 config: &ConfigSet,
176 ) -> Result<bool> {
177 let data = repo.odb().read(&commit_oid)?.data;
178 let commit = parse_commit(&data)?;
179 if commit.parents.len() > 1 {
180 return Ok(false);
181 }
182 let parent_tree = if let Some(&parent_oid) = commit.parents.first() {
183 let parent_data = repo.odb().read(&parent_oid)?.data;
184 let parent = parse_commit(&parent_data)?;
185 if parent.tree == commit.tree {
186 return Ok(false);
187 }
188 Some(parent.tree)
189 } else {
190 None
191 };
192 let renames = PickaxeRenames::new(renames, config);
193 self.matches_trees(
194 repo,
195 parent_tree.as_ref(),
196 &commit.tree,
197 paths,
198 renames,
199 &mut HashMap::new(),
200 )
201 }
202
203 pub(crate) fn matches_trees(
208 &self,
209 repo: &Repository,
210 parent_tree: Option<&ObjectId>,
211 commit_tree: &ObjectId,
212 paths: &[String],
213 renames: PickaxeRenames<'_>,
214 blob_occurrence_cache: &mut HashMap<ObjectId, usize>,
215 ) -> Result<bool> {
216 let diff_options = crate::diff::DiffOptions::from_config(renames.config());
217 let odb = repo.odb();
218 let entries = diff_trees_for_pathspecs(odb, parent_tree, Some(commit_tree), paths)?;
219 let mut unpaired = Vec::new();
222 for entry in entries {
223 if matches!(entry.status, DiffStatus::Added | DiffStatus::Deleted) {
224 unpaired.push(entry);
225 } else if self.changed_filepair_matches(
226 odb,
227 &entry,
228 &diff_options,
229 blob_occurrence_cache,
230 )? {
231 return Ok(true);
232 }
233 }
234 if unpaired.is_empty() {
235 return Ok(false);
236 }
237 let mut any_side_matches = false;
239 for entry in &unpaired {
240 if self.side_may_match(odb, entry, blob_occurrence_cache)? {
241 any_side_matches = true;
242 break;
243 }
244 }
245 if !any_side_matches {
246 return Ok(false);
247 }
248 let pairs = match renames.threshold {
249 Some(threshold) => detect_renames_with_policy(
250 odb,
251 None,
252 unpaired,
253 threshold,
254 renames.config,
255 &ThreadPolicy::default(),
256 None,
257 )?,
258 None => unpaired,
259 };
260 for entry in &pairs {
261 if self.changed_filepair_matches(odb, entry, &diff_options, blob_occurrence_cache)? {
262 return Ok(true);
263 }
264 }
265 Ok(false)
266 }
267
268 fn changed_filepair_matches(
269 &self,
270 odb: &crate::odb::Odb,
271 entry: &DiffEntry,
272 diff_options: &crate::diff::DiffOptions,
273 blob_occurrence_cache: &mut HashMap<ObjectId, usize>,
274 ) -> Result<bool> {
275 if entry.old_oid == entry.new_oid && entry.old_mode == entry.new_mode {
276 return Ok(false);
277 }
278 self.filepair_matches(odb, entry, diff_options, blob_occurrence_cache)
279 }
280
281 fn side_may_match(
283 &self,
284 odb: &crate::odb::Odb,
285 entry: &DiffEntry,
286 blob_occurrence_cache: &mut HashMap<ObjectId, usize>,
287 ) -> Result<bool> {
288 let (oid, mode) = if entry.status == DiffStatus::Deleted {
289 (&entry.old_oid, entry.old_mode)
290 } else {
291 (&entry.new_oid, entry.new_mode)
292 };
293 match &self.kind {
294 PickaxeKind::Occurrences { .. } => {
295 Ok(self.occurrences_in(odb, oid, mode, blob_occurrence_cache)? > 0)
296 }
297 PickaxeKind::LineGrep => {
298 let (bytes, binary) = blob_side(odb, mode, oid)?;
299 let Some(regex) = self.ere.as_ref().filter(|_| !binary) else {
300 return Ok(false);
301 };
302 Ok(regex.is_match(&bytes.data))
303 }
304 }
305 }
306
307 fn filepair_matches(
308 &self,
309 odb: &crate::odb::Odb,
310 entry: &DiffEntry,
311 diff_options: &crate::diff::DiffOptions,
312 blob_occurrence_cache: &mut HashMap<ObjectId, usize>,
313 ) -> Result<bool> {
314 match &self.kind {
315 PickaxeKind::Occurrences { .. } => {
316 let old = self.occurrences_in(
317 odb,
318 &entry.old_oid,
319 entry.old_mode,
320 blob_occurrence_cache,
321 )?;
322 let new = self.occurrences_in(
323 odb,
324 &entry.new_oid,
325 entry.new_mode,
326 blob_occurrence_cache,
327 )?;
328 Ok(old != new)
329 }
330 PickaxeKind::LineGrep => {
331 let (old_bytes, old_bin) = blob_side(odb, entry.old_mode, &entry.old_oid)?;
332 let (new_bytes, new_bin) = blob_side(odb, entry.new_mode, &entry.new_oid)?;
333 if old_bin || new_bin {
334 return Ok(false);
335 }
336 let Some(regex) = self.ere.as_ref() else {
337 return Ok(false);
338 };
339 if !regex.is_match(&old_bytes.data) && !regex.is_match(&new_bytes.data) {
342 return Ok(false);
343 }
344 Ok(line_grep_match(
345 &String::from_utf8_lossy(&old_bytes.data),
346 &String::from_utf8_lossy(&new_bytes.data),
347 regex,
348 diff_options,
349 ))
350 }
351 }
352 }
353
354 fn occurrences_in(
358 &self,
359 odb: &crate::odb::Odb,
360 oid: &ObjectId,
361 mode: FileMode,
362 cache: &mut HashMap<ObjectId, usize>,
363 ) -> Result<usize> {
364 let PickaxeKind::Occurrences { needle } = &self.kind else {
365 return Ok(0);
366 };
367 if mode.is_absent() || *oid == ObjectId::zero() {
368 return Ok(0);
369 }
370 let count_in =
371 |bytes: &[u8]| count_occurrences(bytes, needle, self.ignore_case, self.ere.as_ref());
372 if mode == FileMode::GITLINK {
373 let (bytes, _) = blob_side(odb, mode, oid)?;
374 return Ok(count_in(bytes.data.as_slice()));
375 }
376 if let Some(&count) = cache.get(oid) {
377 return Ok(count);
378 }
379 let count = count_in(&odb.read(oid)?.data);
380 cache.insert(*oid, count);
381 Ok(count)
382 }
383}
384
385#[derive(Debug, thiserror::Error)]
387pub enum PickaxeError {
388 #[error("invalid pickaxe pattern `{pattern}`: {detail}")]
390 InvalidPattern {
391 pattern: String,
393 detail: String,
395 },
396}
397
398impl From<PickaxeError> for Error {
399 fn from(value: PickaxeError) -> Self {
400 match value {
401 PickaxeError::InvalidPattern { pattern, detail } => {
402 Error::Message(format!("invalid pickaxe pattern `{pattern}`: {detail}"))
403 }
404 }
405 }
406}
407
408fn compile_pickaxe_ere(
410 pattern: &str,
411 ignore_case: bool,
412) -> std::result::Result<PosixEre, PickaxeError> {
413 PosixEre::compile(pattern, ignore_case).map_err(|detail| PickaxeError::InvalidPattern {
414 pattern: pattern.to_owned(),
415 detail,
416 })
417}
418
419fn count_occurrences(
420 haystack: &[u8],
421 needle: &str,
422 ignore_case: bool,
423 ere: Option<&PosixEre>,
424) -> usize {
425 if haystack.is_empty() {
426 return 0;
427 }
428 if let Some(re) = ere {
429 return re.count_nonoverlapping(haystack);
430 }
431 if ignore_case {
432 let h = String::from_utf8_lossy(haystack).to_lowercase();
433 let n = needle.to_lowercase();
434 return count_substr(h.as_bytes(), n.as_bytes());
435 }
436 count_substr(haystack, needle.as_bytes())
437}
438
439fn count_substr(haystack: &[u8], needle: &[u8]) -> usize {
440 if needle.is_empty() {
441 return 0;
442 }
443 memchr::memmem::find_iter(haystack, needle).count()
444}
445
446fn blob_side(odb: &crate::odb::Odb, mode: FileMode, oid: &ObjectId) -> Result<(SideBytes, bool)> {
447 if mode == FileMode::GITLINK {
448 let line = if *oid == ObjectId::zero() {
449 String::new()
450 } else {
451 format!("Subproject commit {}\n", oid.to_hex())
452 };
453 return Ok((SideBytes::from_bytes(line.into_bytes()), false));
454 }
455 if mode.is_absent() || *oid == ObjectId::zero() {
456 return Ok((SideBytes::from_bytes(Vec::new()), false));
457 }
458 let data = odb.read(oid)?.data;
459 let binary = data.iter().take(8000).any(|&b| b == 0);
460 Ok((SideBytes::from_bytes(data), binary))
461}
462
463fn line_grep_match(
464 old_text: &str,
465 new_text: &str,
466 regex: &PosixEre,
467 diff_options: &crate::diff::DiffOptions,
468) -> bool {
469 use crate::diff::core::compacted_line_ops_for_options;
470 use crate::diff::unified_emit::LineSide;
471 use similar::DiffOp;
472
473 let before = LineSide::from_text(old_text);
474 let after = LineSide::from_text(new_text);
475 let old_lines: Vec<&str> = before.lines.iter().map(|l| l.body).collect();
476 let new_lines: Vec<&str> = after.lines.iter().map(|l| l.body).collect();
477 for op in compacted_line_ops_for_options(old_text, new_text, diff_options) {
478 match op {
479 DiffOp::Equal { .. } => {}
480 DiffOp::Delete {
481 old_index, old_len, ..
482 } => {
483 for line in &old_lines[old_index..old_index + old_len] {
484 if regex.is_match(line.as_bytes()) {
485 return true;
486 }
487 }
488 }
489 DiffOp::Insert {
490 new_index, new_len, ..
491 } => {
492 for line in &new_lines[new_index..new_index + new_len] {
493 if regex.is_match(line.as_bytes()) {
494 return true;
495 }
496 }
497 }
498 DiffOp::Replace {
499 old_index,
500 old_len,
501 new_index,
502 new_len,
503 ..
504 } => {
505 for line in &old_lines[old_index..old_index + old_len] {
506 if regex.is_match(line.as_bytes()) {
507 return true;
508 }
509 }
510 for line in &new_lines[new_index..new_index + new_len] {
511 if regex.is_match(line.as_bytes()) {
512 return true;
513 }
514 }
515 }
516 }
517 }
518 false
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524
525 #[test]
526 fn literal_counts_do_not_overlap() {
527 assert_eq!(count_occurrences(b"aaaa", "aa", false, None), 2);
528 assert_eq!(count_occurrences(b"aaa", "aa", false, None), 1);
529 assert_eq!(count_occurrences(b"abcabc", "abc", false, None), 2);
530 assert_eq!(count_occurrences(b"abc", "", false, None), 0);
531 assert_eq!(count_occurrences(b"", "a", false, None), 0);
532 }
533
534 #[test]
535 fn ignore_case_counts_fold_both_sides() {
536 assert_eq!(count_occurrences(b"Needle NEEDLE", "needle", true, None), 2);
537 assert_eq!(
538 count_occurrences(b"Needle NEEDLE", "needle", false, None),
539 0
540 );
541 }
542}