/
diff.rb
108 lines (91 loc) · 2.74 KB
/
diff.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# frozen_string_literal: true
module Mutant
module Repository
# Diff index between HEAD and a tree reference
class Diff
include Adamantium, Anima.new(:world, :to)
FORMAT = /\A:\d{6} \d{6} [a-f\d]{40} [a-f\d]{40} [ACDMRTUX]\t(.*)\n\z/.freeze
private_constant(*constants(false))
class Error < RuntimeError; end
# Test if diff changes file at line range
#
# @param [Pathname] path
# @param [Range<Integer>] line_range
#
# @return [Boolean]
#
# @raise [RepositoryError]
# when git command failed
def touches?(path, line_range)
touched_paths
.from_right { |message| fail Error, message }
.fetch(path) { return false }
.touches?(line_range)
end
private
def repository_root
world
.capture_stdout(%w[git rev-parse --show-toplevel])
.fmap(&:chomp)
.fmap(&world.pathname.public_method(:new))
end
def touched_paths
repository_root.bind(&method(:diff_index))
end
memoize :touched_paths
def diff_index(root)
world
.capture_stdout(%W[git diff-index #{to}])
.fmap(&:lines)
.bind do |lines|
Mutant
.traverse(->(line) { parse_line(root, line) }, lines)
.fmap do |paths|
paths.to_h { |path| [path.path, path] }
end
end
end
# rubocop:disable Metrics/MethodLength
def parse_line(root, line)
match = FORMAT.match(line)
if match
Either::Right.new(
Path.new(
path: root.join(match.captures.first),
to: to,
world: world
)
)
else
Either::Left.new("Invalid git diff-index line: #{line}")
end
end
# rubocop:enable Metrics/MethodLength
# Path touched by a diff
class Path
include Adamantium, Anima.new(:world, :to, :path)
DECIMAL = /(?:0|[1-9]\d*)/.freeze
REGEXP = /\A@@ -(#{DECIMAL})(?:,(#{DECIMAL}))? \+(#{DECIMAL})(?:,(#{DECIMAL}))? @@/.freeze
private_constant(*constants(false))
# Test if diff path touches a line range
#
# @param [Range<Integer>] range
#
# @return [Boolean]
def touches?(line_range)
diff_ranges.any? do |range|
Range.overlap?(range, line_range)
end
end
private
def diff_ranges
world
.capture_stdout(%W[git diff --unified=0 #{to} -- #{path}])
.fmap(&Ranges.public_method(:parse))
.from_right
end
memoize :diff_ranges
end # Path
end # Diff
end # Repository
end # Mutant