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.
@@ -0,0 +1,393 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module PatchUtil
6
+ module Git
7
+ class RewriteCLI < Thor
8
+ desc 'abort', 'Remove retained failed rewrite worktree and clear rewrite state'
9
+ option :repo, type: :string, aliases: '-r', banner: 'PATH'
10
+ def abort
11
+ repo_path = options[:repo] || Dir.pwd
12
+ git_cli = PatchUtil::Git::Cli.new
13
+ raise ValidationError, 'rewrite abort requires a git repository' unless git_cli.inside_repo?(repo_path)
14
+
15
+ result = PatchUtil::Git::RewriteSessionManager.new.abort_rewrite(repo_path: repo_path)
16
+ if result.worktree_removed
17
+ puts "removed retained worktree #{result.worktree_path}"
18
+ else
19
+ puts "retained worktree already absent #{result.worktree_path}"
20
+ end
21
+ puts "backup ref remains at #{result.backup_ref}"
22
+ end
23
+
24
+ desc 'continue', 'Resume a retained failed rewrite worktree'
25
+ option :repo, type: :string, aliases: '-r', banner: 'PATH'
26
+ def continue
27
+ repo_path = options[:repo] || Dir.pwd
28
+ git_cli = PatchUtil::Git::Cli.new
29
+ raise ValidationError, 'rewrite continue requires a git repository' unless git_cli.inside_repo?(repo_path)
30
+
31
+ result = PatchUtil::Git::RewriteSessionManager.new.continue_rewrite(repo_path: repo_path)
32
+ puts "rewrote #{result.branch}: #{result.old_head} -> #{result.new_head}"
33
+ puts "backup ref: #{result.backup_ref}"
34
+ puts 'resumed retained rewrite state'
35
+ end
36
+
37
+ desc 'restore', 'Restore the current branch from the recorded rewrite backup ref'
38
+ option :repo, type: :string, aliases: '-r', banner: 'PATH'
39
+ def restore
40
+ repo_path = options[:repo] || Dir.pwd
41
+ git_cli = PatchUtil::Git::Cli.new
42
+ raise ValidationError, 'rewrite restore requires a git repository' unless git_cli.inside_repo?(repo_path)
43
+
44
+ result = PatchUtil::Git::RewriteSessionManager.new.restore_rewrite(repo_path: repo_path)
45
+ puts "restored #{result.branch}: #{result.old_head} -> #{result.restored_head}"
46
+ if result.worktree_removed
47
+ puts "removed retained worktree #{result.worktree_path}"
48
+ else
49
+ puts "retained worktree already absent #{result.worktree_path}"
50
+ end
51
+ puts "backup ref remains at #{result.backup_ref}"
52
+ end
53
+
54
+ desc 'status', 'Show retained rewrite state for the current branch'
55
+ option :repo, type: :string, aliases: '-r', banner: 'PATH'
56
+ def status
57
+ repo_path = options[:repo] || Dir.pwd
58
+ git_cli = PatchUtil::Git::Cli.new
59
+ raise ValidationError, 'rewrite status requires a git repository' unless git_cli.inside_repo?(repo_path)
60
+
61
+ branch = git_cli.current_branch(repo_path)
62
+ raise ValidationError, 'rewrite status requires a checked out branch' if branch.empty?
63
+
64
+ manager = PatchUtil::Git::RewriteSessionManager.new
65
+ status = manager.status(repo_path: repo_path)
66
+ unless status
67
+ puts "no retained rewrite state for branch #{branch}"
68
+ return
69
+ end
70
+
71
+ puts "branch: #{status.branch}"
72
+ puts "target commit: #{status.state.target_sha}"
73
+ puts "recorded head: #{status.state.head_sha}"
74
+ puts "current head: #{status.head_sha}"
75
+ puts "backup ref: #{status.state.backup_ref}"
76
+ puts "retained worktree: #{status.state.worktree_path}"
77
+ puts "last error: #{status.state.message}"
78
+ puts "worktree exists: #{status.worktree_exists}"
79
+ puts "worktree clean: #{status.worktree_clean.nil? ? 'unknown' : status.worktree_clean}"
80
+ puts "unresolved paths: #{status.unresolved_paths.length}"
81
+ puts "unresolved path list: #{status.unresolved_paths.join(', ')}" if status.unresolved_paths.any?
82
+ puts "conflict marker files: #{status.conflict_marker_details.length}"
83
+ if status.conflict_marker_details.any?
84
+ puts "conflict marker file list: #{status.conflict_marker_details.map(&:path).join(', ')}"
85
+ end
86
+ puts "pending revisions: #{status.state.pending_revisions.length}"
87
+ if status.state.pending_revisions.any?
88
+ puts "pending revision list: #{status.state.pending_revisions.join(', ')}"
89
+ end
90
+ puts "current revision: #{status.current_revision}" if status.current_revision
91
+ puts "branch head matches recorded state: #{status.head_matches}"
92
+ puts "next action: #{manager.next_action(status)}"
93
+ end
94
+
95
+ desc 'conflicts', 'Show retained conflict-marker excerpts for the current branch'
96
+ option :repo, type: :string, aliases: '-r', banner: 'PATH'
97
+ def conflicts
98
+ repo_path = options[:repo] || Dir.pwd
99
+ git_cli = PatchUtil::Git::Cli.new
100
+ raise ValidationError, 'rewrite conflicts requires a git repository' unless git_cli.inside_repo?(repo_path)
101
+
102
+ branch = git_cli.current_branch(repo_path)
103
+ raise ValidationError, 'rewrite conflicts requires a checked out branch' if branch.empty?
104
+
105
+ status = PatchUtil::Git::RewriteSessionManager.new.status(repo_path: repo_path)
106
+ unless status
107
+ puts "no retained rewrite state for branch #{branch}"
108
+ return
109
+ end
110
+
111
+ if status.conflict_marker_details.empty?
112
+ puts 'no conflict markers found in the retained worktree'
113
+ return
114
+ end
115
+
116
+ conflict_blocks = git_cli.conflict_block_details(status.state.worktree_path,
117
+ file_paths: status.conflict_marker_details.map(&:path))
118
+
119
+ status.conflict_marker_details.each do |detail|
120
+ puts "path: #{detail.path}"
121
+ puts "marker count: #{detail.marker_count}"
122
+ puts "first marker line: #{detail.first_marker_line}"
123
+ blocks = conflict_blocks.select { |block| block.path == detail.path }
124
+ puts "block ids: #{blocks.map(&:block_id).join(', ')}" if blocks.any?
125
+ puts detail.excerpt
126
+ puts '--'
127
+ end
128
+ end
129
+
130
+ desc 'conflict-blocks', 'Show retained conflict blocks with separate side bodies'
131
+ option :repo, type: :string, aliases: '-r', banner: 'PATH'
132
+ option :path, type: :array, banner: 'PATH[,PATH...]'
133
+ def conflict_blocks
134
+ repo_path = options[:repo] || Dir.pwd
135
+ git_cli = PatchUtil::Git::Cli.new
136
+ unless git_cli.inside_repo?(repo_path)
137
+ raise ValidationError,
138
+ 'rewrite conflict-blocks requires a git repository'
139
+ end
140
+
141
+ branch = git_cli.current_branch(repo_path)
142
+ raise ValidationError, 'rewrite conflict-blocks requires a checked out branch' if branch.empty?
143
+
144
+ result = PatchUtil::Git::RewriteSessionManager.new.conflict_blocks(repo_path: repo_path, paths: options[:path])
145
+ if result.blocks.empty?
146
+ puts 'no conflict blocks found in the retained worktree'
147
+ return
148
+ end
149
+
150
+ result.blocks.each do |block|
151
+ puts "path: #{block.path}"
152
+ puts "block id: #{block.block_id}"
153
+ puts "line range: #{block.start_line}-#{block.end_line}"
154
+ puts 'ours:'
155
+ puts format_multiline(block.ours)
156
+ unless block.ancestor.empty?
157
+ puts 'ancestor:'
158
+ puts format_multiline(block.ancestor)
159
+ end
160
+ puts 'theirs:'
161
+ puts format_multiline(block.theirs)
162
+ puts 'excerpt:'
163
+ puts format_multiline(block.excerpt)
164
+ puts '--'
165
+ end
166
+ end
167
+
168
+ desc 'export-block', 'Export one retained conflict block into an editable template file'
169
+ option :repo, type: :string, aliases: '-r', banner: 'PATH'
170
+ option :path, type: :string, required: true, banner: 'PATH'
171
+ option :block, type: :numeric, required: true, banner: 'N'
172
+ option :output, type: :string, required: true, aliases: '-o', banner: 'PATH'
173
+ def export_block
174
+ repo_path = options[:repo] || Dir.pwd
175
+ git_cli = PatchUtil::Git::Cli.new
176
+ raise ValidationError, 'rewrite export-block requires a git repository' unless git_cli.inside_repo?(repo_path)
177
+
178
+ result = PatchUtil::Git::RewriteSessionManager.new.export_conflict_block(
179
+ repo_path: repo_path,
180
+ path: options[:path],
181
+ block_id: options[:block],
182
+ output_path: options[:output]
183
+ )
184
+
185
+ puts "retained worktree: #{result.worktree_path}"
186
+ puts "exported block #{result.block_id} from #{result.path} to #{result.output_path}"
187
+ end
188
+
189
+ desc 'apply-block-edit', 'Apply edited block template content back into the retained worktree'
190
+ option :repo, type: :string, aliases: '-r', banner: 'PATH'
191
+ option :path, type: :string, required: true, banner: 'PATH'
192
+ option :block, type: :numeric, required: true, banner: 'N'
193
+ option :input, type: :string, required: true, aliases: '-i', banner: 'PATH'
194
+ def apply_block_edit
195
+ repo_path = options[:repo] || Dir.pwd
196
+ git_cli = PatchUtil::Git::Cli.new
197
+ unless git_cli.inside_repo?(repo_path)
198
+ raise ValidationError,
199
+ 'rewrite apply-block-edit requires a git repository'
200
+ end
201
+
202
+ result = PatchUtil::Git::RewriteSessionManager.new.apply_conflict_block_edit(
203
+ repo_path: repo_path,
204
+ path: options[:path],
205
+ block_id: options[:block],
206
+ input_path: options[:input]
207
+ )
208
+
209
+ puts "retained worktree: #{result.worktree_path}"
210
+ puts "applied edited block #{result.block_id} from #{result.input_path} into #{result.path}"
211
+ puts "remaining blocks in file: #{result.remaining_blocks.length}"
212
+ if result.remaining_blocks.any?
213
+ puts "remaining block ids: #{result.remaining_blocks.map(&:block_id).join(', ')}"
214
+ puts 'file still has conflict markers; it is not staged yet'
215
+ else
216
+ puts 'file has no remaining conflict markers and is staged'
217
+ end
218
+ end
219
+
220
+ desc 'export-session', 'Export multiple retained conflict blocks into one editable session file'
221
+ option :repo, type: :string, aliases: '-r', banner: 'PATH'
222
+ option :path, type: :array, banner: 'PATH[,PATH...]'
223
+ option :output, type: :string, required: true, aliases: '-o', banner: 'PATH'
224
+ def export_session
225
+ repo_path = options[:repo] || Dir.pwd
226
+ git_cli = PatchUtil::Git::Cli.new
227
+ raise ValidationError, 'rewrite export-session requires a git repository' unless git_cli.inside_repo?(repo_path)
228
+
229
+ result = PatchUtil::Git::RewriteSessionManager.new.export_conflict_block_session(
230
+ repo_path: repo_path,
231
+ paths: options[:path],
232
+ output_path: options[:output]
233
+ )
234
+
235
+ puts "retained worktree: #{result.worktree_path}"
236
+ puts "exported #{result.blocks.length} blocks to #{result.output_path}"
237
+ puts "files in session: #{result.files.length}"
238
+ print_conflict_block_file_summary(result.files)
239
+ end
240
+
241
+ desc 'apply-session-edit', 'Apply a multi-block edited session template back into the retained worktree'
242
+ option :repo, type: :string, aliases: '-r', banner: 'PATH'
243
+ option :input, type: :string, required: true, aliases: '-i', banner: 'PATH'
244
+ def apply_session_edit
245
+ repo_path = options[:repo] || Dir.pwd
246
+ git_cli = PatchUtil::Git::Cli.new
247
+ unless git_cli.inside_repo?(repo_path)
248
+ raise ValidationError,
249
+ 'rewrite apply-session-edit requires a git repository'
250
+ end
251
+
252
+ result = PatchUtil::Git::RewriteSessionManager.new.apply_conflict_block_session_edit(
253
+ repo_path: repo_path,
254
+ input_path: options[:input]
255
+ )
256
+
257
+ puts "retained worktree: #{result.worktree_path}"
258
+ puts "applied #{result.applied_blocks.length} edited blocks from #{result.input_path}"
259
+ puts "staged paths: #{result.staged_paths.length}"
260
+ puts "staged path list: #{result.staged_paths.join(', ')}" if result.staged_paths.any?
261
+
262
+ remaining_files = result.remaining_blocks_by_file.select { |_path, blocks| blocks.any? }
263
+ puts "files still containing conflict blocks: #{remaining_files.length}"
264
+ if remaining_files.any?
265
+ puts "remaining conflict files: #{remaining_files.keys.join(', ')}"
266
+ else
267
+ puts 'all edited files are free of conflict markers'
268
+ end
269
+
270
+ print_applied_session_file_summary(result.files)
271
+ end
272
+
273
+ desc 'session-summary', 'Show file-aware retained conflict block session summary'
274
+ option :repo, type: :string, aliases: '-r', banner: 'PATH'
275
+ option :path, type: :array, banner: 'PATH[,PATH...]'
276
+ def session_summary
277
+ repo_path = options[:repo] || Dir.pwd
278
+ git_cli = PatchUtil::Git::Cli.new
279
+ unless git_cli.inside_repo?(repo_path)
280
+ raise ValidationError,
281
+ 'rewrite session-summary requires a git repository'
282
+ end
283
+
284
+ result = PatchUtil::Git::RewriteSessionManager.new.conflict_block_session_summary(
285
+ repo_path: repo_path,
286
+ paths: options[:path]
287
+ )
288
+
289
+ puts "retained worktree: #{result.worktree_path}"
290
+ puts "files with conflict blocks: #{result.files.length}"
291
+ puts "total conflict blocks: #{result.blocks.length}"
292
+ if result.files.empty?
293
+ puts 'no conflict blocks found for session export'
294
+ return
295
+ end
296
+
297
+ print_conflict_block_file_summary(result.files)
298
+ end
299
+
300
+ desc 'resolve', 'Choose ours/theirs for retained unresolved paths and stage them'
301
+ option :repo, type: :string, aliases: '-r', banner: 'PATH'
302
+ option :side, type: :string, required: true, banner: 'ours|theirs'
303
+ option :path, type: :array, banner: 'PATH[,PATH...]'
304
+ option :all, type: :boolean, default: false, banner: 'BOOL'
305
+ def resolve
306
+ repo_path = options[:repo] || Dir.pwd
307
+ git_cli = PatchUtil::Git::Cli.new
308
+ raise ValidationError, 'rewrite resolve requires a git repository' unless git_cli.inside_repo?(repo_path)
309
+ raise ValidationError, 'use either --path or --all, not both' if options[:all] && options[:path]
310
+
311
+ result = PatchUtil::Git::RewriteSessionManager.new.resolve_conflicts(
312
+ repo_path: repo_path,
313
+ side: options[:side],
314
+ paths: options[:path],
315
+ all_unresolved: options[:all]
316
+ )
317
+
318
+ puts "retained worktree: #{result.worktree_path}"
319
+ puts "resolved with #{result.side}: #{result.resolved_paths.join(', ')}"
320
+ puts "remaining unresolved paths: #{result.remaining_unresolved_paths.length}"
321
+ if result.remaining_unresolved_paths.any?
322
+ puts "remaining unresolved path list: #{result.remaining_unresolved_paths.join(', ')}"
323
+ else
324
+ puts 'all retained unresolved paths are now staged'
325
+ end
326
+ end
327
+
328
+ desc 'resolve-block', 'Resolve one retained conflict block within a file'
329
+ option :repo, type: :string, aliases: '-r', banner: 'PATH'
330
+ option :path, type: :string, required: true, banner: 'PATH'
331
+ option :block, type: :numeric, required: true, banner: 'N'
332
+ option :side, type: :string, required: true, banner: 'ours|theirs|ancestor'
333
+ def resolve_block
334
+ repo_path = options[:repo] || Dir.pwd
335
+ git_cli = PatchUtil::Git::Cli.new
336
+ raise ValidationError, 'rewrite resolve-block requires a git repository' unless git_cli.inside_repo?(repo_path)
337
+
338
+ result = PatchUtil::Git::RewriteSessionManager.new.resolve_conflict_block(
339
+ repo_path: repo_path,
340
+ path: options[:path],
341
+ block_id: options[:block],
342
+ side: options[:side]
343
+ )
344
+
345
+ puts "retained worktree: #{result.worktree_path}"
346
+ puts "resolved block #{result.block_id} in #{result.path} with #{result.side}"
347
+ puts "remaining blocks in file: #{result.remaining_blocks.length}"
348
+ if result.remaining_blocks.any?
349
+ puts "remaining block ids: #{result.remaining_blocks.map(&:block_id).join(', ')}"
350
+ puts 'file still has conflict markers; it is not staged yet'
351
+ else
352
+ puts 'file has no remaining conflict markers and is staged'
353
+ end
354
+ end
355
+
356
+ no_commands do
357
+ def format_multiline(text)
358
+ return ' <empty>' if text.empty?
359
+
360
+ text.lines(chomp: true).map { |line| " #{line}" }.join("\n")
361
+ end
362
+
363
+ def print_conflict_block_file_summary(files)
364
+ files.each do |path, info|
365
+ puts "path: #{path}"
366
+ puts "block count: #{info[:block_count]}"
367
+ puts "block ids: #{format_id_list(info[:block_ids])}"
368
+ puts "has ancestor blocks: #{info[:has_ancestor]}"
369
+ puts '--'
370
+ end
371
+ end
372
+
373
+ def print_applied_session_file_summary(files)
374
+ files.each do |path, info|
375
+ puts "path: #{path}"
376
+ puts "applied block count: #{info[:applied_block_count]}"
377
+ puts "applied block ids: #{format_id_list(info[:applied_block_ids])}"
378
+ puts "remaining block count: #{info[:remaining_block_count]}"
379
+ puts "remaining block ids: #{format_id_list(info[:remaining_block_ids])}"
380
+ puts "staged: #{info[:staged]}"
381
+ puts '--'
382
+ end
383
+ end
384
+
385
+ def format_id_list(ids)
386
+ return '<none>' if ids.empty?
387
+
388
+ ids.join(', ')
389
+ end
390
+ end
391
+ end
392
+ end
393
+ end