Class: Moult::Diff
- Inherits:
-
Object
- Object
- Moult::Diff
- Defined in:
- lib/moult/diff.rb
Overview
A Moult-owned value object describing what changed between a base ref and the working tree, plus the pure filter the gate uses to decide whether a finding is "in the diff". This is the genuinely novel component of the PR gate — it is pinned against hand-built git output exactly like the coverage Resolver and the ABC metric; drift is a bug.
Git is the only file that shells git; it hands this class raw
--name-status and --unified=0 text. Diff.parse turns that text into a Diff
with no IO, so it is trivially unit-testable. Diff.compute is the thin IO wrapper
that calls git then Diff.parse.
Line ranges are taken from the NEW side of each --unified=0 hunk header
(@@ -a,b +c,d @@): with zero context they are precisely the added/changed
lines. Paths are repo-root-relative (git's own framing); the gate is meant to
run at the repository root, where they line up with Moult's root-relative
finding paths.
Defined Under Namespace
Classes: ChangedFile
Instance Attribute Summary collapse
-
#base_ref ⇒ Object
readonly
Returns the value of attribute base_ref.
-
#files ⇒ Object
readonly
Returns the value of attribute files.
-
#merge_base ⇒ Object
readonly
Returns the value of attribute merge_base.
-
#scope ⇒ Object
readonly
Returns the value of attribute scope.
Class Method Summary collapse
-
.compute(root:, base_ref:, scope: :diff) ⇒ Diff
Resolve the diff for
rootagainstbase_refvia Git, then Diff.parse. -
.parse(name_status:, unified_diff:, base_ref:, merge_base:, scope: :diff) ⇒ Diff
Build a Diff from raw git text.
Instance Method Summary collapse
-
#in_diff?(path:, start_line: nil, end_line: nil) ⇒ Boolean
Line-level membership: is the span [start_line, end_line] inside the diff? Used where an analysis has lines (complexity methods, dead-code spans, duplication/flag occurrences).
-
#includes_path?(path) ⇒ Boolean
Path-level membership: did this file change at all? The fallback where an analysis is file-keyed with no line numbers (boundaries — null symbol_id).
-
#initialize(base_ref:, merge_base:, scope:, files:) ⇒ Diff
constructor
A new instance of Diff.
Constructor Details
#initialize(base_ref:, merge_base:, scope:, files:) ⇒ Diff
Returns a new instance of Diff.
42 43 44 45 46 47 48 |
# File 'lib/moult/diff.rb', line 42 def initialize(base_ref:, merge_base:, scope:, files:) @base_ref = base_ref @merge_base = merge_base @scope = scope @files = files @by_path = files.to_h { |f| [f.path, f] } end |
Instance Attribute Details
#base_ref ⇒ Object (readonly)
Returns the value of attribute base_ref.
36 37 38 |
# File 'lib/moult/diff.rb', line 36 def base_ref @base_ref end |
#files ⇒ Object (readonly)
Returns the value of attribute files.
36 37 38 |
# File 'lib/moult/diff.rb', line 36 def files @files end |
#merge_base ⇒ Object (readonly)
Returns the value of attribute merge_base.
36 37 38 |
# File 'lib/moult/diff.rb', line 36 def merge_base @merge_base end |
#scope ⇒ Object (readonly)
Returns the value of attribute scope.
36 37 38 |
# File 'lib/moult/diff.rb', line 36 def scope @scope end |
Class Method Details
.compute(root:, base_ref:, scope: :diff) ⇒ Diff
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/moult/diff.rb', line 92 def compute(root:, base_ref:, scope: :diff) return new(base_ref: nil, merge_base: nil, scope: :all, files: []) if scope == :all mb = Git.merge_base(root, base_ref) unless mb raise Moult::Error, "could not resolve a merge-base between #{base_ref.inspect} and HEAD " \ "(unknown ref, shallow clone, or not a git repository); " \ "pass --base REF or --scope all" end parse( name_status: Git.diff_name_status(root, mb) || "", unified_diff: Git.diff_unified_zero(root, mb) || "", base_ref: base_ref, merge_base: mb, scope: :diff ) end |
.parse(name_status:, unified_diff:, base_ref:, merge_base:, scope: :diff) ⇒ Diff
Build a Diff from raw git text. PURE — no IO. Pinned in test/test_diff.rb.
80 81 82 83 84 85 86 |
# File 'lib/moult/diff.rb', line 80 def parse(name_status:, unified_diff:, base_ref:, merge_base:, scope: :diff) ranges = parse_unified(utf8(unified_diff)) files = parse_name_status(utf8(name_status)).map do |path, status| ChangedFile.new(path: path, status: status, line_ranges: ranges[path] || []) end new(base_ref: base_ref, merge_base: merge_base, scope: scope, files: files) end |
Instance Method Details
#in_diff?(path:, start_line: nil, end_line: nil) ⇒ Boolean
Line-level membership: is the span [start_line, end_line] inside the diff?
Used where an analysis has lines (complexity methods, dead-code spans,
duplication/flag occurrences). With start_line nil this falls back to
path-level. Always true under :all scope.
55 56 57 58 59 60 61 62 63 |
# File 'lib/moult/diff.rb', line 55 def in_diff?(path:, start_line: nil, end_line: nil) return true if scope == :all return includes_path?(path) if start_line.nil? file = @by_path[path] return false unless file file.changed_range?(start_line, end_line || start_line) end |
#includes_path?(path) ⇒ Boolean
Path-level membership: did this file change at all? The fallback where an analysis is file-keyed with no line numbers (boundaries — null symbol_id). Always true under :all scope.
69 70 71 72 73 |
# File 'lib/moult/diff.rb', line 69 def includes_path?(path) return true if scope == :all @by_path.key?(path) end |