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.
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Commiti
4
+ module TextGenerationStyle
5
+ DEFAULT_PR_SECTIONS = [
6
+ { name: 'Summary', guidance: 'Summarize the change in one concise paragraph.' },
7
+ { name: 'Motivation', guidance: 'Explain why this change is needed.' },
8
+ { name: 'Changes Made', guidance: 'List the concrete changes made in the diff.' },
9
+ { name: 'Testing Notes', guidance: 'Describe the tests, checks, or verification performed.' }
10
+ ].freeze
11
+
12
+ DEFAULT_CONFIG = {
13
+ commit: {
14
+ subject_case: 'preserve'
15
+ },
16
+ pr: {
17
+ sections: DEFAULT_PR_SECTIONS
18
+ }
19
+ }.freeze
20
+
21
+ ALLOWED_COMMIT_SUBJECT_CASES = %w[preserve uppercase lowercase].freeze
22
+
23
+ def self.normalize(raw_config)
24
+ raw_hash = raw_config.is_a?(Hash) ? raw_config : {}
25
+ style_hash = lookup(raw_hash, :text_generation) || lookup(raw_hash, :generation) || raw_hash
26
+
27
+ {
28
+ commit: {
29
+ subject_case: normalize_subject_case(lookup(lookup(style_hash, :commit), :subject_case))
30
+ },
31
+ pr: {
32
+ sections: normalize_pr_sections(lookup(lookup(style_hash, :pr), :sections))
33
+ }
34
+ }
35
+ end
36
+
37
+ def self.commit_subject_case(style_config)
38
+ lookup(lookup(style_config, :commit), :subject_case) || DEFAULT_CONFIG[:commit][:subject_case]
39
+ end
40
+
41
+ def self.commit_subject_case_instruction(style_config)
42
+ case commit_subject_case(style_config)
43
+ when 'uppercase'
44
+ 'Capitalize the first alphabetic character in the subject line.'
45
+ when 'lowercase'
46
+ 'Keep the first alphabetic character in the subject line lowercase.'
47
+ else
48
+ 'Preserve the natural casing chosen by the content of the change.'
49
+ end
50
+ end
51
+
52
+ def self.apply_commit_subject_case(subject, style_config)
53
+ normalized_subject = subject.to_s
54
+
55
+ case commit_subject_case(style_config)
56
+ when 'uppercase'
57
+ normalized_subject.sub(/\A([[:alpha:]])/) { Regexp.last_match(1).upcase }
58
+ when 'lowercase'
59
+ normalized_subject.sub(/\A([[:alpha:]])/) { Regexp.last_match(1).downcase }
60
+ else
61
+ normalized_subject
62
+ end
63
+ end
64
+
65
+ def self.pr_sections(style_config)
66
+ sections = lookup(lookup(style_config, :pr), :sections)
67
+ sections = DEFAULT_PR_SECTIONS if sections.nil? || sections.empty?
68
+ sections
69
+ end
70
+
71
+ def self.pr_section_headers(style_config)
72
+ pr_sections(style_config).map { |section| "## #{section[:name]}" }
73
+ end
74
+
75
+ def self.pr_section_prompt_lines(style_config)
76
+ pr_sections(style_config).each_with_index.map do |section, index|
77
+ guidance = section[:guidance] || default_pr_section_guidance(index, section[:name])
78
+ " - ## #{section[:name]}\n #{guidance}"
79
+ end.join("\n")
80
+ end
81
+
82
+ def self.first_pr_section_header(style_config)
83
+ pr_section_headers(style_config).first
84
+ end
85
+
86
+ def self.normalize_subject_case(value)
87
+ normalized = string_value(value)
88
+ ALLOWED_COMMIT_SUBJECT_CASES.include?(normalized) ? normalized : DEFAULT_CONFIG[:commit][:subject_case]
89
+ end
90
+ private_class_method :normalize_subject_case
91
+
92
+ def self.normalize_pr_sections(value)
93
+ sections = Array(value).filter_map { |section| normalize_section(section) }
94
+ sections.empty? ? DEFAULT_PR_SECTIONS : sections
95
+ end
96
+ private_class_method :normalize_pr_sections
97
+
98
+ def self.normalize_section(section)
99
+ case section
100
+ when String
101
+ name = string_value(section)
102
+ return nil if name.empty?
103
+
104
+ { name: name, guidance: nil }
105
+ when Hash
106
+ # Strict schema: only accept `name` and `guidance` keys
107
+ allowed_keys = %w[name guidance]
108
+ provided_keys = section.keys.map(&:to_s)
109
+ return nil unless (provided_keys - allowed_keys).empty?
110
+
111
+ name = string_value(lookup(section, :name))
112
+ return nil if name.empty?
113
+
114
+ guidance_raw = lookup(section, :guidance)
115
+ guidance = sanitize_guidance(guidance_raw)
116
+ { name: name, guidance: guidance.empty? ? nil : guidance }
117
+ end
118
+ end
119
+ private_class_method :normalize_section
120
+
121
+ def self.sanitize_guidance(raw)
122
+ return '' if raw.nil?
123
+
124
+ text = raw.to_s.dup
125
+ # Remove code fences and backtick blocks
126
+ text.gsub!(/```.*?```/m, ' ')
127
+ text.gsub!(/`.*?`/, ' ')
128
+ # Remove HTML tags
129
+ text.gsub!(/<[^>]+>/, ' ')
130
+ # Neutralize suspicious phrases that look like instructions
131
+ suspicious = Regexp.union(
132
+ /ignore previous/i,
133
+ /disregard previous/i,
134
+ /do not follow/i,
135
+ /only follow/i,
136
+ /override previous/i,
137
+ /ignore all previous instructions/i,
138
+ /follow only/i
139
+ )
140
+ text.gsub!(suspicious, '[redacted]')
141
+ # Collapse whitespace and truncate
142
+ text = text.strip.gsub(/ +/, ' ').gsub(/\s+/, ' ')
143
+ max = 300
144
+ text[0, max]
145
+ end
146
+ private_class_method :sanitize_guidance
147
+
148
+ def self.default_pr_section_guidance(index, name)
149
+ defaults = [
150
+ 'Summarize the overall change and the problem it solves.',
151
+ 'Explain the motivation or context behind the change.',
152
+ 'List the concrete code, docs, or workflow updates.',
153
+ 'Describe the verification performed or why it was not needed.'
154
+ ]
155
+
156
+ defaults[index] || "Describe the #{name} aspect of the change using concrete details from the diff."
157
+ end
158
+ private_class_method :default_pr_section_guidance
159
+
160
+ def self.lookup(hash, key)
161
+ return nil unless hash.is_a?(Hash)
162
+
163
+ hash[key] || hash[key.to_s] || hash[key.to_sym]
164
+ end
165
+ private_class_method :lookup
166
+
167
+ def self.string_value(value)
168
+ value.to_s.strip
169
+ end
170
+ private_class_method :string_value
171
+ end
172
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: commiti
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Setoju
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-19 00:00:00.000000000 Z
11
+ date: 2026-05-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dotenv
@@ -38,8 +38,8 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.21'
41
- description: Generates git commit messages and PR descriptions using Google AI text
42
- generation models. Supports GitHub, GitLab, and GitBucket with prefilled PR/MR forms.
41
+ description: 'Generates git commit messages and PR descriptions using Google AI text
42
+ generation models. '
43
43
  email:
44
44
  - setoju48@gmail.com
45
45
  executables:
@@ -64,15 +64,21 @@ files:
64
64
  - lib/services/git/diff_parser.rb
65
65
  - lib/services/git/git_reader.rb
66
66
  - lib/services/git/git_writer.rb
67
+ - lib/services/git/pr/browser_opener.rb
68
+ - lib/services/git/pr/pr_creator.rb
67
69
  - lib/services/git/pr/pr_opener.rb
70
+ - lib/services/git/pr/remote_parser.rb
68
71
  - lib/services/google_client.rb
69
72
  - lib/services/helpers/clipboard.rb
70
73
  - lib/services/helpers/config_loader.rb
71
74
  - lib/services/helpers/interactive_prompt.rb
72
75
  - lib/services/helpers/prompt_builder.rb
73
76
  - lib/services/helpers/spinner.rb
77
+ - lib/services/helpers/terminal_ui.rb
74
78
  - lib/services/message_generator.rb
79
+ - lib/services/message_generator_support.rb
75
80
  - lib/services/message_presenter.rb
81
+ - lib/services/text_generation_style.rb
76
82
  homepage: https://github.com/setoju/commiti
77
83
  licenses:
78
84
  - MIT