Module: Mutineer::ChangedLines

Defined in:
lib/mutineer/changed_lines.rb

Overview

Maps each source file to the set of NEW-side line numbers changed since a git ref.

By parsing git diff --unified=0, this restricts mutations to only the diff (issue #2): on a PR you care whether the changed code is tested, so mutating just those lines is fast and actionable.

git is an external tool, not a gem dependency — shelling out is fine. The parse carries the logic; git_diff is injectable so it stays testable without invoking git.

Constant Summary collapse

HUNK =

Matches unified-diff hunks and captures the new-file start/count.

/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/

Class Method Summary collapse

Class Method Details

.for(ref:, files:, project_root:, runner: method(:git_diff)) ⇒ Hash<String, Set<Integer>>

Builds a per-file map of changed new-side lines.

Parameters:

  • ref (String)

    git ref to diff against.

  • files (Array<String>)

    source files to inspect.

  • project_root (String)

    repository root for git -C.

  • runner (#call) (defaults to: method(:git_diff))

    injectable diff producer.

Returns:

  • (Hash<String, Set<Integer>>)

    absolute file path to changed lines.



51
52
53
54
55
56
# File 'lib/mutineer/changed_lines.rb', line 51

def for(ref:, files:, project_root:, runner: method(:git_diff))
  files.each_with_object({}) do |file, acc|
    abs = File.expand_path(file, project_root)
    acc[abs] = parse(runner.call(ref, abs, project_root))
  end
end

.git_diff(ref, abs_file, project_root) ⇒ String

Returns the stdout of git -C <root> diff --unified=0 <ref> -- <file>.

Parameters:

  • ref (String)

    git ref to diff against.

  • abs_file (String)

    absolute path of the file being diffed.

  • project_root (String)

    repository root for git -C.

Returns:

  • (String)

    diff text, or "" on failure.



64
65
66
67
68
69
70
71
# File 'lib/mutineer/changed_lines.rb', line 64

def git_diff(ref, abs_file, project_root)
  out, _err, status = Open3.capture3(
    "git", "-C", project_root, "diff", "--unified=0", ref, "--", abs_file
  )
  status.success? ? out : ""
rescue StandardError
  ""
end

.parse(diff_text) ⇒ Set<Integer>

Parses unified diff text into the set of NEW-side line numbers.

With --unified=0 each hunk's +c,d block is exactly the changed lines: c..c+d-1. d absent means 1 line; d == 0 is a pure deletion and contributes nothing.

Parameters:

  • diff_text (String)

    raw git diff --unified=0 output.

Returns:

  • (Set<Integer>)

    changed line numbers on the new side.



31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/mutineer/changed_lines.rb', line 31

def parse(diff_text)
  lines = Set.new
  diff_text.each_line do |row|
    m = HUNK.match(row) or next
    start = m[1].to_i
    count = m[2].nil? ? 1 : m[2].to_i
    next if count.zero?

    lines.merge(start...(start + count))
  end
  lines
end