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
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
data/CHANGELOG.md
ADDED
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
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,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
|