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,480 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module PatchUtil
6
+ module Git
7
+ class RewriteSessionManager
8
+ Result = Data.define(:branch, :old_head, :new_head, :backup_ref, :commits)
9
+ AbortResult = Data.define(:branch, :backup_ref, :worktree_path, :worktree_removed)
10
+ RestoreResult = Data.define(:branch, :old_head, :restored_head, :backup_ref, :worktree_path,
11
+ :worktree_removed)
12
+ ResolveResult = Data.define(:branch, :worktree_path, :side, :resolved_paths, :remaining_unresolved_paths)
13
+ ResolveBlockResult = Data.define(:branch, :worktree_path, :path, :block_id, :side, :remaining_blocks,
14
+ :staged)
15
+ ConflictBlocksResult = Data.define(:branch, :worktree_path, :blocks)
16
+ ExportBlockResult = Data.define(:branch, :worktree_path, :path, :block_id, :output_path)
17
+ ApplyBlockEditResult = Data.define(:branch, :worktree_path, :path, :block_id, :input_path, :remaining_blocks,
18
+ :staged)
19
+ ExportBlockSessionResult = Data.define(:branch, :worktree_path, :output_path, :blocks, :files)
20
+ ApplyBlockSessionEditResult = Data.define(:branch, :worktree_path, :input_path, :applied_blocks,
21
+ :remaining_blocks_by_file, :staged_paths, :files)
22
+ SessionSummaryResult = Data.define(:branch, :worktree_path, :blocks, :files)
23
+ Status = Data.define(:branch, :head_sha, :state, :head_matches, :worktree_exists, :worktree_clean,
24
+ :cherry_pick_in_progress, :current_revision, :unresolved_paths,
25
+ :conflict_marker_details)
26
+
27
+ def initialize(git_cli: Cli.new, clock: -> { Time.now.utc })
28
+ @git_cli = git_cli
29
+ @clock = clock
30
+ end
31
+
32
+ def abort_rewrite(repo_path:)
33
+ branch = @git_cli.current_branch(repo_path)
34
+ raise PatchUtil::ValidationError, 'rewrite abort requires a checked out branch' if branch.empty?
35
+
36
+ state_store = RewriteStateStore.new(git_dir: @git_cli.git_dir(repo_path))
37
+ state = state_store.find_branch(branch)
38
+ raise PatchUtil::ValidationError, "no retained rewrite state for branch #{branch}" unless state
39
+
40
+ worktree_removed = remove_retained_worktree(repo_path, state.worktree_path)
41
+ state_store.clear_branch(branch)
42
+
43
+ AbortResult.new(
44
+ branch: branch,
45
+ backup_ref: state.backup_ref,
46
+ worktree_path: state.worktree_path,
47
+ worktree_removed: worktree_removed
48
+ )
49
+ end
50
+
51
+ def continue_rewrite(repo_path:)
52
+ branch, head, state_store, state = resume_state_for(repo_path)
53
+ worktree = state.worktree_path
54
+ pending_revisions = state.pending_revisions.dup
55
+
56
+ begin
57
+ if @git_cli.cherry_pick_in_progress?(worktree)
58
+ current_revision = @git_cli.cherry_pick_head(worktree)
59
+ pending_revisions = normalize_pending_revisions(repo_path, state, current_revision, pending_revisions)
60
+ @git_cli.cherry_pick_continue(worktree)
61
+ pending_revisions.shift if pending_revisions.first == current_revision
62
+ elsif !@git_cli.worktree_clean?(worktree)
63
+ raise PatchUtil::ValidationError,
64
+ 'retained rewrite worktree has unresolved changes; finish the in-progress cherry-pick or clean it before continuing'
65
+ end
66
+
67
+ until pending_revisions.empty?
68
+ revision = pending_revisions.first
69
+ @git_cli.cherry_pick(worktree, revision)
70
+ pending_revisions.shift
71
+ end
72
+
73
+ finalize_rewrite(repo_path, branch, head, state.backup_ref, worktree, state_store)
74
+ rescue PatchUtil::Error, PatchUtil::ValidationError => e
75
+ state_store.record_failure(
76
+ RewriteStateStore::State.new(
77
+ branch: state.branch,
78
+ target_sha: state.target_sha,
79
+ head_sha: state.head_sha,
80
+ backup_ref: state.backup_ref,
81
+ worktree_path: state.worktree_path,
82
+ status: 'failed',
83
+ message: e.message,
84
+ created_at: @clock.call.iso8601,
85
+ pending_revisions: pending_revisions
86
+ )
87
+ )
88
+ raise PatchUtil::Error,
89
+ continue_failure_message(state, worktree, e.message)
90
+ end
91
+ end
92
+
93
+ def status(repo_path:)
94
+ branch = @git_cli.current_branch(repo_path)
95
+ raise PatchUtil::ValidationError, 'rewrite-status requires a checked out branch' if branch.empty?
96
+
97
+ state_store = RewriteStateStore.new(git_dir: @git_cli.git_dir(repo_path))
98
+ state = state_store.find_branch(branch)
99
+ return nil unless state
100
+
101
+ head_sha = @git_cli.head_sha(repo_path)
102
+ worktree_exists = File.directory?(state.worktree_path)
103
+ cherry_pick_in_progress = worktree_exists && @git_cli.cherry_pick_in_progress?(state.worktree_path)
104
+ current_revision = cherry_pick_in_progress ? @git_cli.cherry_pick_head(state.worktree_path) : nil
105
+ worktree_clean = worktree_exists ? @git_cli.worktree_clean?(state.worktree_path) : nil
106
+ unresolved_paths = worktree_exists ? @git_cli.unresolved_paths(state.worktree_path) : []
107
+ conflict_marker_details = if worktree_exists
108
+ @git_cli.conflict_marker_details(state.worktree_path,
109
+ file_paths: unresolved_paths)
110
+ else
111
+ []
112
+ end
113
+ if worktree_exists && conflict_marker_details.empty?
114
+ conflict_marker_details = @git_cli.conflict_marker_details(state.worktree_path)
115
+ end
116
+
117
+ Status.new(
118
+ branch: branch,
119
+ head_sha: head_sha,
120
+ state: state,
121
+ head_matches: head_sha == state.head_sha,
122
+ worktree_exists: worktree_exists,
123
+ worktree_clean: worktree_clean,
124
+ cherry_pick_in_progress: cherry_pick_in_progress,
125
+ current_revision: current_revision,
126
+ unresolved_paths: unresolved_paths,
127
+ conflict_marker_details: conflict_marker_details
128
+ )
129
+ end
130
+
131
+ def restore_rewrite(repo_path:)
132
+ unless @git_cli.worktree_clean?(repo_path)
133
+ raise PatchUtil::ValidationError,
134
+ 'restore-rewrite requires a clean worktree'
135
+ end
136
+
137
+ branch = @git_cli.current_branch(repo_path)
138
+ raise PatchUtil::ValidationError, 'restore-rewrite requires a checked out branch' if branch.empty?
139
+
140
+ state_store = RewriteStateStore.new(git_dir: @git_cli.git_dir(repo_path))
141
+ state = state_store.find_branch(branch)
142
+ raise PatchUtil::ValidationError, "no retained rewrite state for branch #{branch}" unless state
143
+
144
+ old_head = @git_cli.head_sha(repo_path)
145
+ restored_head = resolve_backup_ref(repo_path, state.backup_ref)
146
+
147
+ @git_cli.reset_hard(repo_path, restored_head)
148
+ worktree_removed = remove_retained_worktree(repo_path, state.worktree_path)
149
+ state_store.clear_branch(branch)
150
+
151
+ RestoreResult.new(
152
+ branch: branch,
153
+ old_head: old_head,
154
+ restored_head: restored_head,
155
+ backup_ref: state.backup_ref,
156
+ worktree_path: state.worktree_path,
157
+ worktree_removed: worktree_removed
158
+ )
159
+ end
160
+
161
+ def resolve_conflicts(repo_path:, side:, paths: nil, all_unresolved: false)
162
+ branch = @git_cli.current_branch(repo_path)
163
+ raise PatchUtil::ValidationError, 'rewrite-resolve requires a checked out branch' if branch.empty?
164
+
165
+ state_store = RewriteStateStore.new(git_dir: @git_cli.git_dir(repo_path))
166
+ state = state_store.find_branch(branch)
167
+ raise PatchUtil::ValidationError, "no retained rewrite state for branch #{branch}" unless state
168
+
169
+ unless File.directory?(state.worktree_path)
170
+ raise PatchUtil::ValidationError,
171
+ "retained rewrite worktree is missing: #{state.worktree_path}"
172
+ end
173
+
174
+ unresolved_paths = @git_cli.unresolved_paths(state.worktree_path)
175
+ if unresolved_paths.empty?
176
+ raise PatchUtil::ValidationError,
177
+ 'no unresolved paths found in the retained worktree'
178
+ end
179
+
180
+ resolved_paths = if all_unresolved
181
+ unresolved_paths
182
+ else
183
+ normalized_paths = Array(paths).map(&:to_s).map(&:strip).reject(&:empty?).uniq
184
+ if normalized_paths.empty?
185
+ raise PatchUtil::ValidationError,
186
+ 'provide --path PATH[,PATH...] or --all'
187
+ end
188
+
189
+ unknown_paths = normalized_paths - unresolved_paths
190
+ unless unknown_paths.empty?
191
+ raise PatchUtil::ValidationError,
192
+ "paths are not currently unresolved: #{unknown_paths.join(', ')}"
193
+ end
194
+
195
+ normalized_paths
196
+ end
197
+
198
+ @git_cli.checkout_conflict_side(state.worktree_path, side, resolved_paths)
199
+ @git_cli.add_paths(state.worktree_path, resolved_paths)
200
+ remaining_unresolved_paths = @git_cli.unresolved_paths(state.worktree_path)
201
+
202
+ ResolveResult.new(
203
+ branch: branch,
204
+ worktree_path: state.worktree_path,
205
+ side: side,
206
+ resolved_paths: resolved_paths,
207
+ remaining_unresolved_paths: remaining_unresolved_paths
208
+ )
209
+ end
210
+
211
+ def resolve_conflict_block(repo_path:, path:, block_id:, side:)
212
+ branch = @git_cli.current_branch(repo_path)
213
+ raise PatchUtil::ValidationError, 'rewrite-resolve-block requires a checked out branch' if branch.empty?
214
+
215
+ state_store = RewriteStateStore.new(git_dir: @git_cli.git_dir(repo_path))
216
+ state = state_store.find_branch(branch)
217
+ raise PatchUtil::ValidationError, "no retained rewrite state for branch #{branch}" unless state
218
+
219
+ unless File.directory?(state.worktree_path)
220
+ raise PatchUtil::ValidationError,
221
+ "retained rewrite worktree is missing: #{state.worktree_path}"
222
+ end
223
+
224
+ details = @git_cli.conflict_block_details(state.worktree_path, file_paths: [path])
225
+ raise PatchUtil::ValidationError, "no conflict blocks found for #{path}" if details.empty?
226
+
227
+ result = @git_cli.resolve_conflict_block(state.worktree_path, file_path: path, block_id: block_id, side: side)
228
+
229
+ ResolveBlockResult.new(
230
+ branch: branch,
231
+ worktree_path: state.worktree_path,
232
+ path: path,
233
+ block_id: block_id,
234
+ side: side,
235
+ remaining_blocks: result[:remaining_blocks],
236
+ staged: result[:staged]
237
+ )
238
+ end
239
+
240
+ def conflict_blocks(repo_path:, paths: nil)
241
+ branch = @git_cli.current_branch(repo_path)
242
+ raise PatchUtil::ValidationError, 'rewrite-conflict-blocks requires a checked out branch' if branch.empty?
243
+
244
+ state_store = RewriteStateStore.new(git_dir: @git_cli.git_dir(repo_path))
245
+ state = state_store.find_branch(branch)
246
+ raise PatchUtil::ValidationError, "no retained rewrite state for branch #{branch}" unless state
247
+
248
+ unless File.directory?(state.worktree_path)
249
+ raise PatchUtil::ValidationError,
250
+ "retained rewrite worktree is missing: #{state.worktree_path}"
251
+ end
252
+
253
+ selected_paths = Array(paths).map(&:to_s).map(&:strip).reject(&:empty?).uniq
254
+ selected_paths = nil if selected_paths.empty?
255
+ blocks = @git_cli.conflict_block_details(state.worktree_path, file_paths: selected_paths)
256
+
257
+ ConflictBlocksResult.new(
258
+ branch: branch,
259
+ worktree_path: state.worktree_path,
260
+ blocks: blocks
261
+ )
262
+ end
263
+
264
+ def export_conflict_block(repo_path:, path:, block_id:, output_path:)
265
+ branch, state = retained_state_for(repo_path, command_name: 'rewrite-export-block')
266
+ @git_cli.export_conflict_block_template(state.worktree_path, file_path: path, block_id: block_id,
267
+ output_path: output_path)
268
+
269
+ ExportBlockResult.new(
270
+ branch: branch,
271
+ worktree_path: state.worktree_path,
272
+ path: path,
273
+ block_id: block_id,
274
+ output_path: File.expand_path(output_path)
275
+ )
276
+ end
277
+
278
+ def apply_conflict_block_edit(repo_path:, path:, block_id:, input_path:)
279
+ branch, state = retained_state_for(repo_path, command_name: 'rewrite-apply-block-edit')
280
+ details = @git_cli.conflict_block_details(state.worktree_path, file_paths: [path])
281
+ raise PatchUtil::ValidationError, "no conflict blocks found for #{path}" if details.empty?
282
+
283
+ result = @git_cli.apply_conflict_block_edit(state.worktree_path, file_path: path, block_id: block_id,
284
+ input_path: input_path)
285
+
286
+ ApplyBlockEditResult.new(
287
+ branch: branch,
288
+ worktree_path: state.worktree_path,
289
+ path: path,
290
+ block_id: block_id,
291
+ input_path: File.expand_path(input_path),
292
+ remaining_blocks: result[:remaining_blocks],
293
+ staged: result[:staged]
294
+ )
295
+ end
296
+
297
+ def export_conflict_block_session(repo_path:, output_path:, paths: nil)
298
+ branch, state = retained_state_for(repo_path, command_name: 'rewrite-export-session')
299
+ selected_paths = Array(paths).map(&:to_s).map(&:strip).reject(&:empty?).uniq
300
+ selected_paths = nil if selected_paths.empty?
301
+ result = @git_cli.export_conflict_block_session_template(state.worktree_path, file_paths: selected_paths,
302
+ output_path: output_path)
303
+
304
+ ExportBlockSessionResult.new(
305
+ branch: branch,
306
+ worktree_path: state.worktree_path,
307
+ output_path: result[:output_path],
308
+ blocks: result[:blocks],
309
+ files: summarize_conflict_blocks_by_file(result[:blocks])
310
+ )
311
+ end
312
+
313
+ def apply_conflict_block_session_edit(repo_path:, input_path:)
314
+ branch, state = retained_state_for(repo_path, command_name: 'rewrite-apply-session-edit')
315
+ result = @git_cli.apply_conflict_block_session_edit(state.worktree_path, input_path: input_path)
316
+
317
+ ApplyBlockSessionEditResult.new(
318
+ branch: branch,
319
+ worktree_path: state.worktree_path,
320
+ input_path: File.expand_path(input_path),
321
+ applied_blocks: result[:applied_blocks],
322
+ remaining_blocks_by_file: result[:remaining_blocks_by_file],
323
+ staged_paths: result[:staged_paths],
324
+ files: summarize_applied_blocks_by_file(
325
+ applied_blocks: result[:applied_blocks],
326
+ remaining_blocks_by_file: result[:remaining_blocks_by_file],
327
+ staged_paths: result[:staged_paths]
328
+ )
329
+ )
330
+ end
331
+
332
+ def conflict_block_session_summary(repo_path:, paths: nil)
333
+ branch, state = retained_state_for(repo_path, command_name: 'rewrite-session-summary')
334
+ selected_paths = Array(paths).map(&:to_s).map(&:strip).reject(&:empty?).uniq
335
+ selected_paths = nil if selected_paths.empty?
336
+ blocks = @git_cli.conflict_block_details(state.worktree_path, file_paths: selected_paths)
337
+
338
+ SessionSummaryResult.new(
339
+ branch: branch,
340
+ worktree_path: state.worktree_path,
341
+ blocks: blocks,
342
+ files: summarize_conflict_blocks_by_file(blocks)
343
+ )
344
+ end
345
+
346
+ def next_action(status)
347
+ return 'run restore-rewrite or abort-rewrite; retained worktree is missing' unless status.worktree_exists
348
+ return 'run restore-rewrite before continuing; branch head changed' unless status.head_matches
349
+ return 'resolve conflicts in retained worktree, then run continue-rewrite' if status.cherry_pick_in_progress
350
+ if status.conflict_marker_details.any?
351
+ return 'inspect conflict markers with rewrite-conflicts, resolve them in the retained worktree, then run continue-rewrite'
352
+ end
353
+ if status.unresolved_paths.any?
354
+ return 'resolve unresolved paths in the retained worktree, then run continue-rewrite'
355
+ end
356
+ return 'clean the retained worktree or abort-rewrite before continuing' if status.worktree_clean == false
357
+ return 'run continue-rewrite to replay the remaining descendant commits' if status.state.pending_revisions.any?
358
+
359
+ 'run continue-rewrite to finalize the retained rewrite'
360
+ end
361
+
362
+ private
363
+
364
+ def retained_state_for(repo_path, command_name:)
365
+ branch = @git_cli.current_branch(repo_path)
366
+ raise PatchUtil::ValidationError, "#{command_name} requires a checked out branch" if branch.empty?
367
+
368
+ state_store = RewriteStateStore.new(git_dir: @git_cli.git_dir(repo_path))
369
+ state = state_store.find_branch(branch)
370
+ raise PatchUtil::ValidationError, "no retained rewrite state for branch #{branch}" unless state
371
+
372
+ unless File.directory?(state.worktree_path)
373
+ raise PatchUtil::ValidationError,
374
+ "retained rewrite worktree is missing: #{state.worktree_path}"
375
+ end
376
+
377
+ [branch, state]
378
+ end
379
+
380
+ def summarize_conflict_blocks_by_file(blocks)
381
+ blocks.group_by(&:path).transform_values do |file_blocks|
382
+ {
383
+ block_ids: file_blocks.map(&:block_id).sort,
384
+ block_count: file_blocks.length,
385
+ has_ancestor: file_blocks.any? { |block| !block.ancestor.empty? }
386
+ }
387
+ end.sort.to_h
388
+ end
389
+
390
+ def summarize_applied_blocks_by_file(applied_blocks:, remaining_blocks_by_file:, staged_paths:)
391
+ all_paths = (applied_blocks.map { |entry| entry[:path] } + remaining_blocks_by_file.keys).uniq.sort
392
+ summary = {}
393
+
394
+ all_paths.each do |path|
395
+ file_applied_blocks = applied_blocks.select { |entry| entry[:path] == path }
396
+ .sort_by { |entry| entry[:block_id] }
397
+ remaining_blocks = remaining_blocks_by_file.fetch(path, []).sort_by(&:block_id)
398
+
399
+ summary[path] = {
400
+ applied_block_ids: file_applied_blocks.map { |entry| entry[:block_id] },
401
+ applied_block_count: file_applied_blocks.length,
402
+ remaining_block_ids: remaining_blocks.map(&:block_id),
403
+ remaining_block_count: remaining_blocks.length,
404
+ staged: staged_paths.include?(path)
405
+ }
406
+ end
407
+
408
+ summary
409
+ end
410
+
411
+ def resolve_backup_ref(repo_path, backup_ref)
412
+ @git_cli.rev_parse(repo_path, backup_ref)
413
+ rescue PatchUtil::Error
414
+ raise PatchUtil::ValidationError, "backup ref #{backup_ref} no longer exists"
415
+ end
416
+
417
+ def remove_retained_worktree(repo_path, worktree_path)
418
+ return false unless File.directory?(worktree_path)
419
+
420
+ @git_cli.worktree_remove(repo_path, worktree_path)
421
+ true
422
+ end
423
+
424
+ def continue_failure_message(state, worktree, message)
425
+ lines = [message, "backup ref: #{state.backup_ref}", "retained worktree: #{state.worktree_path}"]
426
+ unresolved_paths = @git_cli.unresolved_paths(worktree)
427
+ if unresolved_paths.any?
428
+ lines << "unresolved paths: #{unresolved_paths.join(', ')}"
429
+ lines << 'next step: resolve the listed paths in the retained worktree, then run rewrite-status or continue-rewrite'
430
+ else
431
+ lines << 'next step: run rewrite-status to inspect the retained state before continuing or aborting'
432
+ end
433
+ lines.join("\n")
434
+ end
435
+
436
+ def resume_state_for(repo_path)
437
+ unless @git_cli.worktree_clean?(repo_path)
438
+ raise PatchUtil::ValidationError,
439
+ 'continue-rewrite requires a clean worktree'
440
+ end
441
+
442
+ branch = @git_cli.current_branch(repo_path)
443
+ raise PatchUtil::ValidationError, 'continue-rewrite requires a checked out branch' if branch.empty?
444
+
445
+ state_store = RewriteStateStore.new(git_dir: @git_cli.git_dir(repo_path))
446
+ state = state_store.find_branch(branch)
447
+ raise PatchUtil::ValidationError, "no retained rewrite state for branch #{branch}" unless state
448
+
449
+ head = @git_cli.head_sha(repo_path)
450
+ unless head == state.head_sha
451
+ raise PatchUtil::ValidationError,
452
+ "branch #{branch} moved from #{state.head_sha} to #{head}; run restore-rewrite to restore from #{state.backup_ref} before continuing"
453
+ end
454
+
455
+ unless File.directory?(state.worktree_path)
456
+ raise PatchUtil::ValidationError,
457
+ "retained rewrite worktree is missing: #{state.worktree_path}; run restore-rewrite or abort-rewrite"
458
+ end
459
+
460
+ [branch, head, state_store, state]
461
+ end
462
+
463
+ def normalize_pending_revisions(repo_path, state, current_revision, pending_revisions)
464
+ return pending_revisions if pending_revisions.first == current_revision
465
+
466
+ [current_revision, *@git_cli.rev_list(repo_path, "#{current_revision}..#{state.head_sha}")]
467
+ end
468
+
469
+ def finalize_rewrite(repo_path, branch, old_head, backup_ref, worktree, state_store)
470
+ new_head = @git_cli.head_sha(worktree)
471
+ @git_cli.update_ref(repo_path, "refs/heads/#{branch}", new_head, old_head)
472
+ @git_cli.reset_hard(repo_path, new_head)
473
+ @git_cli.worktree_remove(repo_path, worktree)
474
+ state_store.clear_branch(branch)
475
+
476
+ Result.new(branch: branch, old_head: old_head, new_head: new_head, backup_ref: backup_ref, commits: [])
477
+ end
478
+ end
479
+ end
480
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+
6
+ module PatchUtil
7
+ module Git
8
+ class RewriteStateStore
9
+ State = Data.define(:branch, :target_sha, :head_sha, :backup_ref, :worktree_path, :status, :message, :created_at,
10
+ :pending_revisions)
11
+
12
+ def initialize(git_dir:)
13
+ @git_dir = File.expand_path(git_dir)
14
+ end
15
+
16
+ def path
17
+ File.join(@git_dir, 'patch_util', 'rewrite_state.json')
18
+ end
19
+
20
+ def load
21
+ return [] unless File.exist?(path)
22
+
23
+ payload = JSON.parse(File.read(path))
24
+ payload.fetch('states', []).map do |item|
25
+ State.new(
26
+ branch: item.fetch('branch'),
27
+ target_sha: item.fetch('target_sha'),
28
+ head_sha: item.fetch('head_sha'),
29
+ backup_ref: item.fetch('backup_ref'),
30
+ worktree_path: item.fetch('worktree_path'),
31
+ status: item.fetch('status'),
32
+ message: item.fetch('message'),
33
+ created_at: item.fetch('created_at'),
34
+ pending_revisions: item.fetch('pending_revisions', [])
35
+ )
36
+ end
37
+ end
38
+
39
+ def record_failure(state)
40
+ states = load.reject { |item| item.branch == state.branch }
41
+ states << state
42
+ save(states)
43
+ end
44
+
45
+ def clear_branch(branch)
46
+ states = load.reject { |item| item.branch == branch }
47
+ save(states)
48
+ end
49
+
50
+ def find_branch(branch)
51
+ load.find { |state| state.branch == branch }
52
+ end
53
+
54
+ private
55
+
56
+ def save(states)
57
+ if states.empty?
58
+ FileUtils.rm_f(path)
59
+ return
60
+ end
61
+
62
+ FileUtils.mkdir_p(File.dirname(path))
63
+ File.write(path, JSON.pretty_generate('states' => states.map { |state| serialize(state) }) + "\n")
64
+ end
65
+
66
+ def serialize(state)
67
+ {
68
+ 'branch' => state.branch,
69
+ 'target_sha' => state.target_sha,
70
+ 'head_sha' => state.head_sha,
71
+ 'backup_ref' => state.backup_ref,
72
+ 'worktree_path' => state.worktree_path,
73
+ 'status' => state.status,
74
+ 'message' => state.message,
75
+ 'created_at' => state.created_at,
76
+ 'pending_revisions' => state.pending_revisions
77
+ }
78
+ end
79
+ end
80
+ end
81
+ end