hunkify 0.2.1 → 0.3.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 +4 -4
- data/README.md +7 -7
- data/lib/hunkify/cli.rb +3 -3
- data/lib/hunkify/{anthropic_api.rb → opencode.rb} +35 -88
- data/lib/hunkify/ui.rb +3 -3
- data/lib/hunkify/version.rb +1 -1
- data/lib/hunkify.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 488c3d31406ccddec198dfa1f48e26ae6fe88ac4830e4dc3152ed8378a718a07
|
|
4
|
+
data.tar.gz: ea034c42d3dac0e85ef205243ed9da7875f280b3c756e42bfa37afb8dbe99fd8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 44222ddc4ec5685814d177030618a964555fd4562b77cd58bc1c3ea06fd5a989de48fb5fc31fc3b529c1357c28763a57d68908c07556612bbe830b6d9ff2475d
|
|
7
|
+
data.tar.gz: 26a34a3f74cacdaca9b4a2f0a75bbd201266bce7b05ab7bf27c77c615122cab30db0e3f90480738318638fc96aa5a034d72dcba2903c7f995bb16fa53b0dbeba
|
data/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# hunkify
|
|
2
2
|
|
|
3
|
-
Split your staged changes into atomic, well-scoped commits — powered by
|
|
3
|
+
Split your staged changes into atomic, well-scoped commits — powered by GitHub Copilot through OpenCode.
|
|
4
4
|
|
|
5
|
-
Instead of cramming everything into a single `git commit -m "wip"`, `hunkify` parses your staged diff hunk by hunk, asks
|
|
5
|
+
Instead of cramming everything into a single `git commit -m "wip"`, `hunkify` parses your staged diff hunk by hunk, asks OpenCode to group them by intent using GitHub Copilot, and produces conventional commits (with gitmojis) ready to apply.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **Hunk-level analysis** — each change block is treated independently, so one file can span multiple commits if needed.
|
|
10
|
-
- **AI-powered grouping** via
|
|
10
|
+
- **AI-powered grouping** via GitHub Copilot and OpenCode.
|
|
11
11
|
- **Gitmoji + conventional commits** — `:sparkles: feat(scope): …`, `:bug: fix(scope): …`, etc.
|
|
12
12
|
- **Interactive review** — confirm, edit, or skip each proposed commit before it's created.
|
|
13
13
|
- **Free-form context argument** — pass a ticket ID, feature name, or any directive to steer the output.
|
|
@@ -16,7 +16,7 @@ Instead of cramming everything into a single `git commit -m "wip"`, `hunkify` pa
|
|
|
16
16
|
## Requirements
|
|
17
17
|
|
|
18
18
|
- Ruby ≥ 2.7
|
|
19
|
-
-
|
|
19
|
+
- [OpenCode](https://opencode.ai/) authenticated with GitHub Copilot
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
@@ -32,10 +32,10 @@ gem "hunkify"
|
|
|
32
32
|
|
|
33
33
|
## Setup
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Authenticate OpenCode with GitHub Copilot:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
|
|
38
|
+
opencode providers login
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
## Usage
|
|
@@ -56,7 +56,7 @@ hunkify "focus on refactoring"
|
|
|
56
56
|
### Workflow
|
|
57
57
|
|
|
58
58
|
1. `hunkify` parses your staged diff into hunks.
|
|
59
|
-
2.
|
|
59
|
+
2. OpenCode asks GitHub Copilot to propose a grouping into logical commits.
|
|
60
60
|
3. For each proposed commit, you can **confirm**, **edit the message**, **skip**, or **quit**.
|
|
61
61
|
4. After a final confirmation, the plan is applied — each commit is created via `git apply --cached`.
|
|
62
62
|
|
data/lib/hunkify/cli.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require_relative "color"
|
|
4
4
|
require_relative "diff_parser"
|
|
5
5
|
require_relative "git"
|
|
6
|
-
require_relative "
|
|
6
|
+
require_relative "opencode"
|
|
7
7
|
require_relative "ui"
|
|
8
8
|
|
|
9
9
|
module Hunkify
|
|
@@ -43,11 +43,11 @@ module Hunkify
|
|
|
43
43
|
|
|
44
44
|
hunks_by_id = hunks.each_with_object({}) { |h, acc| acc[h.id] = h }
|
|
45
45
|
|
|
46
|
-
puts Color.dim(" Analyzing and grouping via
|
|
46
|
+
puts Color.dim(" Analyzing and grouping via OpenCode #{OpenCode::MODEL}...")
|
|
47
47
|
puts
|
|
48
48
|
|
|
49
49
|
begin
|
|
50
|
-
result =
|
|
50
|
+
result = OpenCode.group_hunks(hunks, context: context)
|
|
51
51
|
rescue JSON::ParserError
|
|
52
52
|
puts Color.red(" ✗ Invalid AI response (malformed JSON). Try again.")
|
|
53
53
|
exit 1
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "net/http"
|
|
4
3
|
require "json"
|
|
5
|
-
require "
|
|
4
|
+
require "open3"
|
|
6
5
|
|
|
7
6
|
module Hunkify
|
|
8
|
-
module
|
|
9
|
-
|
|
10
|
-
MODEL = "claude-haiku-4-5-20251001"
|
|
7
|
+
module OpenCode
|
|
8
|
+
MODEL = "github-copilot/gpt-5.6-terra"
|
|
11
9
|
|
|
12
10
|
SYSTEM_PROMPT = <<~PROMPT
|
|
13
11
|
You are a Git expert. You are given a list of hunks (blocks of modifications)
|
|
@@ -20,11 +18,11 @@ module Hunkify
|
|
|
20
18
|
- STRONGLY prefer fine-grained, atomic commits over large bundled ones.
|
|
21
19
|
When in doubt, SPLIT rather than merge.
|
|
22
20
|
- Heuristics to split:
|
|
23
|
-
* Different modules/components/features
|
|
24
|
-
* Core logic vs. tests
|
|
25
|
-
* Core logic vs. docs
|
|
26
|
-
* Core logic vs. config/build files
|
|
27
|
-
* Unrelated fixes bundled with a feature
|
|
21
|
+
* Different modules/components/features -> different commits
|
|
22
|
+
* Core logic vs. tests -> different commits (one feat commit + one test commit)
|
|
23
|
+
* Core logic vs. docs -> different commits
|
|
24
|
+
* Core logic vs. config/build files -> different commits
|
|
25
|
+
* Unrelated fixes bundled with a feature -> separate them
|
|
28
26
|
* Each file introducing a new, independent capability usually deserves
|
|
29
27
|
its own commit
|
|
30
28
|
- Only bundle hunks together when they genuinely cannot be reviewed or
|
|
@@ -39,11 +37,6 @@ module Hunkify
|
|
|
39
37
|
"message": ":sparkles: feat(scope): description in English",
|
|
40
38
|
"hunk_ids": [1, 3, 5],
|
|
41
39
|
"reasoning": "brief explanation of the grouping"
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"message": ":bug: fix(scope): description in English",
|
|
45
|
-
"hunk_ids": [2, 4],
|
|
46
|
-
"reasoning": "brief explanation"
|
|
47
40
|
}
|
|
48
41
|
]
|
|
49
42
|
}
|
|
@@ -65,53 +58,6 @@ module Hunkify
|
|
|
65
58
|
RESPOND ONLY WITH THE JSON. No markdown, no explanation.
|
|
66
59
|
PROMPT
|
|
67
60
|
|
|
68
|
-
def self.group_hunks(hunks, context: nil)
|
|
69
|
-
api_key = ENV["ANTHROPIC_API_KEY"]
|
|
70
|
-
raise "ANTHROPIC_API_KEY missing! Add it to your ~/.zshrc or ~/.bashrc" if api_key.nil? || api_key.empty?
|
|
71
|
-
|
|
72
|
-
user_ctx = context && !context.empty? ? "\nUser context: #{context}" : ""
|
|
73
|
-
hunks_summary = hunks.map(&:to_summary).join("\n\n---\n\n")
|
|
74
|
-
user_message = "#{user_ctx}\n\nHere are the hunks to group:\n\n#{hunks_summary}"
|
|
75
|
-
|
|
76
|
-
uri = URI(API_URL)
|
|
77
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
78
|
-
http.use_ssl = true
|
|
79
|
-
http.read_timeout = 30
|
|
80
|
-
|
|
81
|
-
request = Net::HTTP::Post.new(uri.path)
|
|
82
|
-
request["Content-Type"] = "application/json"
|
|
83
|
-
request["x-api-key"] = api_key
|
|
84
|
-
request["anthropic-version"] = "2023-06-01"
|
|
85
|
-
request.body = JSON.generate({
|
|
86
|
-
model: MODEL,
|
|
87
|
-
max_tokens: 1024,
|
|
88
|
-
system: SYSTEM_PROMPT,
|
|
89
|
-
messages: [{role: "user", content: user_message}]
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
response = http.request(request)
|
|
93
|
-
body = JSON.parse(response.body)
|
|
94
|
-
|
|
95
|
-
raise "API Error #{response.code}: #{body["error"]&.dig("message")}" unless response.code == "200"
|
|
96
|
-
|
|
97
|
-
raw = body.dig("content", 0, "text")&.strip
|
|
98
|
-
|
|
99
|
-
if ENV["HUNKIFY_DEBUG"]
|
|
100
|
-
warn "\n--- RAW AI RESPONSE ---\n#{raw}\n-----------------------\n"
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
cleaned = raw
|
|
104
|
-
.gsub(/\A```(?:json)?\s*/i, "")
|
|
105
|
-
.gsub(/\s*```\z/, "")
|
|
106
|
-
.strip
|
|
107
|
-
|
|
108
|
-
if (match = cleaned.match(/(\{.+\})/m))
|
|
109
|
-
cleaned = match[1]
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
JSON.parse(cleaned)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
61
|
SUGGEST_SYSTEM_PROMPT = <<~PROMPT
|
|
116
62
|
You are a Git expert. You are given one or more hunks that the user wants
|
|
117
63
|
to bundle into a single commit. Produce ONE conventional commit message
|
|
@@ -132,35 +78,36 @@ module Hunkify
|
|
|
132
78
|
RESPOND ONLY WITH THE MESSAGE. No markdown, no quotes, no explanation.
|
|
133
79
|
PROMPT
|
|
134
80
|
|
|
135
|
-
def self.
|
|
136
|
-
|
|
137
|
-
|
|
81
|
+
def self.group_hunks(hunks, context: nil)
|
|
82
|
+
user_ctx = context && !context.empty? ? "\nUser context: #{context}" : ""
|
|
83
|
+
summary = hunks.map(&:to_summary).join("\n\n---\n\n")
|
|
84
|
+
raw = ask(SYSTEM_PROMPT, "#{user_ctx}\n\nHere are the hunks to group:\n\n#{summary}")
|
|
85
|
+
cleaned = raw.gsub(/\A```(?:json)?\s*/i, "").gsub(/\s*```\z/, "").strip
|
|
86
|
+
cleaned = Regexp.last_match(1) if cleaned.match(/(\{.+\})/m)
|
|
87
|
+
JSON.parse(cleaned)
|
|
88
|
+
end
|
|
138
89
|
|
|
90
|
+
def self.suggest_message(hunks, context: nil)
|
|
139
91
|
user_ctx = context && !context.empty? ? "\nUser context: #{context}" : ""
|
|
140
92
|
summary = hunks.map(&:to_summary).join("\n\n---\n\n")
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
response = http.request(request)
|
|
160
|
-
body = JSON.parse(response.body)
|
|
161
|
-
raise "API Error #{response.code}: #{body["error"]&.dig("message")}" unless response.code == "200"
|
|
162
|
-
|
|
163
|
-
body.dig("content", 0, "text").to_s.strip.lines.first.to_s.strip
|
|
93
|
+
ask(SUGGEST_SYSTEM_PROMPT, "#{user_ctx}\n\nHunks to bundle into a single commit:\n\n#{summary}").lines.first.to_s.strip
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.ask(system_prompt, user_message)
|
|
97
|
+
output, status = Open3.capture2e("opencode", "run", "--model", MODEL, "--format", "json", "#{system_prompt}\n\n#{user_message}")
|
|
98
|
+
raise "OpenCode failed: #{output.strip}" unless status.success?
|
|
99
|
+
|
|
100
|
+
raw = output.lines.filter_map do |line|
|
|
101
|
+
event = JSON.parse(line)
|
|
102
|
+
event.dig("part", "text") if event["type"] == "text"
|
|
103
|
+
rescue JSON::ParserError
|
|
104
|
+
nil
|
|
105
|
+
end.join.strip
|
|
106
|
+
|
|
107
|
+
raise "OpenCode returned no text response" if raw.empty?
|
|
108
|
+
|
|
109
|
+
warn "\n--- RAW AI RESPONSE ---\n#{raw}\n-----------------------\n" if ENV["HUNKIFY_DEBUG"]
|
|
110
|
+
raw
|
|
164
111
|
end
|
|
165
112
|
end
|
|
166
113
|
end
|
data/lib/hunkify/ui.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Hunkify
|
|
|
12
12
|
puts
|
|
13
13
|
puts " #{Color.magenta("▲")} #{Color.bold("hunkify")} #{Color.dim("v#{version}")}"
|
|
14
14
|
puts " #{Color.dim("─" * 40)}"
|
|
15
|
-
puts " #{Color.dim("Atomic commits, grouped by
|
|
15
|
+
puts " #{Color.dim("Atomic commits, grouped by GitHub Copilot.")}"
|
|
16
16
|
puts
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -65,9 +65,9 @@ module Hunkify
|
|
|
65
65
|
|
|
66
66
|
def prompt_new_message(hunk_ids, hunks_by_id, context:)
|
|
67
67
|
hunks = hunk_ids.map { |id| hunks_by_id[id] }.compact
|
|
68
|
-
print Color.dim(" Asking
|
|
68
|
+
print Color.dim(" Asking OpenCode for a suggestion... ")
|
|
69
69
|
begin
|
|
70
|
-
suggestion =
|
|
70
|
+
suggestion = OpenCode.suggest_message(hunks, context: context)
|
|
71
71
|
rescue => e
|
|
72
72
|
puts Color.red("failed (#{e.message})")
|
|
73
73
|
suggestion = nil
|
data/lib/hunkify/version.rb
CHANGED
data/lib/hunkify.rb
CHANGED
|
@@ -5,7 +5,7 @@ require_relative "hunkify/color"
|
|
|
5
5
|
require_relative "hunkify/hunk"
|
|
6
6
|
require_relative "hunkify/diff_parser"
|
|
7
7
|
require_relative "hunkify/git"
|
|
8
|
-
require_relative "hunkify/
|
|
8
|
+
require_relative "hunkify/opencode"
|
|
9
9
|
require_relative "hunkify/ui"
|
|
10
10
|
require_relative "hunkify/cli"
|
|
11
11
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hunkify
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tom SCHIAVI
|
|
@@ -9,8 +9,8 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description: hunkify analyzes staged hunks, asks
|
|
13
|
-
commits, and applies them via git apply --cached.
|
|
12
|
+
description: hunkify analyzes staged hunks, asks OpenCode with GitHub Copilot to group
|
|
13
|
+
them into logical commits, and applies them via git apply --cached.
|
|
14
14
|
executables:
|
|
15
15
|
- hunkify
|
|
16
16
|
extensions: []
|
|
@@ -20,12 +20,12 @@ files:
|
|
|
20
20
|
- README.md
|
|
21
21
|
- bin/hunkify
|
|
22
22
|
- lib/hunkify.rb
|
|
23
|
-
- lib/hunkify/anthropic_api.rb
|
|
24
23
|
- lib/hunkify/cli.rb
|
|
25
24
|
- lib/hunkify/color.rb
|
|
26
25
|
- lib/hunkify/diff_parser.rb
|
|
27
26
|
- lib/hunkify/git.rb
|
|
28
27
|
- lib/hunkify/hunk.rb
|
|
28
|
+
- lib/hunkify/opencode.rb
|
|
29
29
|
- lib/hunkify/ui.rb
|
|
30
30
|
- lib/hunkify/version.rb
|
|
31
31
|
homepage: https://github.com/tomschiavi/smartcommit
|
|
@@ -51,5 +51,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
51
51
|
requirements: []
|
|
52
52
|
rubygems_version: 4.0.8
|
|
53
53
|
specification_version: 4
|
|
54
|
-
summary: Split staged changes into atomic commits using
|
|
54
|
+
summary: Split staged changes into atomic commits using GitHub Copilot.
|
|
55
55
|
test_files: []
|