commiti 1.3.0 → 1.3.2
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 +70 -8
- data/lib/commiti.rb +3 -0
- data/lib/flows/base_flow.rb +7 -2
- data/lib/flows/commit_flow.rb +6 -4
- data/lib/flows/pr_flow.rb +39 -11
- data/lib/services/diff_summarization/fallback_builder.rb +69 -30
- data/lib/services/flow_context_builder.rb +3 -2
- data/lib/services/git/commit/commit_execution.rb +47 -28
- data/lib/services/git/commit/commit_staging.rb +3 -3
- data/lib/services/git/git_reader.rb +29 -19
- data/lib/services/git/pr/browser_opener.rb +44 -0
- data/lib/services/git/pr/pr_creator.rb +167 -0
- data/lib/services/git/pr/pr_opener.rb +96 -113
- data/lib/services/git/pr/remote_parser.rb +73 -0
- data/lib/services/google_client.rb +6 -13
- data/lib/services/helpers/clipboard.rb +4 -8
- data/lib/services/helpers/config_loader.rb +42 -5
- data/lib/services/helpers/interactive_prompt.rb +5 -5
- data/lib/services/helpers/prompt_builder.rb +44 -3
- data/lib/services/helpers/spinner.rb +19 -11
- data/lib/services/helpers/terminal_ui.rb +58 -0
- data/lib/services/message_generator.rb +43 -132
- data/lib/services/message_generator_support.rb +111 -0
- data/lib/services/message_presenter.rb +10 -8
- data/lib/services/text_generation_style.rb +172 -0
- metadata +10 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 82b26f3eb5a9606aa58e723e85a764395b485d7d84175878560173262c266615
|
|
4
|
+
data.tar.gz: 4e92dadbaf1f3fc84f15b4650b632a054aa0ddf924349590d652361964213db5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b0fe4d3e034e6c05809c8ec000b1b44eecc8de21c3d166e5f8735f96f8d718d3bcfc2472a421f540e1509c203ed91c430d99122ae128bd4cfe9824d83de93efa
|
|
7
|
+
data.tar.gz: d79d70ab801e09ab81c8d6d2e9339b01d799ff874225ec49684b21e9e55baf0b85352ebf8914e2710a3ed4a2d46496f567ca9ee7cce2106047da8129e90c5ec0
|
data/README.md
CHANGED
|
@@ -39,25 +39,49 @@ commiti [options]
|
|
|
39
39
|
|
|
40
40
|
## Configuration
|
|
41
41
|
|
|
42
|
-
Commiti uses a
|
|
42
|
+
Commiti uses environment variables for secrets and a checked-in project config file for text-generation styling.
|
|
43
43
|
|
|
44
44
|
Set variables in your shell, CI secret manager, or local `.env` file (in your project):
|
|
45
45
|
|
|
46
46
|
```dotenv
|
|
47
47
|
GOOGLE_API_KEY=your_google_ai_key
|
|
48
48
|
|
|
49
|
+
# Optional: provider API tokens for API-first PR/MR creation
|
|
50
|
+
# COMMITI_GITHUB_TOKEN=your_github_token
|
|
51
|
+
# COMMITI_GITLAB_TOKEN=your_gitlab_token
|
|
52
|
+
# COMMITI_GITBUCKET_TOKEN=your_gitbucket_token
|
|
53
|
+
|
|
49
54
|
# Optional overrides:
|
|
50
55
|
# COMMITI_MODEL=gemma-4-31b-it
|
|
51
56
|
# COMMITI_CANDIDATES=1
|
|
52
57
|
# COMMITI_BASE_BRANCH=main
|
|
53
58
|
# COMMITI_NO_COPY=false
|
|
54
59
|
# COMMITI_AUTO_SPLIT=false
|
|
60
|
+
|
|
61
|
+
# Optional per-project prompt styling (safe YAML, no code execution):
|
|
62
|
+
# COMMITI_CONFIG=.commiti.yml
|
|
55
63
|
```
|
|
56
64
|
|
|
57
65
|
`GEMINI_API_KEY` is also accepted as an alias for `GOOGLE_API_KEY`.
|
|
58
66
|
|
|
59
67
|
You can copy `.env.example` as a starting point.
|
|
60
68
|
|
|
69
|
+
For project-specific wording and structure, add a `.commiti.yml` file at the repo root:
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
text_generation:
|
|
73
|
+
commit:
|
|
74
|
+
subject_case: uppercase # uppercase, lowercase, or preserve
|
|
75
|
+
pr:
|
|
76
|
+
sections:
|
|
77
|
+
- name: Overview
|
|
78
|
+
guidance: Summarize the change in one paragraph.
|
|
79
|
+
- name: Validation
|
|
80
|
+
guidance: Describe the checks or tests that were run.
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The file is parsed with safe YAML loading, and Commiti only accepts declarative styling settings from it.
|
|
84
|
+
|
|
61
85
|
Your API key is sent directly from your local process to Google's API.
|
|
62
86
|
Commiti does not store it and does not proxy requests through any Commiti server.
|
|
63
87
|
Never commit `.env` to git.
|
|
@@ -113,14 +137,52 @@ Commit edit mode uses:
|
|
|
113
137
|
- `## Motivation`
|
|
114
138
|
- `## Changes Made`
|
|
115
139
|
- `## Testing Notes`
|
|
116
|
-
3.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
-
|
|
140
|
+
3. Attempts to create and open PR/MR:
|
|
141
|
+
- **API-first path** (when token is configured):
|
|
142
|
+
- GitHub/GitBucket: creates PR via provider API and opens the created PR URL.
|
|
143
|
+
- GitLab: creates MR via provider API and opens the created MR URL.
|
|
144
|
+
- **Fallback path** (when no token, provider unsupported, or API call fails):
|
|
145
|
+
- Opens browser with prefilled PR/MR form using query parameters.
|
|
146
|
+
- If the URL would exceed safe browser/provider limits (~1800 characters), Commiti keeps the title and intelligently truncates the description to the longest text that still fits.
|
|
121
147
|
4. Asks before opening browser.
|
|
122
148
|
|
|
123
|
-
|
|
149
|
+
Commiti can create PRs/MRs via provider APIs when tokens are configured, and always opens the resulting page in your browser.
|
|
150
|
+
|
|
151
|
+
### Provider API Logic
|
|
152
|
+
|
|
153
|
+
When you set a provider token in your configuration, Commiti uses an **API-first strategy**:
|
|
154
|
+
|
|
155
|
+
**Supported Providers:**
|
|
156
|
+
- **GitHub** (github.com and GitHub Enterprise): Uses GitHub REST API v3
|
|
157
|
+
- **GitLab** (gitlab.com and self-hosted): Uses GitLab API v4
|
|
158
|
+
- **GitBucket**: Uses GitHub-compatible API
|
|
159
|
+
|
|
160
|
+
**Token Configuration:**
|
|
161
|
+
```dotenv
|
|
162
|
+
COMMITI_GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
163
|
+
COMMITI_GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
164
|
+
COMMITI_GITBUCKET_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**API Request Flow:**
|
|
168
|
+
1. Parses the Git remote URL to extract provider, host, namespace, and repository.
|
|
169
|
+
2. Constructs provider-specific API endpoint and authentication headers.
|
|
170
|
+
3. Sends HTTP POST request with generated PR title and description.
|
|
171
|
+
4. On success (HTTP 2xx): Returns the created PR/MR URL directly.
|
|
172
|
+
5. On failure: Falls back to browser prefill with a user-friendly error message explaining why.
|
|
173
|
+
|
|
174
|
+
**Error Handling:**
|
|
175
|
+
- **Missing token**: Falls back to browser prefill. (Info message)
|
|
176
|
+
- **Unsupported provider**: Falls back to browser prefill. (Warning message)
|
|
177
|
+
- **API error**: Falls back to browser prefill with error details. (Warning message)
|
|
178
|
+
- **Redirect handling**: Automatically follows HTTP redirects (301, 302, 307, 308) but aborts if redirected to a different host.
|
|
179
|
+
- **Network errors**: Caught and reported with fallback to browser prefill.
|
|
180
|
+
|
|
181
|
+
**Advantages of API-First:**
|
|
182
|
+
- Creates PR/MR immediately without manual form interaction.
|
|
183
|
+
- Preserves full description text (no URL length constraints).
|
|
184
|
+
- Seamlessly opens the created PR/MR for immediate review and collaboration.
|
|
185
|
+
- Gracefully degrades to browser prefill if API is unavailable.
|
|
124
186
|
|
|
125
187
|
### Diff Context Protocol
|
|
126
188
|
|
|
@@ -180,7 +242,7 @@ Core services:
|
|
|
180
242
|
- `lib/services/diff_summarization/diff_summarizer.rb`: Orchestrates large-diff summarization and summary combine.
|
|
181
243
|
- `lib/services/diff_summarization/batch_runner.rb`: Runs asynchronous, batched per-file summarization jobs.
|
|
182
244
|
- `lib/services/diff_summarization/fallback_builder.rb`: Builds deterministic summaries when model summarization fails or times out.
|
|
183
|
-
- `lib/services/helpers/config_loader.rb`: Loads
|
|
245
|
+
- `lib/services/helpers/config_loader.rb`: Loads environment config plus secure project-level text-generation styling.
|
|
184
246
|
- `lib/services/helpers/prompt_builder.rb`: Builds strict system/user prompts for commit and PR modes.
|
|
185
247
|
- `lib/services/helpers/interactive_prompt.rb`: Handles confirmation prompts, candidate selection, editor loop, and commit message validation.
|
|
186
248
|
- `lib/services/helpers/clipboard.rb`: Provides cross-platform clipboard support.
|
data/lib/commiti.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'services/git/git_reader'
|
|
4
4
|
require_relative 'services/git/git_writer'
|
|
5
|
+
require_relative 'services/text_generation_style'
|
|
5
6
|
require_relative 'services/google_client'
|
|
6
7
|
require_relative 'services/helpers/config_loader'
|
|
7
8
|
require_relative 'services/git/diff_parser'
|
|
@@ -9,7 +10,9 @@ require_relative 'services/diff_summarization/diff_summarizer'
|
|
|
9
10
|
require_relative 'services/helpers/prompt_builder'
|
|
10
11
|
require_relative 'services/helpers/interactive_prompt'
|
|
11
12
|
require_relative 'services/git/pr/pr_opener'
|
|
13
|
+
require_relative 'services/git/pr/pr_creator'
|
|
12
14
|
require_relative 'services/helpers/clipboard'
|
|
15
|
+
require_relative 'services/helpers/terminal_ui'
|
|
13
16
|
require_relative 'services/helpers/spinner'
|
|
14
17
|
require_relative 'services/flow_context_builder'
|
|
15
18
|
require_relative 'services/message_generator'
|
data/lib/flows/base_flow.rb
CHANGED
|
@@ -18,7 +18,8 @@ module Commiti
|
|
|
18
18
|
diff: diff,
|
|
19
19
|
client: client,
|
|
20
20
|
run_stage: method(:run_stage),
|
|
21
|
-
model: selected_model
|
|
21
|
+
model: selected_model,
|
|
22
|
+
text_generation_config: options[:text_generation]
|
|
22
23
|
)
|
|
23
24
|
Commiti::MessagePresenter.print_summarization_notice(context[:summarized_result])
|
|
24
25
|
|
|
@@ -91,7 +92,11 @@ module Commiti
|
|
|
91
92
|
end
|
|
92
93
|
|
|
93
94
|
def message_generator
|
|
94
|
-
@message_generator ||= Commiti::MessageGenerator.new(
|
|
95
|
+
@message_generator ||= Commiti::MessageGenerator.new(
|
|
96
|
+
flow_type: flow_type,
|
|
97
|
+
run_stage: method(:run_stage),
|
|
98
|
+
text_generation_config: options[:text_generation]
|
|
99
|
+
)
|
|
95
100
|
end
|
|
96
101
|
end
|
|
97
102
|
end
|
data/lib/flows/commit_flow.rb
CHANGED
|
@@ -51,7 +51,8 @@ module Commiti
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def run_single_group_context(context:, client:, model:)
|
|
54
|
-
|
|
54
|
+
message = 'Auto-split found a single connected change group. Falling back to single commit flow.'
|
|
55
|
+
puts "\n#{Commiti::TerminalUI.status(:info, message)}"
|
|
55
56
|
Commiti::MessagePresenter.print_summarization_notice(context[:summarized_result])
|
|
56
57
|
|
|
57
58
|
message = generate_message_for_context(context:, client:, model:)
|
|
@@ -63,7 +64,7 @@ module Commiti
|
|
|
63
64
|
groups = context[:change_groups]
|
|
64
65
|
run_stage('Unstaging current index for grouped commit execution') { Commiti::GitWriter.unstage_all! }
|
|
65
66
|
|
|
66
|
-
puts "\
|
|
67
|
+
puts "\n#{Commiti::TerminalUI.status(:info, "Auto-split detected #{groups.length} connected change groups.")}"
|
|
67
68
|
|
|
68
69
|
groups.each_with_index do |group, index|
|
|
69
70
|
break if process_group(group:, index:, total: groups.length, client:, model:) == :stop
|
|
@@ -74,7 +75,7 @@ module Commiti
|
|
|
74
75
|
run_stage("Staging files for group #{index + 1}/#{total}") { Commiti::GitWriter.stage_files!(group[:files]) }
|
|
75
76
|
return :continue unless run_stage('Checking staged changes') { Commiti::GitWriter.staged_changes? }
|
|
76
77
|
|
|
77
|
-
puts "\
|
|
78
|
+
puts "\n#{Commiti::TerminalUI.header("Group #{index + 1}/#{total} files")}:"
|
|
78
79
|
group[:files].each { |path| puts "- #{path}" }
|
|
79
80
|
|
|
80
81
|
group_context = build_context(diff: group_diff(group), client:, model:)
|
|
@@ -84,7 +85,8 @@ module Commiti
|
|
|
84
85
|
maybe_copy_to_clipboard(message)
|
|
85
86
|
return :continue if finalize(message) == :committed
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
stop_message = "Stopping auto-split flow at group #{index + 1} because commit was skipped."
|
|
89
|
+
puts Commiti::TerminalUI.status(:warn, stop_message)
|
|
88
90
|
run_stage('Restaging remaining uncommitted changes') { Commiti::GitWriter.stage_all! }
|
|
89
91
|
:stop
|
|
90
92
|
end
|
data/lib/flows/pr_flow.rb
CHANGED
|
@@ -20,25 +20,53 @@ module Commiti
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def maybe_open_pr_page(description, base_branch)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
head_branch = Commiti::GitWriter.current_branch
|
|
24
|
+
origin_url = Commiti::GitWriter.origin_url
|
|
25
|
+
title = Commiti::PrOpener.suggest_title(description, head_branch: head_branch)
|
|
26
|
+
|
|
27
|
+
prompt_text = 'Create PR and open it in browser now?'
|
|
28
|
+
|
|
29
|
+
unless Commiti::InteractivePrompt.ask_yes_no(prompt_text, default: :no)
|
|
30
|
+
puts "\nPR creation skipped.\n\n"
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
api_result = run_stage('Creating PR/MR via provider API (if token configured)') do
|
|
35
|
+
Commiti::PrCreator.create(
|
|
28
36
|
origin_url: origin_url,
|
|
29
37
|
base_branch: base_branch,
|
|
30
38
|
head_branch: head_branch,
|
|
31
39
|
title: title,
|
|
32
|
-
body: description
|
|
40
|
+
body: description,
|
|
41
|
+
config: options
|
|
33
42
|
)
|
|
34
43
|
end
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
pr_url = api_result[:url]
|
|
46
|
+
|
|
47
|
+
if pr_url.nil?
|
|
48
|
+
case api_result[:reason]
|
|
49
|
+
when :missing_token
|
|
50
|
+
puts "\n#{Commiti::TerminalUI.status(:info, "No #{api_result[:provider]} token configured; using browser prefill fallback.")}"
|
|
51
|
+
when :unsupported_provider
|
|
52
|
+
puts "\n#{Commiti::TerminalUI.status(:warn, 'Provider API is unsupported; using browser prefill fallback.')}"
|
|
53
|
+
when :api_error
|
|
54
|
+
puts "\n#{Commiti::TerminalUI.status(:warn, "PR API create failed: #{api_result[:error]}. Using browser prefill fallback.")}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
pr_url = run_stage('Preparing prefilled PR URL') do
|
|
58
|
+
Commiti::PrOpener.compare_url(
|
|
59
|
+
origin_url: origin_url,
|
|
60
|
+
base_branch: base_branch,
|
|
61
|
+
head_branch: head_branch,
|
|
62
|
+
title: title,
|
|
63
|
+
body: description
|
|
64
|
+
)
|
|
65
|
+
end
|
|
41
66
|
end
|
|
67
|
+
|
|
68
|
+
run_stage('Opening browser') { Commiti::PrOpener.open_in_browser(pr_url) }
|
|
69
|
+
puts "\nOpened PR page:\n#{pr_url}\n\n"
|
|
42
70
|
end
|
|
43
71
|
end
|
|
44
72
|
end
|
|
@@ -12,49 +12,88 @@ module Commiti
|
|
|
12
12
|
|
|
13
13
|
def fallback_summary(diff, chunks: nil)
|
|
14
14
|
parsed_chunks = chunks || Commiti::DiffParser.split_by_file(diff)
|
|
15
|
-
files =
|
|
15
|
+
files = file_stats_for(parsed_chunks)
|
|
16
|
+
return diff.to_s[0, FALLBACK_BYTES] if files.empty?
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
path: chunk[:path].to_s,
|
|
20
|
-
additions: 0,
|
|
21
|
-
deletions: 0,
|
|
22
|
-
status: 'modified'
|
|
23
|
-
}
|
|
18
|
+
render_fallback_summary(files)
|
|
19
|
+
end
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
stripped = line.strip
|
|
27
|
-
current[:status] = 'added' if stripped.start_with?('new file mode')
|
|
28
|
-
current[:status] = 'deleted' if stripped.start_with?('deleted file mode')
|
|
29
|
-
current[:status] = 'renamed' if stripped.start_with?('rename from ') || stripped.start_with?('rename to ')
|
|
21
|
+
private
|
|
30
22
|
|
|
31
|
-
|
|
23
|
+
def file_stats_for(chunks)
|
|
24
|
+
chunks.map { |chunk| file_stats_for_chunk(chunk) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def file_stats_for_chunk(chunk)
|
|
28
|
+
status, additions, deletions = file_status_and_counts(chunk[:diff])
|
|
29
|
+
{
|
|
30
|
+
path: chunk[:path].to_s,
|
|
31
|
+
additions: additions,
|
|
32
|
+
deletions: deletions,
|
|
33
|
+
status: status
|
|
34
|
+
}
|
|
35
|
+
end
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
def file_status_and_counts(diff_text)
|
|
38
|
+
status = 'modified'
|
|
39
|
+
additions = 0
|
|
40
|
+
deletions = 0
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
diff_text.to_s.each_line do |line|
|
|
43
|
+
status = detect_status(line, current: status)
|
|
44
|
+
next if metadata_line?(line)
|
|
45
|
+
|
|
46
|
+
additions += 1 if line.start_with?('+')
|
|
47
|
+
deletions += 1 if line.start_with?('-')
|
|
38
48
|
end
|
|
39
49
|
|
|
40
|
-
|
|
50
|
+
[status, additions, deletions]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def detect_status(line, current:)
|
|
54
|
+
stripped = line.strip
|
|
55
|
+
return 'added' if stripped.start_with?('new file mode')
|
|
56
|
+
return 'deleted' if stripped.start_with?('deleted file mode')
|
|
57
|
+
return 'renamed' if stripped.start_with?('rename from ') || stripped.start_with?('rename to ')
|
|
41
58
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
current
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def metadata_line?(line)
|
|
63
|
+
line.start_with?('diff --git ', '+++', '---', '@@')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def render_fallback_summary(files)
|
|
67
|
+
summary_lines = [
|
|
68
|
+
'### Diff Overview',
|
|
69
|
+
"- Total files changed: #{files.length}",
|
|
70
|
+
''
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
append_file_sections(summary_lines, files)
|
|
74
|
+
append_truncation_notice(summary_lines, files)
|
|
46
75
|
|
|
76
|
+
summary_lines.join("\n").strip
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def append_file_sections(summary_lines, files)
|
|
47
80
|
files.first(MAX_FILES_IN_SUMMARY).each do |file|
|
|
48
|
-
|
|
49
|
-
lines << "- Status: #{file[:status]}"
|
|
50
|
-
lines << "- Added lines: #{file[:additions]}"
|
|
51
|
-
lines << "- Removed lines: #{file[:deletions]}"
|
|
52
|
-
lines << ''
|
|
81
|
+
summary_lines.concat(render_file_section(file))
|
|
53
82
|
end
|
|
83
|
+
end
|
|
54
84
|
|
|
55
|
-
|
|
85
|
+
def append_truncation_notice(summary_lines, files)
|
|
86
|
+
summary_lines << "...and #{files.length - MAX_FILES_IN_SUMMARY} more files" if files.length > MAX_FILES_IN_SUMMARY
|
|
87
|
+
end
|
|
56
88
|
|
|
57
|
-
|
|
89
|
+
def render_file_section(file)
|
|
90
|
+
[
|
|
91
|
+
"### #{file[:path]}",
|
|
92
|
+
"- Status: #{file[:status]}",
|
|
93
|
+
"- Added lines: #{file[:additions]}",
|
|
94
|
+
"- Removed lines: #{file[:deletions]}",
|
|
95
|
+
''
|
|
96
|
+
]
|
|
58
97
|
end
|
|
59
98
|
end
|
|
60
99
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Commiti
|
|
4
4
|
module FlowContextBuilder
|
|
5
|
-
def self.build(flow_type:, diff:, client:, run_stage:, model:)
|
|
5
|
+
def self.build(flow_type:, diff:, client:, run_stage:, model:, text_generation_config: nil)
|
|
6
6
|
line_chunks = Commiti::DiffParser.split_by_file_lines(diff)
|
|
7
7
|
diff_metadata = Commiti::DiffParser.metadata_from_line_chunks(line_chunks)
|
|
8
8
|
change_groups = Commiti::ChangeGrouping.group(line_chunks)
|
|
@@ -21,7 +21,8 @@ module Commiti
|
|
|
21
21
|
diff: summarized_result[:content],
|
|
22
22
|
summarized: summarized_result[:summarized],
|
|
23
23
|
raw_diff: diff,
|
|
24
|
-
diff_metadata: diff_metadata
|
|
24
|
+
diff_metadata: diff_metadata,
|
|
25
|
+
style_config: text_generation_config
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
{
|
|
@@ -10,47 +10,66 @@ module Commiti
|
|
|
10
10
|
|
|
11
11
|
case action
|
|
12
12
|
when :yes
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
edited = edit_message_until_valid(working_message)
|
|
20
|
-
if edited.nil?
|
|
21
|
-
puts "\nEditor did not exit successfully. Commit skipped.\n\n"
|
|
22
|
-
return :skipped
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
working_message = edited
|
|
26
|
-
print_message.call(working_message)
|
|
27
|
-
next
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
puts "\nCommit skipped.\n\n"
|
|
31
|
-
return :skipped
|
|
32
|
-
end
|
|
13
|
+
result, next_message = handle_yes_action(
|
|
14
|
+
working_message,
|
|
15
|
+
run_stage: run_stage,
|
|
16
|
+
print_message: print_message
|
|
17
|
+
)
|
|
18
|
+
return result if result
|
|
33
19
|
|
|
34
|
-
|
|
35
|
-
puts output unless output.to_s.strip.empty?
|
|
36
|
-
puts "\nCommit created.\n\n"
|
|
37
|
-
return :committed
|
|
20
|
+
working_message = next_message
|
|
38
21
|
when :edit
|
|
39
22
|
edited = edit_message_until_valid(working_message)
|
|
40
23
|
if edited.nil?
|
|
41
|
-
puts "\
|
|
24
|
+
puts "\n#{Commiti::TerminalUI.status(:fail, 'Editor did not exit successfully.')}\n\n"
|
|
42
25
|
next
|
|
43
26
|
end
|
|
44
27
|
|
|
45
28
|
working_message = edited
|
|
46
29
|
print_message.call(working_message)
|
|
47
30
|
else
|
|
48
|
-
|
|
31
|
+
print_skip_message
|
|
49
32
|
return :skipped
|
|
50
33
|
end
|
|
51
34
|
end
|
|
52
35
|
end
|
|
53
36
|
|
|
37
|
+
def self.handle_yes_action(working_message, run_stage:, print_message:)
|
|
38
|
+
errors = Commiti::InteractivePrompt.commit_message_errors(working_message)
|
|
39
|
+
return commit_message(working_message, run_stage: run_stage) if errors.empty?
|
|
40
|
+
|
|
41
|
+
puts "\n#{Commiti::TerminalUI.status(:warn, 'Current message needs fixes before commit:')}"
|
|
42
|
+
errors.each { |error| puts "- #{error}" }
|
|
43
|
+
|
|
44
|
+
unless Commiti::InteractivePrompt.ask_yes_no('Open editor to fix now?', default: :yes)
|
|
45
|
+
print_skip_message
|
|
46
|
+
return [:skipped, nil]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
edited = edit_message_until_valid(working_message)
|
|
50
|
+
if edited.nil?
|
|
51
|
+
puts "\n#{Commiti::TerminalUI.status(:fail, 'Editor did not exit successfully. Commit skipped.')}\n\n"
|
|
52
|
+
return [:skipped, nil]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
print_message.call(edited)
|
|
56
|
+
[nil, edited]
|
|
57
|
+
end
|
|
58
|
+
private_class_method :handle_yes_action
|
|
59
|
+
|
|
60
|
+
def self.commit_message(message, run_stage:)
|
|
61
|
+
output = run_stage.call('Writing commit') { Commiti::GitWriter.commit_with_message_file(message) }
|
|
62
|
+
puts output unless output.to_s.strip.empty?
|
|
63
|
+
puts "\n#{Commiti::TerminalUI.status(:success, 'Commit created.')}\n\n"
|
|
64
|
+
[:committed, nil]
|
|
65
|
+
end
|
|
66
|
+
private_class_method :commit_message
|
|
67
|
+
|
|
68
|
+
def self.print_skip_message
|
|
69
|
+
puts "\n#{Commiti::TerminalUI.status(:warn, 'Commit skipped.')}\n\n"
|
|
70
|
+
end
|
|
71
|
+
private_class_method :print_skip_message
|
|
72
|
+
|
|
54
73
|
def self.edit_message_until_valid(initial_message)
|
|
55
74
|
working = initial_message
|
|
56
75
|
|
|
@@ -59,7 +78,7 @@ module Commiti
|
|
|
59
78
|
return nil if edited.nil?
|
|
60
79
|
|
|
61
80
|
if edited == working.to_s.strip
|
|
62
|
-
puts "\
|
|
81
|
+
puts "\n#{Commiti::TerminalUI.status(:info, 'No changes detected in editor.')}"
|
|
63
82
|
return edited unless Commiti::InteractivePrompt.ask_yes_no('Re-open editor now?', default: :yes)
|
|
64
83
|
|
|
65
84
|
next
|
|
@@ -68,7 +87,7 @@ module Commiti
|
|
|
68
87
|
errors = Commiti::InteractivePrompt.commit_message_errors(edited)
|
|
69
88
|
return edited if errors.empty?
|
|
70
89
|
|
|
71
|
-
puts "\
|
|
90
|
+
puts "\n#{Commiti::TerminalUI.status(:warn, 'Edited message needs fixes:')}"
|
|
72
91
|
errors.each { |error| puts "- #{error}" }
|
|
73
92
|
return edited unless Commiti::InteractivePrompt.ask_yes_no('Re-open editor now?', default: :yes)
|
|
74
93
|
|
|
@@ -11,11 +11,11 @@ module Commiti
|
|
|
11
11
|
status = run_stage.call('Reading git status') { Commiti::GitWriter.status_short }
|
|
12
12
|
raise 'No changes found in working tree.' if status.strip.empty?
|
|
13
13
|
|
|
14
|
-
puts "\
|
|
14
|
+
puts "\n#{Commiti::TerminalUI.header('Current git status')}\n\n#{status}"
|
|
15
15
|
return unless Commiti::InteractivePrompt.ask_yes_no('Run git add -A now?', default: :no)
|
|
16
16
|
|
|
17
17
|
run_stage.call('Staging changes (git add -A)') { Commiti::GitWriter.stage_all! }
|
|
18
|
-
puts "\
|
|
18
|
+
puts "\n#{Commiti::TerminalUI.status(:success, 'Staged changes with git add -A.')}\n"
|
|
19
19
|
end
|
|
20
20
|
private_class_method :maybe_stage_changes
|
|
21
21
|
|
|
@@ -26,7 +26,7 @@ module Commiti
|
|
|
26
26
|
if Commiti::InteractivePrompt.ask_yes_no('No staged changes found. Stage all changes now with git add -A?',
|
|
27
27
|
default: :yes)
|
|
28
28
|
run_stage.call('Staging changes (git add -A)') { Commiti::GitWriter.stage_all! }
|
|
29
|
-
puts "\
|
|
29
|
+
puts "\n#{Commiti::TerminalUI.status(:success, 'Staged changes with git add -A.')}\n"
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
staged = run_stage.call('Checking staged changes') { Commiti::GitWriter.staged_changes? }
|
|
@@ -124,34 +124,44 @@ module Commiti
|
|
|
124
124
|
output = +''
|
|
125
125
|
return output if max_bytes <= 0
|
|
126
126
|
|
|
127
|
+
header_lines, hunks = partition_chunk_lines(lines)
|
|
128
|
+
append_lines_with_limit(output, header_lines, max_bytes: max_bytes)
|
|
129
|
+
return output if hunks.empty?
|
|
130
|
+
|
|
131
|
+
append_hunks_with_limit(output, hunks, max_bytes: max_bytes)
|
|
132
|
+
output
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.partition_chunk_lines(lines)
|
|
127
136
|
header_lines = []
|
|
128
137
|
hunks = []
|
|
129
138
|
current_hunk = nil
|
|
130
|
-
in_hunks = false
|
|
131
139
|
|
|
132
140
|
lines.each do |line|
|
|
133
141
|
if line.start_with?('@@')
|
|
134
|
-
in_hunks = true
|
|
135
142
|
current_hunk = [line]
|
|
136
143
|
hunks << current_hunk
|
|
137
|
-
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
if in_hunks
|
|
144
|
+
elsif current_hunk
|
|
141
145
|
current_hunk << line
|
|
142
146
|
else
|
|
143
147
|
header_lines << line
|
|
144
148
|
end
|
|
145
149
|
end
|
|
146
150
|
|
|
147
|
-
header_lines
|
|
151
|
+
[header_lines, hunks]
|
|
152
|
+
end
|
|
153
|
+
private_class_method :partition_chunk_lines
|
|
154
|
+
|
|
155
|
+
def self.append_lines_with_limit(output, lines, max_bytes:)
|
|
156
|
+
lines.each do |line|
|
|
148
157
|
break if output.bytesize + line.bytesize > max_bytes
|
|
149
158
|
|
|
150
159
|
output << line
|
|
151
160
|
end
|
|
161
|
+
end
|
|
162
|
+
private_class_method :append_lines_with_limit
|
|
152
163
|
|
|
153
|
-
|
|
154
|
-
|
|
164
|
+
def self.append_hunks_with_limit(output, hunks, max_bytes:)
|
|
155
165
|
hunks.each do |hunk|
|
|
156
166
|
hunk_text = hunk.join
|
|
157
167
|
if output.bytesize + hunk_text.bytesize <= max_bytes
|
|
@@ -159,20 +169,20 @@ module Commiti
|
|
|
159
169
|
next
|
|
160
170
|
end
|
|
161
171
|
|
|
162
|
-
|
|
163
|
-
break if output.bytesize + hunk_header.bytesize > max_bytes
|
|
164
|
-
|
|
165
|
-
output << hunk_header
|
|
166
|
-
hunk[1..].to_a.each do |line|
|
|
167
|
-
break if output.bytesize + line.bytesize > max_bytes
|
|
168
|
-
|
|
169
|
-
output << line
|
|
170
|
-
end
|
|
172
|
+
append_partial_hunk(output, hunk, max_bytes: max_bytes)
|
|
171
173
|
break
|
|
172
174
|
end
|
|
175
|
+
end
|
|
176
|
+
private_class_method :append_hunks_with_limit
|
|
173
177
|
|
|
174
|
-
|
|
178
|
+
def self.append_partial_hunk(output, hunk, max_bytes:)
|
|
179
|
+
hunk_header = hunk.first
|
|
180
|
+
return if output.bytesize + hunk_header.bytesize > max_bytes
|
|
181
|
+
|
|
182
|
+
output << hunk_header
|
|
183
|
+
append_lines_with_limit(output, hunk[1..].to_a, max_bytes: max_bytes)
|
|
175
184
|
end
|
|
185
|
+
private_class_method :append_partial_hunk
|
|
176
186
|
|
|
177
187
|
def self.append_notice(clipped_diff, max_bytes:)
|
|
178
188
|
safe_clipped = clipped_diff.to_s
|