commiti 1.3.1 → 1.3.3
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 +74 -8
- data/bin/commiti +6 -0
- data/lib/commiti.rb +3 -0
- data/lib/flows/base_flow.rb +7 -2
- data/lib/flows/commit_flow.rb +7 -3
- 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/change_grouping.rb +6 -0
- data/lib/services/git/commit/commit_execution.rb +45 -26
- data/lib/services/git/commit/commit_staging.rb +1 -1
- data/lib/services/git/commit/group_editor.rb +254 -0
- 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 +19 -9
- data/lib/services/helpers/prompt_builder.rb +44 -3
- data/lib/services/helpers/spinner.rb +4 -2
- data/lib/services/helpers/terminal_ui.rb +104 -8
- data/lib/services/message_generator.rb +46 -135
- data/lib/services/message_generator_support.rb +111 -0
- data/lib/services/message_presenter.rb +3 -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: 7853991e7fa65bd7c35642c50b00aead27bc7b3a4c11ffec7bb38be03a9e96ca
|
|
4
|
+
data.tar.gz: a42e9fbfec33a80cf95befe00c14f3e2bd23e7d334606589e21d2e1b6ab31a6f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82ed71618c42e1fbb745bfc04974a754b43396f232d26923c214a34738860d0a7566c90c22caa8e60dee383d953dcce0937e908a3bd81c1d1519e4cb84b3790a
|
|
7
|
+
data.tar.gz: b45f9669306182469b502bc98a2a1754c9ad3e9320b8801ac2a72bd3f412d0d01f348e8ac75fd5a82b002f84356b9e0ebb99df9c941ac6cc4607b8f53912c2ce
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Commiti
|
|
2
2
|
|
|
3
|
+
[](coverage/index.html) [](#)
|
|
4
|
+
|
|
3
5
|
AI-powered commit message and pull request description generator for Git repositories, using Google AI models.
|
|
4
6
|
|
|
5
7
|
## What It Does
|
|
@@ -39,25 +41,49 @@ commiti [options]
|
|
|
39
41
|
|
|
40
42
|
## Configuration
|
|
41
43
|
|
|
42
|
-
Commiti uses a
|
|
44
|
+
Commiti uses environment variables for secrets and a checked-in project config file for text-generation styling.
|
|
43
45
|
|
|
44
46
|
Set variables in your shell, CI secret manager, or local `.env` file (in your project):
|
|
45
47
|
|
|
46
48
|
```dotenv
|
|
47
49
|
GOOGLE_API_KEY=your_google_ai_key
|
|
48
50
|
|
|
51
|
+
# Optional: provider API tokens for API-first PR/MR creation
|
|
52
|
+
# COMMITI_GITHUB_TOKEN=your_github_token
|
|
53
|
+
# COMMITI_GITLAB_TOKEN=your_gitlab_token
|
|
54
|
+
# COMMITI_GITBUCKET_TOKEN=your_gitbucket_token
|
|
55
|
+
|
|
49
56
|
# Optional overrides:
|
|
50
57
|
# COMMITI_MODEL=gemma-4-31b-it
|
|
51
58
|
# COMMITI_CANDIDATES=1
|
|
52
59
|
# COMMITI_BASE_BRANCH=main
|
|
53
60
|
# COMMITI_NO_COPY=false
|
|
54
61
|
# COMMITI_AUTO_SPLIT=false
|
|
62
|
+
|
|
63
|
+
# Optional per-project prompt styling (safe YAML, no code execution):
|
|
64
|
+
# COMMITI_CONFIG=.commiti.yml
|
|
55
65
|
```
|
|
56
66
|
|
|
57
67
|
`GEMINI_API_KEY` is also accepted as an alias for `GOOGLE_API_KEY`.
|
|
58
68
|
|
|
59
69
|
You can copy `.env.example` as a starting point.
|
|
60
70
|
|
|
71
|
+
For project-specific wording and structure, add a `.commiti.yml` file at the repo root:
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
text_generation:
|
|
75
|
+
commit:
|
|
76
|
+
subject_case: uppercase # uppercase, lowercase, or preserve
|
|
77
|
+
pr:
|
|
78
|
+
sections:
|
|
79
|
+
- name: Overview
|
|
80
|
+
guidance: Summarize the change in one paragraph.
|
|
81
|
+
- name: Validation
|
|
82
|
+
guidance: Describe the checks or tests that were run.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The file is parsed with safe YAML loading, and Commiti only accepts declarative styling settings from it.
|
|
86
|
+
|
|
61
87
|
Your API key is sent directly from your local process to Google's API.
|
|
62
88
|
Commiti does not store it and does not proxy requests through any Commiti server.
|
|
63
89
|
Never commit `.env` to git.
|
|
@@ -76,6 +102,8 @@ Never commit `.env` to git.
|
|
|
76
102
|
By default, Commiti creates a single commit from staged changes.
|
|
77
103
|
Use `--auto-split` to let Commiti group connected file changes into multiple atomic commits.
|
|
78
104
|
|
|
105
|
+
When auto-split detects multiple groups, Commiti can optionally open a quick group editor so you can move files between groups before committing.
|
|
106
|
+
|
|
79
107
|
1. Shows `git status --short`.
|
|
80
108
|
2. Asks for confirmation before staging (`git add -A`).
|
|
81
109
|
3. Ensures there are staged changes.
|
|
@@ -113,14 +141,52 @@ Commit edit mode uses:
|
|
|
113
141
|
- `## Motivation`
|
|
114
142
|
- `## Changes Made`
|
|
115
143
|
- `## Testing Notes`
|
|
116
|
-
3.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
-
|
|
144
|
+
3. Attempts to create and open PR/MR:
|
|
145
|
+
- **API-first path** (when token is configured):
|
|
146
|
+
- GitHub/GitBucket: creates PR via provider API and opens the created PR URL.
|
|
147
|
+
- GitLab: creates MR via provider API and opens the created MR URL.
|
|
148
|
+
- **Fallback path** (when no token, provider unsupported, or API call fails):
|
|
149
|
+
- Opens browser with prefilled PR/MR form using query parameters.
|
|
150
|
+
- 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
151
|
4. Asks before opening browser.
|
|
122
152
|
|
|
123
|
-
|
|
153
|
+
Commiti can create PRs/MRs via provider APIs when tokens are configured, and always opens the resulting page in your browser.
|
|
154
|
+
|
|
155
|
+
### Provider API Logic
|
|
156
|
+
|
|
157
|
+
When you set a provider token in your configuration, Commiti uses an **API-first strategy**:
|
|
158
|
+
|
|
159
|
+
**Supported Providers:**
|
|
160
|
+
- **GitHub** (github.com and GitHub Enterprise): Uses GitHub REST API v3
|
|
161
|
+
- **GitLab** (gitlab.com and self-hosted): Uses GitLab API v4
|
|
162
|
+
- **GitBucket**: Uses GitHub-compatible API
|
|
163
|
+
|
|
164
|
+
**Token Configuration:**
|
|
165
|
+
```dotenv
|
|
166
|
+
COMMITI_GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
167
|
+
COMMITI_GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
168
|
+
COMMITI_GITBUCKET_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**API Request Flow:**
|
|
172
|
+
1. Parses the Git remote URL to extract provider, host, namespace, and repository.
|
|
173
|
+
2. Constructs provider-specific API endpoint and authentication headers.
|
|
174
|
+
3. Sends HTTP POST request with generated PR title and description.
|
|
175
|
+
4. On success (HTTP 2xx): Returns the created PR/MR URL directly.
|
|
176
|
+
5. On failure: Falls back to browser prefill with a user-friendly error message explaining why.
|
|
177
|
+
|
|
178
|
+
**Error Handling:**
|
|
179
|
+
- **Missing token**: Falls back to browser prefill. (Info message)
|
|
180
|
+
- **Unsupported provider**: Falls back to browser prefill. (Warning message)
|
|
181
|
+
- **API error**: Falls back to browser prefill with error details. (Warning message)
|
|
182
|
+
- **Redirect handling**: Automatically follows HTTP redirects (301, 302, 307, 308) but aborts if redirected to a different host.
|
|
183
|
+
- **Network errors**: Caught and reported with fallback to browser prefill.
|
|
184
|
+
|
|
185
|
+
**Advantages of API-First:**
|
|
186
|
+
- Creates PR/MR immediately without manual form interaction.
|
|
187
|
+
- Preserves full description text (no URL length constraints).
|
|
188
|
+
- Seamlessly opens the created PR/MR for immediate review and collaboration.
|
|
189
|
+
- Gracefully degrades to browser prefill if API is unavailable.
|
|
124
190
|
|
|
125
191
|
### Diff Context Protocol
|
|
126
192
|
|
|
@@ -180,7 +246,7 @@ Core services:
|
|
|
180
246
|
- `lib/services/diff_summarization/diff_summarizer.rb`: Orchestrates large-diff summarization and summary combine.
|
|
181
247
|
- `lib/services/diff_summarization/batch_runner.rb`: Runs asynchronous, batched per-file summarization jobs.
|
|
182
248
|
- `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
|
|
249
|
+
- `lib/services/helpers/config_loader.rb`: Loads environment config plus secure project-level text-generation styling.
|
|
184
250
|
- `lib/services/helpers/prompt_builder.rb`: Builds strict system/user prompts for commit and PR modes.
|
|
185
251
|
- `lib/services/helpers/interactive_prompt.rb`: Handles confirmation prompts, candidate selection, editor loop, and commit message validation.
|
|
186
252
|
- `lib/services/helpers/clipboard.rb`: Provides cross-platform clipboard support.
|
data/bin/commiti
CHANGED
|
@@ -47,6 +47,12 @@ OptionParser.new do |opts|
|
|
|
47
47
|
end.parse!
|
|
48
48
|
|
|
49
49
|
begin
|
|
50
|
+
model_label = ENV.fetch('COMMITI_MODEL', Commiti::GoogleClient::DEFAULT_MODEL)
|
|
51
|
+
flow_label = options[:type] == :pr ? 'PR flow' : 'Commit flow'
|
|
52
|
+
base_label = options[:type] == :pr ? "Base: #{options[:base_branch] || 'main'}" : nil
|
|
53
|
+
meta = [flow_label, "Model: #{model_label}", base_label].compact.join(' • ')
|
|
54
|
+
puts Commiti::TerminalUI.banner(title: 'Commiti', subtitle: 'AI commit & PR generator', meta: meta)
|
|
55
|
+
|
|
50
56
|
flow = if options[:type] == :pr
|
|
51
57
|
Commiti::Flows::PrFlow.new(options: options)
|
|
52
58
|
else
|
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,6 +10,7 @@ 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'
|
|
13
15
|
require_relative 'services/helpers/terminal_ui'
|
|
14
16
|
require_relative 'services/helpers/spinner'
|
|
@@ -18,6 +20,7 @@ require_relative 'services/message_presenter'
|
|
|
18
20
|
require_relative 'services/git/commit/commit_staging'
|
|
19
21
|
require_relative 'services/git/commit/commit_execution'
|
|
20
22
|
require_relative 'services/git/commit/change_grouping'
|
|
23
|
+
require_relative 'services/git/commit/group_editor'
|
|
21
24
|
require_relative 'flows/base_flow'
|
|
22
25
|
require_relative 'flows/commit_flow'
|
|
23
26
|
require_relative 'flows/pr_flow'
|
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
|
@@ -61,7 +61,12 @@ module Commiti
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def run_grouped_context(context:, client:, model:)
|
|
64
|
-
groups = context[:change_groups]
|
|
64
|
+
groups = Commiti::GroupEditor.edit(context[:change_groups])
|
|
65
|
+
if groups.length <= 1
|
|
66
|
+
single_context = groups.first ? build_context(diff: group_diff(groups.first), client:, model:) : context
|
|
67
|
+
return run_single_group_context(context: single_context, client:, model:)
|
|
68
|
+
end
|
|
69
|
+
|
|
65
70
|
run_stage('Unstaging current index for grouped commit execution') { Commiti::GitWriter.unstage_all! }
|
|
66
71
|
|
|
67
72
|
puts "\n#{Commiti::TerminalUI.status(:info, "Auto-split detected #{groups.length} connected change groups.")}"
|
|
@@ -75,8 +80,7 @@ module Commiti
|
|
|
75
80
|
run_stage("Staging files for group #{index + 1}/#{total}") { Commiti::GitWriter.stage_files!(group[:files]) }
|
|
76
81
|
return :continue unless run_stage('Checking staged changes') { Commiti::GitWriter.staged_changes? }
|
|
77
82
|
|
|
78
|
-
puts "\n#{Commiti::TerminalUI.
|
|
79
|
-
group[:files].each { |path| puts "- #{path}" }
|
|
83
|
+
puts "\n#{Commiti::TerminalUI.panel("Group #{index + 1}/#{total} files", Commiti::TerminalUI.bullets(group[:files]))}\n"
|
|
80
84
|
|
|
81
85
|
group_context = build_context(diff: group_diff(group), client:, model:)
|
|
82
86
|
Commiti::MessagePresenter.print_summarization_notice(group_context[:summarized_result])
|
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 "\n#{Commiti::TerminalUI.status(:warn, 'PR 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 "\n#{Commiti::TerminalUI.panel('Opened PR page', 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
|
{
|
|
@@ -18,6 +18,12 @@ module Commiti
|
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
def self.related?(left, right)
|
|
22
|
+
return false if left.to_s.strip.empty? || right.to_s.strip.empty?
|
|
23
|
+
|
|
24
|
+
connected?(left, right)
|
|
25
|
+
end
|
|
26
|
+
|
|
21
27
|
def self.connected_components(paths)
|
|
22
28
|
visited = {}
|
|
23
29
|
ordered_components = []
|
|
@@ -10,31 +10,14 @@ 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 "\n#{Commiti::TerminalUI.status(:fail, 'Editor 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 "\n#{Commiti::TerminalUI.status(:warn, 'Commit 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 "\n#{Commiti::TerminalUI.status(:success, 'Commit 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?
|
|
@@ -45,12 +28,48 @@ module Commiti
|
|
|
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 Commiti::TerminalUI.bullet(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
|
|
|
@@ -69,7 +88,7 @@ module Commiti
|
|
|
69
88
|
return edited if errors.empty?
|
|
70
89
|
|
|
71
90
|
puts "\n#{Commiti::TerminalUI.status(:warn, 'Edited message needs fixes:')}"
|
|
72
|
-
errors.each { |error| puts
|
|
91
|
+
errors.each { |error| puts Commiti::TerminalUI.bullet(error) }
|
|
73
92
|
return edited unless Commiti::InteractivePrompt.ask_yes_no('Re-open editor now?', default: :yes)
|
|
74
93
|
|
|
75
94
|
working = edited
|
|
@@ -11,7 +11,7 @@ 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 "\n#{Commiti::TerminalUI.
|
|
14
|
+
puts "\n#{Commiti::TerminalUI.panel('Current git status', status)}\n"
|
|
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! }
|