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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e6922fb856381f64462a95b3d1cac90be5d9cce4eca63783cb0966da67252047
4
+ data.tar.gz: c4b080b73b5e6eacc8bf030c375ae3bd5caf12bc490bb55d6e78b6d2aca00f87
5
+ SHA512:
6
+ metadata.gz: 815db1d23e934a049c97f733b7948d8fa88cb040262c52fe00f0f3241f96e6d35ffad69553b4245b35de98007314017716356f8e324987d5358bda7e8a5f9b93
7
+ data.tar.gz: d1b3348828a4732ca3250b3a4ab392fa3738c45d273463124e18bdc30f06f1dc1f971f3fdb458a5fa0513a8f63c040e48d97eabc68bb9c25c73549e0ce3cce16
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ Initial release.
2
+
3
+ - Add GitHub Actions CI for Ruby matrix testing.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hmdne
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,285 @@
1
+ # PatchUtil
2
+
3
+ PatchUtil is a Ruby library and CLI for splitting one large patch into a sequence of smaller, reviewable patches. It is designed around an `inspect -> plan -> apply` workflow that works well for both humans and AI agents.
4
+
5
+ The main surface is the `split` subsystem:
6
+
7
+ - `split inspect` shows a patch with stable hunk and changed-line labels
8
+ - `split plan` turns those labels into named split chunks
9
+ - `split apply` materializes the saved plan as ordered patch files or rewritten commits
10
+
11
+ The `rewrite` subsystem is supporting machinery for harder git-history splits. It manages retained rewrite state, conflict recovery, and resume/restore flows after `split apply --rewrite` has started.
12
+
13
+ The API and CLI are still evolving and should be considered unstable until version 1.0.
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem "patch_util"
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ ```sh
26
+ bundle install
27
+ ```
28
+
29
+ If you want the gem installed directly:
30
+
31
+ ```sh
32
+ gem install patch_util
33
+ ```
34
+
35
+ For local development from this checkout:
36
+
37
+ ```sh
38
+ bundle install
39
+ bundle exec rake spec
40
+ ```
41
+
42
+ ## Why PatchUtil
43
+
44
+ PatchUtil is built for cases where one diff or one commit mixes several independent intent units.
45
+
46
+ Typical examples:
47
+
48
+ - split a large refactor into reviewable commits
49
+ - separate rename/mode metadata from later content edits
50
+ - split one earlier git commit and replay descendants on top
51
+ - let an AI agent inspect a large patch, propose chunk boundaries, and then apply them deterministically
52
+
53
+ The project uses stable hunk labels (`a`, `b`, `c`) and changed-line labels (`a1`, `a2`, `b1`) so plans can stay textual and easy to generate.
54
+
55
+ ## Split Workflow
56
+
57
+ The normal workflow is:
58
+
59
+ 1. inspect the commit or patch
60
+ 2. choose selectors for named chunks
61
+ 3. persist the plan
62
+ 4. apply the plan
63
+
64
+ ### Inspect
65
+
66
+ Choose source options based on what you are splitting:
67
+
68
+ - use `--repo` plus `--commit` for a git-backed split workflow
69
+ - use `--patch` (and usually `--plan`) for a standalone patch-file workflow
70
+ - these flags are contextual source selectors, not mandatory boilerplate on every invocation
71
+
72
+ Inspect a git commit:
73
+
74
+ ```sh
75
+ patch_util split inspect --repo /path/to/repo --commit HEAD~2
76
+ ```
77
+
78
+ Inspect a patch file:
79
+
80
+ ```sh
81
+ patch_util split inspect --patch sample.diff --plan sample.plan.json
82
+ ```
83
+
84
+ The output labels hunks and changed lines so you can refer to them later:
85
+
86
+ - whole hunks: `a`, `b`, `c`
87
+ - whole-hunk ranges: `a-c`, `z-ab`
88
+ - single changed lines: `a1`, `a2`
89
+ - ranges inside one hunk: `a1-a4`
90
+
91
+ Default inspect output is the full annotated diff because it is the authoritative planning surface.
92
+
93
+ ### Compact Inspect For Agents
94
+
95
+ For large commits, especially vendor-heavy ones, compact inspect gives a skim-friendly overview:
96
+
97
+ ```sh
98
+ patch_util split inspect --repo /path/to/repo --commit HEAD~2 --compact
99
+ ```
100
+
101
+ Compact mode keeps the same labels but adds a layered summary:
102
+
103
+ - compact legend
104
+ - file index
105
+ - per-file and per-hunk counts
106
+ - largest-first index ordering
107
+ - compact hunk summaries in original diff order
108
+
109
+ You can then drill into only the hunks that matter:
110
+
111
+ ```sh
112
+ patch_util split inspect --repo /path/to/repo --commit HEAD~2 --compact --expand a-c,br
113
+ ```
114
+
115
+ That keeps the compact skim for the whole patch while expanding only the selected hunks to full annotated row bodies.
116
+
117
+ `--expand` is intentionally narrow:
118
+
119
+ - it only works together with `--compact`
120
+ - it accepts whole-hunk labels and hunk ranges such as `a,b,br` or `a-c`
121
+ - it does not accept changed-line selectors such as `a1-a4`
122
+
123
+ Recommended agent loop for very large commits:
124
+
125
+ 1. start with full `split inspect` if the patch still looks manageable
126
+ 2. switch to `--compact` when the patch is too noisy to scan directly
127
+ 3. use `--expand` only on the few hunks that look relevant
128
+ 4. move to `split plan` once the chunk boundaries are clear
129
+
130
+ ### Plan
131
+
132
+ Create a split plan from named chunks and selectors:
133
+
134
+ ```sh
135
+ patch_util split plan \
136
+ --repo /path/to/repo \
137
+ --commit HEAD~2 \
138
+ "rename and setup" "a-b" \
139
+ "logic change" "c1-c4,d" \
140
+ "leftovers"
141
+ ```
142
+
143
+ Selectors can combine whole hunks and changed-line ranges:
144
+
145
+ ```text
146
+ a-c,e1-e4,e6
147
+ ```
148
+
149
+ Rules:
150
+
151
+ - selecting a whole hunk and partial lines from that same hunk is an error
152
+ - overlapping selections across chunks are an error
153
+ - if changed lines are left unassigned, planning fails unless you declare a leftovers chunk
154
+ - leftovers are declared as the final positional chunk name, with no selector text after it
155
+
156
+ If you do not specify a leftovers chunk, PatchUtil fails closed instead of silently dropping the unassigned changes. That is deliberate: omitted leftovers would otherwise mean those changed lines disappear from the emitted patches or rewritten history.
157
+
158
+ Today, PatchUtil treats that as a safety stop, even though removal might sometimes be the right outcome. In those cases, the current workflow is to re-plan explicitly rather than relying on implicit omission.
159
+
160
+ Example with leftovers:
161
+
162
+ ```sh
163
+ patch_util split plan \
164
+ --repo /path/to/repo \
165
+ --commit HEAD~2 \
166
+ "rename metadata" "a" \
167
+ "core logic" "b1-b5,c-d" \
168
+ "leftovers"
169
+ ```
170
+
171
+ Inside a git repository, plans default to `.git/patch_util/plans.json`.
172
+
173
+ ### Apply
174
+
175
+ Materialize the saved plan as patch files:
176
+
177
+ ```sh
178
+ patch_util split apply \
179
+ --patch sample.diff \
180
+ --plan sample.plan.json \
181
+ --output-dir out
182
+ ```
183
+
184
+ Use this mode when you want PatchUtil to emit patch files only, without changing git history.
185
+
186
+ Apply a saved git-backed plan by rewriting an earlier commit:
187
+
188
+ ```sh
189
+ patch_util split apply \
190
+ --repo /path/to/repo \
191
+ --commit HEAD~2 \
192
+ --rewrite
193
+ ```
194
+
195
+ Use `--rewrite` only when the split should become real replacement commits inside the repository history. In other words:
196
+
197
+ - without `--rewrite`, PatchUtil emits patch files
198
+ - with `--rewrite`, PatchUtil replaces the targeted commit with one commit per chunk and then replays later descendants on top
199
+
200
+ When rewriting history, PatchUtil:
201
+
202
+ - creates one replacement commit per named chunk
203
+ - preserves the original split commit's author, committer, body, and trailers
204
+ - appends `Split-from:` and `Original-subject:` metadata
205
+ - replays later descendants on top
206
+ - records a backup ref under `refs/patch_util-backups/...`
207
+
208
+ Current rewrite guardrails:
209
+
210
+ - merge commits are rejected as split targets
211
+ - descendant replay is only supported on linear history; replay ranges containing merge commits fail up front
212
+
213
+ ## Rewrite Subsystem
214
+
215
+ The top-level `rewrite` commands are mainly recovery and inspection tools for difficult history rewrites.
216
+
217
+ You normally start from `split apply --rewrite`, and only use `rewrite ...` if the rewrite needs help afterward.
218
+
219
+ For agents, this boundary matters:
220
+
221
+ - prefer `split inspect`, `split plan`, and `split apply` in normal explanations
222
+ - treat `rewrite ...` as recovery/support tooling, not the default planning interface
223
+ - surface `rewrite status`, `rewrite conflicts`, `rewrite continue`, and `rewrite restore` after a rewrite has already started or failed
224
+
225
+ Examples:
226
+
227
+ ```sh
228
+ patch_util rewrite status
229
+ patch_util rewrite conflicts
230
+ patch_util rewrite continue
231
+ patch_util rewrite restore
232
+ ```
233
+
234
+ This layer exists so harder split rewrites can be resumed, inspected, or restored without mixing that recovery flow into the main `split` planning UX.
235
+
236
+ ## After PatchUtil
237
+
238
+ PatchUtil handles the split itself. After that, you may still want ordinary git history-polish steps outside PatchUtil.
239
+
240
+ Human-driven follow-up:
241
+
242
+ - use `git rebase -i` later if you want to combine adjacent split commits, reorder them, or reword commit messages
243
+
244
+ Agent-friendly or non-TTY follow-up:
245
+
246
+ - use non-interactive git commands such as `git commit --amend -m ...`, `git reset --soft HEAD~2 && git commit ...`, or scripted cherry-pick/replay flows when you need similar cleanup without an interactive editor
247
+
248
+ Those steps are outside PatchUtil's command surface, but they fit naturally after `split apply --rewrite` has produced the first-pass split history.
249
+
250
+ ## Agent Skill
251
+
252
+ PatchUtil is intended to be usable by AI agents. This repository includes a `SKILL.md` focused on the `split` workflow.
253
+
254
+ OpenCode one-liner install:
255
+
256
+ ```sh
257
+ mkdir -p ~/.config/opencode/skills/patch_util && curl -fsSL https://raw.githubusercontent.com/rbutils/patch_util/master/SKILL.md -o ~/.config/opencode/skills/patch_util/SKILL.md
258
+ ```
259
+
260
+ After that, OpenCode can discover the skill from the standard global skills directory.
261
+
262
+ ## Development
263
+
264
+ After checking out the repo, run:
265
+
266
+ ```sh
267
+ bundle install
268
+ bundle exec rake spec
269
+ ```
270
+
271
+ You can run the executable directly from the checkout:
272
+
273
+ ```sh
274
+ bundle exec exe/patch_util version
275
+ bundle exec exe/patch_util split help
276
+ bundle exec exe/patch_util rewrite help
277
+ ```
278
+
279
+ ## Contributing
280
+
281
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rbutils/patch_util.
282
+
283
+ ## License
284
+
285
+ The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/SKILL.md ADDED
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: patch_util
3
+ description: Split large diffs or commits into smaller reviewable patches with an inspect -> plan -> apply workflow. Prefer the split subsystem; use rewrite only for retained rewrite recovery and harder history-rewrite cases.
4
+ license: MIT
5
+ ---
6
+
7
+ # PatchUtil Skill
8
+
9
+ Use this skill when you need to break one large git commit or diff into smaller reviewable units.
10
+
11
+ ## When To Use It
12
+
13
+ - a git commit should be split into several commits
14
+ - a patch file needs the same inspect -> plan -> apply treatment outside a repository
15
+ - you need a stable textual selector language for agent-planned patch splits
16
+ - a large commit is easier to navigate in compact inspect mode before choosing split boundaries
17
+
18
+ ## Preferred Workflow
19
+
20
+ Default to the `split` subsystem:
21
+
22
+ 1. `split inspect` on the git commit you want to split
23
+ 2. `split plan` against that same git-backed source
24
+ 3. `split apply --rewrite` when the split should become real replacement commits
25
+
26
+ Source selectors are contextual:
27
+
28
+ - use `--repo` with `--commit` for the primary git-backed workflow
29
+ - use `--patch` for standalone diff files when there is no repository-backed source
30
+ - `--repo` and `--commit` are optional source selectors, not mandatory boilerplate
31
+ - these flags are not all required at once; they describe which source PatchUtil should inspect or apply
32
+
33
+ Treat the top-level `rewrite` subsystem as advanced recovery machinery for `split apply --rewrite`, not as the first thing to expose to users.
34
+
35
+ ## Core Commands
36
+
37
+ Primary git-backed inspect:
38
+
39
+ ```sh
40
+ patch_util split inspect --repo /path/to/repo --commit HEAD~2
41
+ ```
42
+
43
+ Compact inspect for a large git commit:
44
+
45
+ ```sh
46
+ patch_util split inspect --repo /path/to/repo --commit HEAD~2 --compact
47
+ ```
48
+
49
+ Compact inspect with targeted drill-down:
50
+
51
+ ```sh
52
+ patch_util split inspect --repo /path/to/repo --commit HEAD~2 --compact --expand a-c,br
53
+ ```
54
+
55
+ Git-backed planning:
56
+
57
+ ```sh
58
+ patch_util split plan \
59
+ --repo /path/to/repo \
60
+ --commit HEAD~2 \
61
+ "first chunk" "a-c" \
62
+ "second chunk" "d1-d4,e" \
63
+ "leftovers"
64
+ ```
65
+
66
+ Use `--expand` only with `--compact`, and only with whole-hunk labels or hunk ranges.
67
+
68
+ Git-backed apply by rewriting the original history:
69
+
70
+ ```sh
71
+ patch_util split apply \
72
+ --repo /path/to/repo \
73
+ --commit HEAD~2 \
74
+ --rewrite
75
+ ```
76
+
77
+ Use `--rewrite` only when the result should become real replacement commits in repository history. If you only want emitted patch files, use `split apply` without `--rewrite` and provide `--output-dir` instead.
78
+
79
+ Patch-file inspect remains available when there is no repo-backed source:
80
+
81
+ ```sh
82
+ patch_util split inspect --patch sample.diff --plan sample.plan.json
83
+ ```
84
+
85
+ Patch-file apply remains available when you want emitted patch files instead of history rewrite:
86
+
87
+ ```sh
88
+ patch_util split apply --patch sample.diff --plan sample.plan.json --output-dir out
89
+ ```
90
+
91
+ ## Selector Rules
92
+
93
+ - whole hunks use labels like `a`, `b`, `c`
94
+ - whole-hunk ranges use labels like `a-c` or `z-ab`
95
+ - changed lines use labels like `a1`, `a2`, `b1`
96
+ - ranges must stay inside one hunk, for example `a1-a4`
97
+ - do not mix whole-hunk and partial selection for the same hunk in one plan
98
+ - if anything is intentionally left unassigned, add a leftovers chunk name as the final positional argument to `split plan`
99
+
100
+ If no leftovers chunk is declared, PatchUtil fails instead of silently removing unassigned changes. That fail-closed behavior is deliberate: implicit omission would otherwise drop those changes from the output. If removal is actually intended, PatchUtil currently expects a more explicit re-plan rather than treating missing leftovers as permission to delete.
101
+
102
+ ## Agent Guidance
103
+
104
+ - start with git-backed `split inspect --repo ... --commit ...` unless the work is explicitly patch-file-only
105
+ - use `--compact` for large or noisy commits
106
+ - use `--expand` only after compact inspect has identified the interesting hunks
107
+ - keep `--expand` inputs at whole-hunk labels or hunk ranges only; changed-line selectors belong to `split plan`, not to compact drill-down
108
+ - propose chunk names based on reviewable intent, not file count alone
109
+ - preserve rename/mode/file-operation intent as first-class patch units when present
110
+ - prefer `split` language in explanations; mention `rewrite` only when recovery or history replay becomes relevant
111
+ - surface `rewrite` commands only after `split apply --rewrite` has started or failed
112
+
113
+ ## Rewrite Notes
114
+
115
+ If `split apply --rewrite` hits trouble, the retained rewrite commands are available:
116
+
117
+ - `rewrite status`
118
+ - `rewrite conflicts`
119
+ - `rewrite continue`
120
+ - `rewrite restore`
121
+
122
+ These are support tools for difficult rewrite cases, not the main planning interface.
123
+
124
+ Rewrite guardrails still apply:
125
+
126
+ - merge commits are not supported as split targets
127
+ - descendant replay ranges containing merge commits are rejected up front
128
+
129
+ ## After The Split
130
+
131
+ PatchUtil's job is to produce the split cleanly. After that, normal git tools may still be useful.
132
+
133
+ - a human may use `git rebase -i` later to combine split commits, reorder them, or reword commit messages
134
+ - agents should prefer non-interactive follow-up commands such as `git commit --amend -m ...`, `git reset --soft HEAD~2 && git commit ...`, or scripted cherry-pick/replay flows instead of assuming an interactive TTY editor
135
+ - those follow-up git steps are outside PatchUtil itself, but they are normal after `split apply --rewrite` has created the first-pass split history
136
+
137
+ ## Installation
138
+
139
+ If `patch_util` is not available on the machine yet, install the gem first:
140
+
141
+ ```sh
142
+ gem install patch_util
143
+ ```
144
+
145
+ Agents should do that whenever they intend to use the tool and the command is not available.
146
+
147
+ OpenCode global install:
148
+
149
+ ```sh
150
+ mkdir -p ~/.config/opencode/skills/patch_util && curl -fsSL https://raw.githubusercontent.com/rbutils/patch_util/master/SKILL.md -o ~/.config/opencode/skills/patch_util/SKILL.md
151
+ ```
152
+
153
+ Repo-local install:
154
+
155
+ ```sh
156
+ mkdir -p .opencode/skills/patch_util && cp SKILL.md .opencode/skills/patch_util/SKILL.md
157
+ ```
data/exe/patch_util ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+ require "patch_util"
6
+
7
+ begin
8
+ PatchUtil::CLI.start(ARGV)
9
+ rescue PatchUtil::Error => e
10
+ warn e.message
11
+ exit 1
12
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module PatchUtil
6
+ class CLI < Thor
7
+ def self.exit_on_failure?
8
+ true
9
+ end
10
+
11
+ desc 'version', 'Display PatchUtil version'
12
+ def version
13
+ puts PatchUtil::VERSION
14
+ end
15
+
16
+ desc 'rewrite SUBCOMMAND ...ARGS', 'Manage retained git rewrite sessions'
17
+ subcommand 'rewrite', PatchUtil::Git::RewriteCLI
18
+
19
+ desc 'split SUBCOMMAND ...ARGS', 'Inspect, plan, and emit split patches'
20
+ subcommand 'split', PatchUtil::Split::CLI
21
+ end
22
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PatchUtil
4
+ Diff = Data.define(:source, :file_diffs) do
5
+ def hunks
6
+ file_diffs.flat_map(&:hunks)
7
+ end
8
+
9
+ def change_rows
10
+ hunks.flat_map(&:change_rows)
11
+ end
12
+
13
+ def hunk_by_label(label)
14
+ hunks.find { |hunk| hunk.label == label }
15
+ end
16
+
17
+ def row_by_id(row_id)
18
+ hunks.each do |hunk|
19
+ row = hunk.rows.find { |candidate| candidate.id == row_id }
20
+ return row if row
21
+ end
22
+ nil
23
+ end
24
+ end
25
+
26
+ FileDiff = Data.define(:old_path, :new_path, :hunks, :diff_git_line, :metadata_lines) do
27
+ def modification?
28
+ old_path != '/dev/null' && new_path != '/dev/null'
29
+ end
30
+
31
+ def addition?
32
+ old_path == '/dev/null'
33
+ end
34
+
35
+ def deletion?
36
+ new_path == '/dev/null'
37
+ end
38
+
39
+ def rename?
40
+ metadata_lines.any? { |line| line.start_with?('rename from ') || line.start_with?('rename to ') }
41
+ end
42
+
43
+ def copy?
44
+ metadata_lines.any? { |line| line.start_with?('copy from ') || line.start_with?('copy to ') }
45
+ end
46
+
47
+ def binary?
48
+ hunks.any?(&:binary?)
49
+ end
50
+
51
+ def operation_hunks
52
+ hunks.select(&:operation?)
53
+ end
54
+
55
+ def operation_hunk
56
+ operation_hunks.first
57
+ end
58
+
59
+ def path_operation_hunk
60
+ operation_hunks.find(&:path_change?)
61
+ end
62
+
63
+ def text_hunks
64
+ hunks.select(&:text?)
65
+ end
66
+ end
67
+
68
+ Hunk = Data.define(:label, :old_start, :old_count, :new_start, :new_count, :section, :rows, :kind, :patch_lines) do
69
+ def change_rows
70
+ rows.select(&:change?)
71
+ end
72
+
73
+ def change_lines
74
+ lines = []
75
+ change_rows.each do |row|
76
+ lines << ChangeLine.new(
77
+ label: row.change_label,
78
+ row_id: row.id,
79
+ kind: row.kind,
80
+ text: row.text,
81
+ old_lineno: row.old_lineno,
82
+ new_lineno: row.new_lineno
83
+ )
84
+ end
85
+ lines
86
+ end
87
+
88
+ def operation?
89
+ kind == :file_operation
90
+ end
91
+
92
+ def binary?
93
+ kind == :binary
94
+ end
95
+
96
+ def text?
97
+ kind == :text
98
+ end
99
+
100
+ def path_change?
101
+ patch_lines.any? do |line|
102
+ line.start_with?('rename from ') || line.start_with?('rename to ') ||
103
+ line.start_with?('copy from ') || line.start_with?('copy to ')
104
+ end
105
+ end
106
+ end
107
+
108
+ Row = Data.define(:id, :kind, :text, :old_lineno, :new_lineno, :change_label, :change_ordinal) do
109
+ def change?
110
+ !change_ordinal.nil?
111
+ end
112
+
113
+ def display_prefix
114
+ case kind
115
+ when :context
116
+ ' '
117
+ when :deletion
118
+ '-'
119
+ when :addition
120
+ '+'
121
+ when :file_operation
122
+ '='
123
+ when :binary
124
+ '='
125
+ else
126
+ raise PatchUtil::UnsupportedFeatureError, "unknown row kind: #{kind.inspect}"
127
+ end
128
+ end
129
+ end
130
+
131
+ ChangeLine = Data.define(:label, :row_id, :kind, :text, :old_lineno, :new_lineno)
132
+ end