patch_util 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +285 -0
- data/Rakefile +8 -0
- data/SKILL.md +157 -0
- data/exe/patch_util +12 -0
- data/lib/patch_util/cli.rb +22 -0
- data/lib/patch_util/diff.rb +132 -0
- data/lib/patch_util/git/cli.rb +664 -0
- data/lib/patch_util/git/rewrite_cli.rb +393 -0
- data/lib/patch_util/git/rewrite_session_manager.rb +480 -0
- data/lib/patch_util/git/rewrite_state_store.rb +81 -0
- data/lib/patch_util/git/rewriter.rb +233 -0
- data/lib/patch_util/git.rb +11 -0
- data/lib/patch_util/parser.rb +412 -0
- data/lib/patch_util/selection.rb +98 -0
- data/lib/patch_util/source.rb +69 -0
- data/lib/patch_util/split/applier.rb +38 -0
- data/lib/patch_util/split/cli.rb +167 -0
- data/lib/patch_util/split/emitter.rb +52 -0
- data/lib/patch_util/split/inspector.rb +203 -0
- data/lib/patch_util/split/plan.rb +33 -0
- data/lib/patch_util/split/plan_store.rb +106 -0
- data/lib/patch_util/split/planner.rb +24 -0
- data/lib/patch_util/split/projector.rb +252 -0
- data/lib/patch_util/split/verifier.rb +133 -0
- data/lib/patch_util/split.rb +21 -0
- data/lib/patch_util/version.rb +5 -0
- data/lib/patch_util.rb +21 -0
- metadata +92 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module PatchUtil
|
|
7
|
+
module Split
|
|
8
|
+
class PlanStore
|
|
9
|
+
FORMAT_VERSION = 1
|
|
10
|
+
|
|
11
|
+
attr_reader :path
|
|
12
|
+
|
|
13
|
+
def initialize(path:)
|
|
14
|
+
@path = File.expand_path(path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def load
|
|
18
|
+
return PlanSet.new(version: FORMAT_VERSION, entries: []) unless File.exist?(path)
|
|
19
|
+
|
|
20
|
+
payload = JSON.parse(File.read(path))
|
|
21
|
+
entries = []
|
|
22
|
+
payload.fetch('entries', []).each do |entry|
|
|
23
|
+
entries << deserialize_entry(entry)
|
|
24
|
+
end
|
|
25
|
+
PlanSet.new(version: payload.fetch('version', FORMAT_VERSION), entries: entries)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def save(plan_set)
|
|
29
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
30
|
+
File.write(path, JSON.pretty_generate(serialize_plan_set(plan_set)) + "\n")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def upsert(plan_set, entry)
|
|
34
|
+
entries = []
|
|
35
|
+
replaced = false
|
|
36
|
+
|
|
37
|
+
plan_set.entries.each do |existing|
|
|
38
|
+
if existing.source_kind == entry.source_kind && existing.source_fingerprint == entry.source_fingerprint
|
|
39
|
+
entries << entry
|
|
40
|
+
replaced = true
|
|
41
|
+
else
|
|
42
|
+
entries << existing
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
entries << entry unless replaced
|
|
47
|
+
PlanSet.new(version: FORMAT_VERSION, entries: entries)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def serialize_plan_set(plan_set)
|
|
53
|
+
entries = []
|
|
54
|
+
plan_set.entries.each do |entry|
|
|
55
|
+
entries << {
|
|
56
|
+
'source_kind' => entry.source_kind,
|
|
57
|
+
'source_label' => entry.source_label,
|
|
58
|
+
'source_fingerprint' => entry.source_fingerprint,
|
|
59
|
+
'created_at' => entry.created_at,
|
|
60
|
+
'chunks' => serialize_chunks(entry.chunks)
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
'version' => plan_set.version,
|
|
66
|
+
'entries' => entries
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def serialize_chunks(chunks)
|
|
71
|
+
items = []
|
|
72
|
+
chunks.each do |chunk|
|
|
73
|
+
items << {
|
|
74
|
+
'name' => chunk.name,
|
|
75
|
+
'selector_text' => chunk.selector_text,
|
|
76
|
+
'row_ids' => chunk.row_ids,
|
|
77
|
+
'change_labels' => chunk.change_labels,
|
|
78
|
+
'leftovers' => chunk.leftovers?
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
items
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def deserialize_entry(entry)
|
|
85
|
+
chunks = []
|
|
86
|
+
entry.fetch('chunks', []).each do |chunk|
|
|
87
|
+
chunks << Chunk.new(
|
|
88
|
+
name: chunk.fetch('name'),
|
|
89
|
+
selector_text: chunk['selector_text'],
|
|
90
|
+
row_ids: chunk.fetch('row_ids'),
|
|
91
|
+
change_labels: chunk.fetch('change_labels'),
|
|
92
|
+
leftovers: chunk.fetch('leftovers', false)
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
PlanEntry.new(
|
|
97
|
+
source_kind: entry.fetch('source_kind'),
|
|
98
|
+
source_label: entry.fetch('source_label'),
|
|
99
|
+
source_fingerprint: entry.fetch('source_fingerprint'),
|
|
100
|
+
chunks: chunks,
|
|
101
|
+
created_at: entry.fetch('created_at')
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module PatchUtil
|
|
6
|
+
module Split
|
|
7
|
+
class Planner
|
|
8
|
+
def initialize(verifier: Verifier.new)
|
|
9
|
+
@verifier = verifier
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build(source:, diff:, chunk_requests:)
|
|
13
|
+
chunks = @verifier.build_chunks(diff: diff, chunk_requests: chunk_requests)
|
|
14
|
+
PlanEntry.new(
|
|
15
|
+
source_kind: source.kind,
|
|
16
|
+
source_label: source.label,
|
|
17
|
+
source_fingerprint: source.fingerprint,
|
|
18
|
+
chunks: chunks,
|
|
19
|
+
created_at: Time.now.utc.iso8601
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PatchUtil
|
|
4
|
+
module Split
|
|
5
|
+
ProjectedFile = Data.define(:old_path, :new_path, :diff_git_line, :metadata_lines, :hunks, :emit_text_headers)
|
|
6
|
+
ProjectedHunk = Data.define(:old_start, :old_count, :new_start, :new_count, :lines, :kind, :patch_lines)
|
|
7
|
+
|
|
8
|
+
class Projector
|
|
9
|
+
def initialize(diff:, plan_entry:)
|
|
10
|
+
@diff = diff
|
|
11
|
+
@plan_entry = plan_entry
|
|
12
|
+
@chunk_index_by_row_id = {}
|
|
13
|
+
|
|
14
|
+
@plan_entry.chunks.each_with_index do |chunk, chunk_index|
|
|
15
|
+
chunk.row_ids.each do |row_id|
|
|
16
|
+
@chunk_index_by_row_id[row_id] = chunk_index
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def project_chunk(chunk_index)
|
|
22
|
+
projected_files = []
|
|
23
|
+
|
|
24
|
+
@diff.file_diffs.each do |file_diff|
|
|
25
|
+
before_offset = 0
|
|
26
|
+
after_offset = 0
|
|
27
|
+
projected_hunks = []
|
|
28
|
+
path_operation_applied_before = path_operation_applied_before?(file_diff, chunk_index)
|
|
29
|
+
path_operation_applied_after = path_operation_applied_after?(file_diff, chunk_index)
|
|
30
|
+
|
|
31
|
+
file_diff.hunks.each do |hunk|
|
|
32
|
+
case hunk.kind
|
|
33
|
+
when :file_operation
|
|
34
|
+
projected_hunk = project_operation_hunk(hunk, chunk_index)
|
|
35
|
+
projected_hunks << projected_hunk if projected_hunk
|
|
36
|
+
when :binary
|
|
37
|
+
projected_hunk = project_binary_hunk(hunk, chunk_index)
|
|
38
|
+
projected_hunks << projected_hunk if projected_hunk
|
|
39
|
+
else
|
|
40
|
+
changed, projected_hunk = project_text_hunk(hunk, chunk_index, before_offset, after_offset)
|
|
41
|
+
projected_hunks << projected_hunk if changed
|
|
42
|
+
|
|
43
|
+
before_offset += applied_delta(hunk, max_chunk_index: chunk_index - 1)
|
|
44
|
+
after_offset += applied_delta(hunk, max_chunk_index: chunk_index)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
next if projected_hunks.empty?
|
|
49
|
+
|
|
50
|
+
metadata_lines = projected_hunks.select { |hunk| hunk.kind == :file_operation }.flat_map(&:patch_lines)
|
|
51
|
+
metadata_lines = implicit_metadata_lines(file_diff, metadata_lines) if metadata_lines.empty?
|
|
52
|
+
before_paths = current_paths(file_diff, path_operation_applied: path_operation_applied_before)
|
|
53
|
+
after_paths = current_paths(file_diff, path_operation_applied: path_operation_applied_after)
|
|
54
|
+
binary_only = projected_hunks.all? { |hunk| hunk.kind == :binary }
|
|
55
|
+
|
|
56
|
+
projected_files << ProjectedFile.new(
|
|
57
|
+
old_path: before_paths[:old_path],
|
|
58
|
+
new_path: after_paths[:new_path],
|
|
59
|
+
diff_git_line: build_diff_git_line(before_paths[:old_path], after_paths[:new_path]),
|
|
60
|
+
metadata_lines: metadata_lines,
|
|
61
|
+
hunks: projected_hunks.reject { |hunk| hunk.kind == :file_operation },
|
|
62
|
+
emit_text_headers: !binary_only || emits_text_headers_for_binary?(file_diff)
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
projected_files
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def project_operation_hunk(hunk, chunk_index)
|
|
72
|
+
return nil unless rows_assigned_to_chunk?(hunk, chunk_index)
|
|
73
|
+
|
|
74
|
+
ProjectedHunk.new(
|
|
75
|
+
old_start: 0,
|
|
76
|
+
old_count: 0,
|
|
77
|
+
new_start: 0,
|
|
78
|
+
new_count: 0,
|
|
79
|
+
lines: [],
|
|
80
|
+
kind: :file_operation,
|
|
81
|
+
patch_lines: hunk.patch_lines
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def project_binary_hunk(hunk, chunk_index)
|
|
86
|
+
return nil unless rows_assigned_to_chunk?(hunk, chunk_index)
|
|
87
|
+
|
|
88
|
+
ProjectedHunk.new(
|
|
89
|
+
old_start: 0,
|
|
90
|
+
old_count: 0,
|
|
91
|
+
new_start: 0,
|
|
92
|
+
new_count: 0,
|
|
93
|
+
lines: [],
|
|
94
|
+
kind: :binary,
|
|
95
|
+
patch_lines: hunk.patch_lines
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def project_text_hunk(hunk, chunk_index, before_offset, after_offset)
|
|
100
|
+
lines = []
|
|
101
|
+
changed = false
|
|
102
|
+
old_count = 0
|
|
103
|
+
new_count = 0
|
|
104
|
+
|
|
105
|
+
hunk.rows.each do |row|
|
|
106
|
+
visible_before = visible_in_before?(row, chunk_index)
|
|
107
|
+
visible_after = visible_in_after?(row, chunk_index)
|
|
108
|
+
changed ||= visible_before != visible_after
|
|
109
|
+
|
|
110
|
+
old_count += 1 if visible_before
|
|
111
|
+
new_count += 1 if visible_after
|
|
112
|
+
next unless visible_before || visible_after
|
|
113
|
+
|
|
114
|
+
prefix = if visible_before && visible_after
|
|
115
|
+
' '
|
|
116
|
+
elsif visible_before
|
|
117
|
+
'-'
|
|
118
|
+
else
|
|
119
|
+
'+'
|
|
120
|
+
end
|
|
121
|
+
lines << "#{prefix}#{row.text}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
[
|
|
125
|
+
changed,
|
|
126
|
+
ProjectedHunk.new(
|
|
127
|
+
old_start: hunk.old_start + before_offset,
|
|
128
|
+
old_count: old_count,
|
|
129
|
+
new_start: hunk.new_start + after_offset,
|
|
130
|
+
new_count: new_count,
|
|
131
|
+
lines: lines,
|
|
132
|
+
kind: :text,
|
|
133
|
+
patch_lines: []
|
|
134
|
+
)
|
|
135
|
+
]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def rows_assigned_to_chunk?(hunk, chunk_index)
|
|
139
|
+
hunk.change_rows.all? { |row| @chunk_index_by_row_id.fetch(row.id) == chunk_index }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def applied_delta(hunk, max_chunk_index:)
|
|
143
|
+
return 0 if max_chunk_index.negative?
|
|
144
|
+
|
|
145
|
+
delta = 0
|
|
146
|
+
hunk.change_rows.each do |row|
|
|
147
|
+
chunk_index = @chunk_index_by_row_id.fetch(row.id)
|
|
148
|
+
next if chunk_index > max_chunk_index
|
|
149
|
+
|
|
150
|
+
delta += 1 if row.kind == :addition
|
|
151
|
+
delta -= 1 if row.kind == :deletion
|
|
152
|
+
end
|
|
153
|
+
delta
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def visible_in_before?(row, chunk_index)
|
|
157
|
+
case row.kind
|
|
158
|
+
when :context
|
|
159
|
+
true
|
|
160
|
+
when :deletion
|
|
161
|
+
@chunk_index_by_row_id.fetch(row.id) >= chunk_index
|
|
162
|
+
when :addition
|
|
163
|
+
@chunk_index_by_row_id.fetch(row.id) < chunk_index
|
|
164
|
+
else
|
|
165
|
+
false
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def visible_in_after?(row, chunk_index)
|
|
170
|
+
case row.kind
|
|
171
|
+
when :context
|
|
172
|
+
true
|
|
173
|
+
when :deletion
|
|
174
|
+
@chunk_index_by_row_id.fetch(row.id) > chunk_index
|
|
175
|
+
when :addition
|
|
176
|
+
@chunk_index_by_row_id.fetch(row.id) <= chunk_index
|
|
177
|
+
else
|
|
178
|
+
false
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def current_paths(file_diff, path_operation_applied:)
|
|
183
|
+
operation_hunk = file_diff.path_operation_hunk
|
|
184
|
+
return { old_path: file_diff.old_path, new_path: file_diff.new_path } unless operation_hunk
|
|
185
|
+
|
|
186
|
+
if path_operation_applied
|
|
187
|
+
{ old_path: file_diff.new_path, new_path: file_diff.new_path }
|
|
188
|
+
else
|
|
189
|
+
{ old_path: file_diff.old_path, new_path: file_diff.old_path }
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def build_diff_git_line(old_path, new_path)
|
|
194
|
+
old_git_path = if old_path == '/dev/null'
|
|
195
|
+
to_git_old_path(new_path)
|
|
196
|
+
elsif new_path == '/dev/null'
|
|
197
|
+
to_git_old_path(old_path)
|
|
198
|
+
else
|
|
199
|
+
to_git_old_path(old_path)
|
|
200
|
+
end
|
|
201
|
+
new_git_path = if new_path == '/dev/null'
|
|
202
|
+
to_git_new_path(old_path)
|
|
203
|
+
elsif old_path == '/dev/null'
|
|
204
|
+
to_git_new_path(new_path)
|
|
205
|
+
else
|
|
206
|
+
to_git_new_path(new_path)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
"diff --git #{old_git_path} #{new_git_path}"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def path_operation_applied_before?(file_diff, chunk_index)
|
|
213
|
+
return false if chunk_index.zero?
|
|
214
|
+
|
|
215
|
+
path_operation_applied_after?(file_diff, chunk_index - 1)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def path_operation_applied_after?(file_diff, chunk_index)
|
|
219
|
+
operation_hunk = file_diff.path_operation_hunk
|
|
220
|
+
return false unless operation_hunk
|
|
221
|
+
|
|
222
|
+
operation_hunk.change_rows.all? do |row|
|
|
223
|
+
@chunk_index_by_row_id.fetch(row.id) <= chunk_index
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def emits_text_headers_for_binary?(file_diff)
|
|
228
|
+
file_diff.modification? || file_diff.path_operation_hunk
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def implicit_metadata_lines(file_diff, metadata_lines)
|
|
232
|
+
return metadata_lines unless file_diff.addition? || file_diff.deletion?
|
|
233
|
+
|
|
234
|
+
file_diff.metadata_lines.select do |line|
|
|
235
|
+
line.start_with?('new file mode ') || line.start_with?('deleted file mode ')
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def to_git_old_path(path)
|
|
240
|
+
return '/dev/null' if path == '/dev/null'
|
|
241
|
+
|
|
242
|
+
"a/#{path.sub(%r{\A[ab]/}, '')}"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def to_git_new_path(path)
|
|
246
|
+
return '/dev/null' if path == '/dev/null'
|
|
247
|
+
|
|
248
|
+
"b/#{path.sub(%r{\A[ab]/}, '')}"
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PatchUtil
|
|
4
|
+
module Split
|
|
5
|
+
class Verifier
|
|
6
|
+
def initialize(selector_parser: PatchUtil::Selection::Parser.new)
|
|
7
|
+
@selector_parser = selector_parser
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def build_chunks(diff:, chunk_requests:)
|
|
11
|
+
chunks = []
|
|
12
|
+
assigned_row_ids = {}
|
|
13
|
+
leftovers_request = nil
|
|
14
|
+
|
|
15
|
+
chunk_requests.each do |request|
|
|
16
|
+
if request.leftovers?
|
|
17
|
+
raise ValidationError, 'leftovers chunk can only be declared once' if leftovers_request
|
|
18
|
+
|
|
19
|
+
leftovers_request = request
|
|
20
|
+
next
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
selectors = @selector_parser.parse(request.selector_text)
|
|
24
|
+
row_ids, change_labels = resolve_selectors(diff, selectors)
|
|
25
|
+
|
|
26
|
+
row_ids.each do |row_id|
|
|
27
|
+
existing = assigned_row_ids[row_id]
|
|
28
|
+
next unless existing
|
|
29
|
+
|
|
30
|
+
label = diff.row_by_id(row_id)&.change_label || row_id
|
|
31
|
+
raise ValidationError, "#{label} is assigned to both #{existing} and #{request.name}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
row_ids.each { |row_id| assigned_row_ids[row_id] = request.name }
|
|
35
|
+
|
|
36
|
+
chunks << Chunk.new(
|
|
37
|
+
name: request.name,
|
|
38
|
+
selector_text: request.selector_text,
|
|
39
|
+
row_ids: row_ids,
|
|
40
|
+
change_labels: change_labels,
|
|
41
|
+
leftovers: false
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
leftovers = []
|
|
46
|
+
diff.change_rows.each do |row|
|
|
47
|
+
leftovers << row unless assigned_row_ids.key?(row.id)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if leftovers.any?
|
|
51
|
+
unless leftovers_request
|
|
52
|
+
raise ValidationError,
|
|
53
|
+
"#{leftovers.length} lines will be removed; re-plan with a leftovers commit if you do not intend removal"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
row_ids = leftovers.map(&:id)
|
|
57
|
+
change_labels = leftovers.map(&:change_label)
|
|
58
|
+
chunks << Chunk.new(
|
|
59
|
+
name: leftovers_request.name,
|
|
60
|
+
selector_text: nil,
|
|
61
|
+
row_ids: row_ids,
|
|
62
|
+
change_labels: change_labels,
|
|
63
|
+
leftovers: true
|
|
64
|
+
)
|
|
65
|
+
elsif leftovers_request
|
|
66
|
+
chunks << Chunk.new(
|
|
67
|
+
name: leftovers_request.name,
|
|
68
|
+
selector_text: nil,
|
|
69
|
+
row_ids: [],
|
|
70
|
+
change_labels: [],
|
|
71
|
+
leftovers: true
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
chunks
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def resolve_selectors(diff, selectors)
|
|
81
|
+
whole_hunks = {}
|
|
82
|
+
partial_hunks = {}
|
|
83
|
+
|
|
84
|
+
selectors.each do |selector|
|
|
85
|
+
if selector.whole_hunk?
|
|
86
|
+
whole_hunks[selector.hunk_label] = true
|
|
87
|
+
else
|
|
88
|
+
partial_hunks[selector.hunk_label] = true
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
whole_hunks.each_key do |hunk_label|
|
|
93
|
+
next unless partial_hunks[hunk_label]
|
|
94
|
+
|
|
95
|
+
raise ValidationError, "cannot select both whole hunk #{hunk_label} and partial changed lines from it"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
selected_row_ids = []
|
|
99
|
+
selected_change_labels = []
|
|
100
|
+
seen_row_ids = {}
|
|
101
|
+
|
|
102
|
+
selectors.each do |selector|
|
|
103
|
+
rows = rows_for_selector(diff, selector)
|
|
104
|
+
rows.each do |row|
|
|
105
|
+
raise ValidationError, "#{row.change_label} is selected more than once" if seen_row_ids[row.id]
|
|
106
|
+
|
|
107
|
+
seen_row_ids[row.id] = true
|
|
108
|
+
selected_row_ids << row.id
|
|
109
|
+
selected_change_labels << row.change_label
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
[selected_row_ids, selected_change_labels]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def rows_for_selector(diff, selector)
|
|
117
|
+
hunk = diff.hunk_by_label(selector.hunk_label)
|
|
118
|
+
raise ValidationError, "unknown hunk label: #{selector.hunk_label}" unless hunk
|
|
119
|
+
|
|
120
|
+
return hunk.change_rows if selector.whole_hunk?
|
|
121
|
+
|
|
122
|
+
rows = []
|
|
123
|
+
selector.ordinals.each do |ordinal|
|
|
124
|
+
row = hunk.change_rows.find { |candidate| candidate.change_ordinal == ordinal }
|
|
125
|
+
raise ValidationError, "unknown changed line #{selector.hunk_label}#{ordinal}" unless row
|
|
126
|
+
|
|
127
|
+
rows << row
|
|
128
|
+
end
|
|
129
|
+
rows
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PatchUtil
|
|
4
|
+
module Split
|
|
5
|
+
autoload :Plan, 'patch_util/split/plan'
|
|
6
|
+
autoload :ChunkRequest, 'patch_util/split/plan'
|
|
7
|
+
autoload :Chunk, 'patch_util/split/plan'
|
|
8
|
+
autoload :PlanEntry, 'patch_util/split/plan'
|
|
9
|
+
autoload :PlanSet, 'patch_util/split/plan'
|
|
10
|
+
autoload :PlanStore, 'patch_util/split/plan_store'
|
|
11
|
+
autoload :Verifier, 'patch_util/split/verifier'
|
|
12
|
+
autoload :Planner, 'patch_util/split/planner'
|
|
13
|
+
autoload :ProjectedFile, 'patch_util/split/projector'
|
|
14
|
+
autoload :ProjectedHunk, 'patch_util/split/projector'
|
|
15
|
+
autoload :Projector, 'patch_util/split/projector'
|
|
16
|
+
autoload :Emitter, 'patch_util/split/emitter'
|
|
17
|
+
autoload :Applier, 'patch_util/split/applier'
|
|
18
|
+
autoload :Inspector, 'patch_util/split/inspector'
|
|
19
|
+
autoload :CLI, 'patch_util/split/cli'
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/patch_util.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PatchUtil
|
|
4
|
+
autoload :VERSION, 'patch_util/version'
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
class ParseError < Error; end
|
|
7
|
+
class ValidationError < Error; end
|
|
8
|
+
class UnsupportedFeatureError < Error; end
|
|
9
|
+
|
|
10
|
+
autoload :Git, 'patch_util/git'
|
|
11
|
+
autoload :Source, 'patch_util/source'
|
|
12
|
+
autoload :Diff, 'patch_util/diff'
|
|
13
|
+
autoload :FileDiff, 'patch_util/diff'
|
|
14
|
+
autoload :Hunk, 'patch_util/diff'
|
|
15
|
+
autoload :Row, 'patch_util/diff'
|
|
16
|
+
autoload :ChangeLine, 'patch_util/diff'
|
|
17
|
+
autoload :Parser, 'patch_util/parser'
|
|
18
|
+
autoload :Selection, 'patch_util/selection'
|
|
19
|
+
autoload :Split, 'patch_util/split'
|
|
20
|
+
autoload :CLI, 'patch_util/cli'
|
|
21
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: patch_util
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- hmdne
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: thor
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.2'
|
|
26
|
+
description: Patch planning and materialization helpers for splitting one unified
|
|
27
|
+
diff into multiple reviewable patches.
|
|
28
|
+
email:
|
|
29
|
+
- 54514036+hmdne@users.noreply.github.com
|
|
30
|
+
executables:
|
|
31
|
+
- patch_util
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- ".rspec"
|
|
36
|
+
- CHANGELOG.md
|
|
37
|
+
- LICENSE.txt
|
|
38
|
+
- README.md
|
|
39
|
+
- Rakefile
|
|
40
|
+
- SKILL.md
|
|
41
|
+
- exe/patch_util
|
|
42
|
+
- lib/patch_util.rb
|
|
43
|
+
- lib/patch_util/cli.rb
|
|
44
|
+
- lib/patch_util/diff.rb
|
|
45
|
+
- lib/patch_util/git.rb
|
|
46
|
+
- lib/patch_util/git/cli.rb
|
|
47
|
+
- lib/patch_util/git/rewrite_cli.rb
|
|
48
|
+
- lib/patch_util/git/rewrite_session_manager.rb
|
|
49
|
+
- lib/patch_util/git/rewrite_state_store.rb
|
|
50
|
+
- lib/patch_util/git/rewriter.rb
|
|
51
|
+
- lib/patch_util/parser.rb
|
|
52
|
+
- lib/patch_util/selection.rb
|
|
53
|
+
- lib/patch_util/source.rb
|
|
54
|
+
- lib/patch_util/split.rb
|
|
55
|
+
- lib/patch_util/split/applier.rb
|
|
56
|
+
- lib/patch_util/split/cli.rb
|
|
57
|
+
- lib/patch_util/split/emitter.rb
|
|
58
|
+
- lib/patch_util/split/inspector.rb
|
|
59
|
+
- lib/patch_util/split/plan.rb
|
|
60
|
+
- lib/patch_util/split/plan_store.rb
|
|
61
|
+
- lib/patch_util/split/planner.rb
|
|
62
|
+
- lib/patch_util/split/projector.rb
|
|
63
|
+
- lib/patch_util/split/verifier.rb
|
|
64
|
+
- lib/patch_util/version.rb
|
|
65
|
+
homepage: https://github.com/rbutils/patch_util
|
|
66
|
+
licenses:
|
|
67
|
+
- MIT
|
|
68
|
+
metadata:
|
|
69
|
+
allowed_push_host: https://rubygems.org
|
|
70
|
+
source_code_uri: https://github.com/rbutils/patch_util
|
|
71
|
+
changelog_uri: https://github.com/rbutils/patch_util/blob/master/CHANGELOG.md
|
|
72
|
+
documentation_uri: https://github.com/rbutils/patch_util#readme
|
|
73
|
+
bug_tracker_uri: https://github.com/rbutils/patch_util/issues
|
|
74
|
+
rubygems_mfa_required: 'true'
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 3.2.0
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 4.0.6
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: Split unified diffs into smaller ordered patches
|
|
92
|
+
test_files: []
|