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