Class: ActiveMutator::SinceFilter

Inherits:
Object
  • Object
show all
Defined in:
lib/active_mutator/since_filter.rb

Overview

Restricts subjects to methods overlapping lines changed since a git ref. Known v1 limit: git diff <ref> omits untracked files, so brand-new uncommitted files are skipped.

Constant Summary collapse

HUNK =
/\A@@ [^+]*\+(\d+)(?:,(\d+))? @@/

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(ref:, root:) ⇒ SinceFilter

Returns a new instance of SinceFilter.

Raises:



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/active_mutator/since_filter.rb', line 23

def initialize(ref:, root:)
  @root = root
  diff = IO.popen(
    ["git", "-C", root, "diff", "--unified=0", ref, "--", "*.rb"], &:read
  )
  raise Error, "git diff #{ref} failed" unless $?.success?

  @changed = self.class.parse(diff)
  untracked = IO.popen(
    ["git", "-C", root, "ls-files", "--others", "--exclude-standard", "--", "*.rb"], &:read
  )
  # Untracked files are invisible to `git diff` but are agentic TDD's most
  # common case (brand-new file + spec). Whole-file sentinel: every line
  # counts as changed.
  untracked.each_line { |l| @changed[l.strip] = :all unless l.strip.empty? }
end

Class Method Details

.parse(diff_text) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
# File 'lib/active_mutator/since_filter.rb', line 8

def self.parse(diff_text)
  changed = Hash.new { |h, k| h[k] = [] }
  current = nil
  diff_text.each_line do |line|
    if line.start_with?("+++ b/")
      current = line.delete_prefix("+++ b/").strip
    elsif current && (match = HUNK.match(line))
      start = match[1].to_i
      count = (match[2] || "1").to_i
      count.times { |i| changed[current] << start + i }
    end
  end
  changed.reject { |_, lines| lines.empty? }
end

Instance Method Details

#cover?(subject) ⇒ Boolean

Returns:

  • (Boolean)


40
41
42
43
44
45
46
# File 'lib/active_mutator/since_filter.rb', line 40

def cover?(subject)
  lines = @changed[subject.file.delete_prefix("#{@root}/")]
  return false unless lines
  return true if lines == :all

  lines.any? { |line| subject.line_range.cover?(line) }
end