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 fn new(detection: RenameDetection, config: &'a ConfigSet) -> Self {
35 Self {
36 threshold: detection.threshold_percent(),
37 config,
38 }
39 }
40
41 pub fn from_config(config: &'a ConfigSet) -> Self {
43 Self::new(renames_from_config(config), config)
44 }
45
46 pub 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 let parent_ctx = crate::shallow::EffectiveCommitParents::load(repo);
181 let effective_parents = parent_ctx.for_commit(commit_oid, &commit.parents);
182 if effective_parents.len() > 1 {
183 return Ok(false);
184 }
185 let parent_tree = if let Some(&parent_oid) = effective_parents.first() {
186 let parent_data = repo.odb().read(&parent_oid)?.data;
187 let parent = parse_commit(&parent_data)?;
188 if parent.tree == commit.tree {
189 return Ok(false);
190 }
191 Some(parent.tree)
192 } else {
193 None
194 };
195 let renames = PickaxeRenames::new(renames, config);
196 self.matches_trees(
197 repo,
198 parent_tree.as_ref(),
199 &commit.tree,
200 paths,
201 renames,
202 &mut HashMap::new(),
203 )
204 }
205
206 pub(crate) fn matches_trees(
211 &self,
212 repo: &Repository,
213 parent_tree: Option<&ObjectId>,
214 commit_tree: &ObjectId,
215 paths: &[String],
216 renames: PickaxeRenames<'_>,
217 blob_occurrence_cache: &mut HashMap<ObjectId, usize>,
218 ) -> Result<bool> {
219 let diff_options = crate::diff::DiffOptions::from_config(renames.config());
220 let odb = repo.odb();
221 let pathspec_matcher = if paths.is_empty() {
222 None
223 } else {
224 Some(repo.pathspec_matcher(paths)?)
225 };
226 let entries = diff_trees_for_pathspecs(
227 odb,
228 parent_tree,
229 Some(commit_tree),
230 paths,
231 pathspec_matcher.as_ref(),
232 )?;
233 let mut unpaired = Vec::new();
236 for entry in entries {
237 if matches!(entry.status, DiffStatus::Added | DiffStatus::Deleted) {
238 unpaired.push(entry);
239 } else if self.changed_filepair_matches(
240 odb,
241 &entry,
242 &diff_options,
243 blob_occurrence_cache,
244 )? {
245 return Ok(true);
246 }
247 }
248 if unpaired.is_empty() {
249 return Ok(false);
250 }
251 let mut any_side_matches = false;
253 for entry in &unpaired {
254 if self.side_may_match(odb, entry, blob_occurrence_cache)? {
255 any_side_matches = true;
256 break;
257 }
258 }
259 if !any_side_matches {
260 return Ok(false);
261 }
262 let pairs = match renames.threshold {
263 Some(threshold) => {
264 detect_renames_with_policy(
265 odb,
266 None,
267 unpaired,
268 threshold,
269 renames.config,
270 &ThreadPolicy::default(),
271 None,
272 DetectRenamesOptions::default(),
273 )?
274 .entries
275 }
276 None => unpaired,
277 };
278 for entry in &pairs {
279 if self.changed_filepair_matches(odb, entry, &diff_options, blob_occurrence_cache)? {
280 return Ok(true);
281 }
282 }
283 Ok(false)
284 }
285
286 fn changed_filepair_matches(
287 &self,
288 odb: &crate::odb::Odb,
289 entry: &DiffEntry,
290 diff_options: &crate::diff::DiffOptions,
291 blob_occurrence_cache: &mut HashMap<ObjectId, usize>,
292 ) -> Result<bool> {
293 if entry.old_oid == entry.new_oid && entry.old_mode == entry.new_mode {
294 return Ok(false);
295 }
296 self.filepair_matches(odb, entry, diff_options, blob_occurrence_cache)
297 }
298
299 fn side_may_match(
301 &self,
302 odb: &crate::odb::Odb,
303 entry: &DiffEntry,
304 blob_occurrence_cache: &mut HashMap<ObjectId, usize>,
305 ) -> Result<bool> {
306 let (oid, mode) = if entry.status == DiffStatus::Deleted {
307 (&entry.old_oid, entry.old_mode)
308 } else {
309 (&entry.new_oid, entry.new_mode)
310 };
311 match &self.kind {
312 PickaxeKind::Occurrences { .. } => {
313 Ok(self.occurrences_in(odb, oid, mode, blob_occurrence_cache)? > 0)
314 }
315 PickaxeKind::LineGrep => {
316 let (bytes, binary) = blob_side(odb, mode, oid)?;
317 let Some(regex) = self.ere.as_ref().filter(|_| !binary) else {
318 return Ok(false);
319 };
320 Ok(regex.is_match(&bytes.data))
321 }
322 }
323 }
324
325 fn filepair_matches(
326 &self,
327 odb: &crate::odb::Odb,
328 entry: &DiffEntry,
329 diff_options: &crate::diff::DiffOptions,
330 blob_occurrence_cache: &mut HashMap<ObjectId, usize>,
331 ) -> Result<bool> {
332 match &self.kind {
333 PickaxeKind::Occurrences { .. } => {
334 let old = self.occurrences_in(
335 odb,
336 &entry.old_oid,
337 entry.old_mode,
338 blob_occurrence_cache,
339 )?;
340 let new = self.occurrences_in(
341 odb,
342 &entry.new_oid,
343 entry.new_mode,
344 blob_occurrence_cache,
345 )?;
346 Ok(old != new)
347 }
348 PickaxeKind::LineGrep => {
349 let (old_bytes, old_bin) = blob_side(odb, entry.old_mode, &entry.old_oid)?;
350 let (new_bytes, new_bin) = blob_side(odb, entry.new_mode, &entry.new_oid)?;
351 if old_bin || new_bin {
352 return Ok(false);
353 }
354 let Some(regex) = self.ere.as_ref() else {
355 return Ok(false);
356 };
357 if !regex.is_match(&old_bytes.data) && !regex.is_match(&new_bytes.data) {
360 return Ok(false);
361 }
362 Ok(line_grep_match(
363 &String::from_utf8_lossy(&old_bytes.data),
364 &String::from_utf8_lossy(&new_bytes.data),
365 regex,
366 diff_options,
367 ))
368 }
369 }
370 }
371
372 fn occurrences_in(
376 &self,
377 odb: &crate::odb::Odb,
378 oid: &ObjectId,
379 mode: FileMode,
380 cache: &mut HashMap<ObjectId, usize>,
381 ) -> Result<usize> {
382 let PickaxeKind::Occurrences { needle } = &self.kind else {
383 return Ok(0);
384 };
385 if mode.is_absent() || *oid == ObjectId::zero() {
386 return Ok(0);
387 }
388 let count_in =
389 |bytes: &[u8]| count_occurrences(bytes, needle, self.ignore_case, self.ere.as_ref());
390 if mode == FileMode::GITLINK {
391 let (bytes, _) = blob_side(odb, mode, oid)?;
392 return Ok(count_in(bytes.data.as_slice()));
393 }
394 if let Some(&count) = cache.get(oid) {
395 return Ok(count);
396 }
397 let count = count_in(&odb.read(oid)?.data);
398 cache.insert(*oid, count);
399 Ok(count)
400 }
401}
402
403#[derive(Debug, thiserror::Error)]
405pub enum PickaxeError {
406 #[error("invalid pickaxe pattern `{pattern}`: {detail}")]
408 InvalidPattern {
409 pattern: String,
411 detail: String,
413 },
414}
415
416impl From<PickaxeError> for Error {
417 fn from(value: PickaxeError) -> Self {
418 match value {
419 PickaxeError::InvalidPattern { pattern, detail } => {
420 Error::Message(format!("invalid pickaxe pattern `{pattern}`: {detail}"))
421 }
422 }
423 }
424}
425
426fn compile_pickaxe_ere(
428 pattern: &str,
429 ignore_case: bool,
430) -> std::result::Result<PosixEre, PickaxeError> {
431 PosixEre::compile(pattern, ignore_case).map_err(|detail| PickaxeError::InvalidPattern {
432 pattern: pattern.to_owned(),
433 detail,
434 })
435}
436
437fn count_occurrences(
438 haystack: &[u8],
439 needle: &str,
440 ignore_case: bool,
441 ere: Option<&PosixEre>,
442) -> usize {
443 if haystack.is_empty() {
444 return 0;
445 }
446 if let Some(re) = ere {
447 return re.count_nonoverlapping(haystack);
448 }
449 if ignore_case {
450 let h = String::from_utf8_lossy(haystack).to_lowercase();
451 let n = needle.to_lowercase();
452 return count_substr(h.as_bytes(), n.as_bytes());
453 }
454 count_substr(haystack, needle.as_bytes())
455}
456
457fn count_substr(haystack: &[u8], needle: &[u8]) -> usize {
458 if needle.is_empty() {
459 return 0;
460 }
461 memchr::memmem::find_iter(haystack, needle).count()
462}
463
464fn blob_side(odb: &crate::odb::Odb, mode: FileMode, oid: &ObjectId) -> Result<(SideBytes, bool)> {
465 if mode == FileMode::GITLINK {
466 let line = if *oid == ObjectId::zero() {
467 String::new()
468 } else {
469 format!("Subproject commit {}\n", oid.to_hex())
470 };
471 return Ok((SideBytes::from_bytes(line.into_bytes()), false));
472 }
473 if mode.is_absent() || *oid == ObjectId::zero() {
474 return Ok((SideBytes::from_bytes(Vec::new()), false));
475 }
476 let data = odb.read(oid)?.data;
477 let binary = data.iter().take(8000).any(|&b| b == 0);
478 Ok((SideBytes::from_bytes(data), binary))
479}
480
481fn line_grep_match(
482 old_text: &str,
483 new_text: &str,
484 regex: &PosixEre,
485 diff_options: &crate::diff::DiffOptions,
486) -> bool {
487 use crate::diff::core::compacted_line_ops_for_options;
488 use crate::diff::unified_emit::LineSide;
489 use similar::DiffOp;
490
491 let before = LineSide::from_text(old_text);
492 let after = LineSide::from_text(new_text);
493 let old_lines: Vec<&str> = before.lines().map(|l| l.body).collect();
494 let new_lines: Vec<&str> = after.lines().map(|l| l.body).collect();
495 for op in compacted_line_ops_for_options(old_text, new_text, diff_options) {
496 match op {
497 DiffOp::Equal { .. } => {}
498 DiffOp::Delete {
499 old_index, old_len, ..
500 } => {
501 for line in &old_lines[old_index..old_index + old_len] {
502 if regex.is_match(line.as_bytes()) {
503 return true;
504 }
505 }
506 }
507 DiffOp::Insert {
508 new_index, new_len, ..
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 DiffOp::Replace {
517 old_index,
518 old_len,
519 new_index,
520 new_len,
521 ..
522 } => {
523 for line in &old_lines[old_index..old_index + old_len] {
524 if regex.is_match(line.as_bytes()) {
525 return true;
526 }
527 }
528 for line in &new_lines[new_index..new_index + new_len] {
529 if regex.is_match(line.as_bytes()) {
530 return true;
531 }
532 }
533 }
534 }
535 }
536 false
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 #[test]
544 fn literal_counts_do_not_overlap() {
545 assert_eq!(count_occurrences(b"aaaa", "aa", false, None), 2);
546 assert_eq!(count_occurrences(b"aaa", "aa", false, None), 1);
547 assert_eq!(count_occurrences(b"abcabc", "abc", false, None), 2);
548 assert_eq!(count_occurrences(b"abc", "", false, None), 0);
549 assert_eq!(count_occurrences(b"", "a", false, None), 0);
550 }
551
552 #[test]
553 fn ignore_case_counts_fold_both_sides() {
554 assert_eq!(count_occurrences(b"Needle NEEDLE", "needle", true, None), 2);
555 assert_eq!(
556 count_occurrences(b"Needle NEEDLE", "needle", false, None),
557 0
558 );
559 }
560}