committer 0.2.2 → 0.4.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 +52 -24
- data/bin/committer +5 -115
- data/lib/clients/claude_client.rb +2 -2
- data/lib/committer/commands/default.rb +36 -0
- data/lib/committer/commands/help.rb +19 -0
- data/lib/committer/commands/output_message.rb +30 -0
- data/lib/committer/commands/setup.rb +17 -0
- data/lib/committer/commands/setup_git_hook.rb +63 -0
- data/lib/committer/commands.rb +26 -0
- data/lib/committer/commit_generator.rb +67 -0
- data/lib/committer/committer_errors.rb +15 -0
- data/lib/committer/config/accessor.rb +98 -0
- data/lib/committer/config/constants.rb +18 -0
- data/lib/committer/config/writer.rb +64 -0
- data/lib/committer/formatting_rules.txt +19 -0
- data/lib/committer/git_helper.rb +29 -0
- data/lib/committer/prepare-commit-msg +35 -0
- data/lib/committer/prompt_templates.rb +66 -77
- data/lib/committer/version.rb +1 -1
- metadata +16 -3
- data/lib/committer/config.rb +0 -47
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f495af607b9c66c6b3526af94a5d9792441dc84f6bf09c872586d70940ac6cb8
|
4
|
+
data.tar.gz: 3b203eedb9030d65417c74c41c2535ca76230836a612033852703476bcfc2e86
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 28b144c3f80ba381a8c3b00208d8ed92e9d8a47e7e2e21c86eb4c904699ecbd1711907a5bccd01227f6c7bb3c4a336adc199813c5590321e7d8b420891c35fb0
|
7
|
+
data.tar.gz: 597f5f6db243a56f05c285377b6b8cbaa44f1ffebc300ce2febabf7945179346a3db9d4f1afaadcb92e1ca9141cd61012dfdda2a32dfed18c72d0c52747ce71c
|
data/README.md
CHANGED
@@ -6,11 +6,12 @@ An AI-powered git commit message generator using Claude.
|
|
6
6
|
|
7
7
|
The goal of committer is to make it easier to write beautiful commits.
|
8
8
|
|
9
|
-
Committer uses Claude AI to analyze your staged git changes and generate conventional commit messages for you. It detects the type of changes (feature, fix, refactor, etc.) and creates a well-formatted commit message that follows best practices.
|
9
|
+
Committer uses Claude AI to analyze your staged git changes and generate [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages for you. It detects the type of changes (feature, fix, refactor, etc.) and creates a well-formatted commit message that follows best practices.
|
10
10
|
|
11
11
|
## What Makes a Good Commit
|
12
12
|
|
13
13
|
A good commit should:
|
14
|
+
|
14
15
|
- Have a summary that clearly describes the change
|
15
16
|
- Explain WHY the change was made, not just what changed
|
16
17
|
|
@@ -19,9 +20,10 @@ When a future developer uses git blame to understand a line of code, they should
|
|
19
20
|
## How Committer Helps
|
20
21
|
|
21
22
|
Committer analyzes your code changes and generates commit messages that:
|
23
|
+
|
22
24
|
1. Provide a clean, descriptive summary of the change
|
23
25
|
2. Include context about why the change was necessary
|
24
|
-
3. Follow conventional commit format for consistency
|
26
|
+
3. Follow [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) format for consistency
|
25
27
|
|
26
28
|
## Installation
|
27
29
|
|
@@ -31,26 +33,6 @@ Committer analyzes your code changes and generates commit messages that:
|
|
31
33
|
gem install committer
|
32
34
|
```
|
33
35
|
|
34
|
-
### Manual Installation
|
35
|
-
|
36
|
-
1. Install Bundler if you haven't already:
|
37
|
-
|
38
|
-
```sh
|
39
|
-
gem install bundler
|
40
|
-
```
|
41
|
-
|
42
|
-
2. Install the project dependencies:
|
43
|
-
|
44
|
-
```sh
|
45
|
-
bundle install
|
46
|
-
```
|
47
|
-
|
48
|
-
3. Link the executable:
|
49
|
-
```bash
|
50
|
-
gem build committer.gemspec
|
51
|
-
gem install committer-0.1.0.gem
|
52
|
-
```
|
53
|
-
|
54
36
|
## Configuration
|
55
37
|
|
56
38
|
Before using Committer, you need to set up your configuration file:
|
@@ -72,7 +54,7 @@ scopes:
|
|
72
54
|
- ui
|
73
55
|
```
|
74
56
|
|
75
|
-
The `scopes` configuration is optional. When provided, Committer will generate conventional commit messages with scopes (like `feat(api): add new endpoint`). If left as `null` or omitted, commit messages will be generated without scopes (like `feat: add new endpoint`).
|
57
|
+
The `scopes` configuration is optional. When provided, Committer will generate [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages with scopes (like `feat(api): add new endpoint`). If left as `null` or omitted, commit messages will be generated without scopes (like `feat: add new endpoint`).
|
76
58
|
|
77
59
|
You only need to do this setup once.
|
78
60
|
|
@@ -89,7 +71,7 @@ This will:
|
|
89
71
|
1. Get the diff of your staged changes
|
90
72
|
2. Ask you for optional context about why you're making the change
|
91
73
|
3. Send the diff and context to Claude for analysis
|
92
|
-
4. Generate a commit message in conventional format (with scope if configured)
|
74
|
+
4. Generate a commit message in [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) format (with scope if configured)
|
93
75
|
5. Open your default git editor with the suggested message
|
94
76
|
6. Allow you to edit the message if needed or simply save to confirm
|
95
77
|
|
@@ -98,6 +80,52 @@ This will:
|
|
98
80
|
- `committer` - Generate commit message for staged changes
|
99
81
|
- `committer setup` - Create the config file template
|
100
82
|
- `committer help` - Display help information
|
83
|
+
- `committer setup-git-hook` - Install the git hook for automatic commit message generation
|
84
|
+
- `committer output-message` - Generate a commit message (used by the git hook)
|
85
|
+
|
86
|
+
## Git Hook Integration
|
87
|
+
|
88
|
+
Committer can be integrated directly with Git using a prepare-commit-msg hook, which automatically generates commit messages whenever you commit.
|
89
|
+
|
90
|
+
### Installing the Git Hook
|
91
|
+
|
92
|
+
To install the git hook, navigate to the root of your git repository and run:
|
93
|
+
|
94
|
+
```bash
|
95
|
+
committer setup-git-hook
|
96
|
+
```
|
97
|
+
|
98
|
+
This command will:
|
99
|
+
1. Verify you're in the root of a git repository
|
100
|
+
2. Install the prepare-commit-msg hook in your `.git/hooks` directory
|
101
|
+
3. Make the hook executable
|
102
|
+
|
103
|
+
### How the Git Hook Works
|
104
|
+
|
105
|
+
Once installed, the git hook will:
|
106
|
+
1. Automatically run whenever you execute `git commit`
|
107
|
+
2. Generate an AI-powered commit message based on your staged changes
|
108
|
+
3. Pre-fill your commit message editor with the generated message
|
109
|
+
4. Allow you to edit the message before finalizing the commit
|
110
|
+
|
111
|
+
Since git hooks run in non-interactive mode, you can provide context for your commit by using the REASON environment variable:
|
112
|
+
|
113
|
+
```bash
|
114
|
+
REASON="Improve performance by optimizing database queries" git commit
|
115
|
+
```
|
116
|
+
|
117
|
+
If you don't provide a REASON, the commit message will still be generated, but without the additional context that would be used to generate a more detailed body.
|
118
|
+
|
119
|
+
## Development
|
120
|
+
|
121
|
+
### Running Tests
|
122
|
+
|
123
|
+
Committer uses RSpec for testing. To run the tests:
|
124
|
+
|
125
|
+
```bash
|
126
|
+
bundle install
|
127
|
+
bundle exec rake spec
|
128
|
+
```
|
101
129
|
|
102
130
|
## License
|
103
131
|
|
data/bin/committer
CHANGED
@@ -2,122 +2,12 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require 'bundler/setup'
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
require_relative '../lib/committer/config'
|
9
|
-
require_relative '../lib/committer/prompt_templates'
|
10
|
-
require_relative '../lib/clients/claude_client'
|
5
|
+
require_relative '../lib/committer/config/accessor'
|
6
|
+
require_relative '../lib/committer/commit_generator'
|
7
|
+
require_relative '../lib/committer/commands'
|
11
8
|
|
12
9
|
# Handle command line arguments
|
13
10
|
command = ARGV[0]
|
11
|
+
args = ARGV[1..]
|
14
12
|
|
15
|
-
|
16
|
-
when 'setup'
|
17
|
-
Committer::Config.setup
|
18
|
-
exit 0
|
19
|
-
|
20
|
-
when 'help', '--help', '-h'
|
21
|
-
puts 'Committer - AI-powered git commit message generator'
|
22
|
-
puts
|
23
|
-
puts 'Commands:'
|
24
|
-
puts ' committer setup - Create the config file template at ~/.committer/config.yml'
|
25
|
-
puts ' committer - Generate commit message for staged changes'
|
26
|
-
puts
|
27
|
-
exit 0
|
28
|
-
end
|
29
|
-
|
30
|
-
# Default behavior: generate commit message
|
31
|
-
def build_commit_prompt(diff, commit_context = nil)
|
32
|
-
scopes = Committer::Config.load
|
33
|
-
scope_section = scopes.empty? ? '' : "\nScopes:\n#{scopes.map { |s| "- #{s}" }.join("\n")}"
|
34
|
-
scope_instruction = if scopes.empty?
|
35
|
-
'- DO NOT include a scope in your commit message'
|
36
|
-
else
|
37
|
-
'- Choose an appropriate scope from the list above if relevant to the change'
|
38
|
-
end
|
39
|
-
format(template(commit_context),
|
40
|
-
diff: diff,
|
41
|
-
scopes_section: scope_section,
|
42
|
-
scope_instruction: scope_instruction,
|
43
|
-
commit_context: commit_context)
|
44
|
-
end
|
45
|
-
|
46
|
-
def template(commit_context)
|
47
|
-
if commit_context.nil? || commit_context.empty?
|
48
|
-
Committer::PromptTemplates::SUMMARY_ONLY
|
49
|
-
else
|
50
|
-
Committer::PromptTemplates::SUMMARY_AND_BODY
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def check_git_status
|
55
|
-
stdout, stderr, status = Open3.capture3('git diff --staged')
|
56
|
-
|
57
|
-
unless status.success?
|
58
|
-
puts 'Error executing git diff --staged:'
|
59
|
-
puts stderr
|
60
|
-
exit 1
|
61
|
-
end
|
62
|
-
|
63
|
-
if stdout.empty?
|
64
|
-
puts 'No changes are staged for commit.'
|
65
|
-
exit 0
|
66
|
-
end
|
67
|
-
|
68
|
-
stdout
|
69
|
-
end
|
70
|
-
|
71
|
-
def parse_response(response, commit_context)
|
72
|
-
text = response.dig('content', 0, 'text')
|
73
|
-
|
74
|
-
# If user didn't provide context, response should only be a summary line
|
75
|
-
if commit_context.nil? || commit_context.empty?
|
76
|
-
{ summary: text.strip, body: nil }
|
77
|
-
else
|
78
|
-
# Split the response into summary and body
|
79
|
-
message_parts = text.split("\n\n", 2)
|
80
|
-
summary = message_parts[0].strip
|
81
|
-
body = message_parts[1]&.strip
|
82
|
-
|
83
|
-
# Wrap body text at 80 characters
|
84
|
-
body = body.gsub(/(.{1,80})(\s+|$)/, "\\1\n").strip if body
|
85
|
-
|
86
|
-
{ summary: summary, body: body }
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
def prepare_commit_message(diff, commit_context = nil)
|
91
|
-
client = Clients::ClaudeClient.new
|
92
|
-
puts 'Sending diff to Claude...'
|
93
|
-
|
94
|
-
prompt = build_commit_prompt(diff, commit_context)
|
95
|
-
response = client.post(prompt)
|
96
|
-
parse_response(response, commit_context)
|
97
|
-
end
|
98
|
-
|
99
|
-
def execute_git_diff_staged
|
100
|
-
diff = check_git_status
|
101
|
-
|
102
|
-
# Prompt user for commit context
|
103
|
-
puts 'Why are you making this change? (Press Enter to skip)'
|
104
|
-
commit_context = gets.chomp
|
105
|
-
|
106
|
-
commit_message = prepare_commit_message(diff, commit_context)
|
107
|
-
|
108
|
-
summary = commit_message[:summary]
|
109
|
-
body = commit_message[:body]
|
110
|
-
|
111
|
-
# Create git commit with the suggested message and open in editor
|
112
|
-
if body
|
113
|
-
system('git', 'commit', '-m', summary, '-m', body, '-e')
|
114
|
-
else
|
115
|
-
system('git', 'commit', '-m', summary, '-e')
|
116
|
-
end
|
117
|
-
rescue Clients::ClaudeClient::ConfigError, StandardError => e
|
118
|
-
puts "Error: #{e.message}"
|
119
|
-
exit 1
|
120
|
-
end
|
121
|
-
|
122
|
-
# Execute the function if no specific command was given
|
123
|
-
execute_git_diff_staged
|
13
|
+
Committer::Commands.run(command, args)
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'json'
|
4
4
|
require 'httparty'
|
5
|
-
require_relative '../committer/config'
|
5
|
+
require_relative '../committer/config/accessor'
|
6
6
|
|
7
7
|
module Clients
|
8
8
|
# Claude API client for communicating with Anthropic's Claude model
|
@@ -12,7 +12,7 @@ module Clients
|
|
12
12
|
class ConfigError < StandardError; end
|
13
13
|
|
14
14
|
def initialize
|
15
|
-
@config = Committer::Config.
|
15
|
+
@config = Committer::Config::Accessor.instance
|
16
16
|
|
17
17
|
return unless @config['api_key'].nil? || @config['api_key'].empty?
|
18
18
|
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../git_helper'
|
4
|
+
|
5
|
+
module Committer
|
6
|
+
module Commands
|
7
|
+
class Default
|
8
|
+
def self.execute_commit(diff, commit_context)
|
9
|
+
commit_generator = Committer::CommitGenerator.new(diff, commit_context)
|
10
|
+
commit_message = commit_generator.prepare_commit_message
|
11
|
+
|
12
|
+
summary = commit_message[:summary]
|
13
|
+
body = commit_message[:body]
|
14
|
+
# Create git commit with the suggested message and open in editor
|
15
|
+
Committer::GitHelper.commit(summary, body)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.execute(_args)
|
19
|
+
diff = Committer::CommitGenerator.check_git_status
|
20
|
+
|
21
|
+
if diff.empty?
|
22
|
+
puts 'No changes are staged for commit.'
|
23
|
+
exit 0
|
24
|
+
end
|
25
|
+
|
26
|
+
# Prompt user for commit context
|
27
|
+
puts 'Why are you making this change? (Press Enter to skip)'
|
28
|
+
commit_context = gets.chomp
|
29
|
+
execute_commit(diff, commit_context)
|
30
|
+
rescue Clients::ClaudeClient::ConfigError, StandardError => e
|
31
|
+
puts "Error: #{e.message}"
|
32
|
+
exit 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committer
|
4
|
+
module Commands
|
5
|
+
class Help
|
6
|
+
def self.execute(_args)
|
7
|
+
puts 'Committer - AI-powered git commit message generator'
|
8
|
+
puts
|
9
|
+
puts 'Commands:'
|
10
|
+
puts ' committer setup - Create the config file template at ~/.committer/config.yml'
|
11
|
+
puts ' committer - Generate commit message for staged changes'
|
12
|
+
puts ' committer output-message - Generate commit message without creating a commit'
|
13
|
+
puts ' committer setup-git-hook - Install the prepare-commit-msg git hook'
|
14
|
+
puts
|
15
|
+
exit 0
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committer
|
4
|
+
module Commands
|
5
|
+
class OutputMessage
|
6
|
+
def self.execute(args)
|
7
|
+
commit_context = args[0]
|
8
|
+
diff = Committer::CommitGenerator.check_git_status
|
9
|
+
|
10
|
+
commit_generator = Committer::CommitGenerator.new(diff, commit_context)
|
11
|
+
commit_message = commit_generator.prepare_commit_message
|
12
|
+
|
13
|
+
summary = commit_message[:summary]
|
14
|
+
body = commit_message[:body]
|
15
|
+
|
16
|
+
puts <<~OUTPUT
|
17
|
+
#{summary}
|
18
|
+
|
19
|
+
#{body}
|
20
|
+
OUTPUT
|
21
|
+
exit 0
|
22
|
+
rescue Clients::ClaudeClient::ConfigError => e
|
23
|
+
puts "Error: #{e.message}"
|
24
|
+
exit 1
|
25
|
+
rescue StandardError
|
26
|
+
exit 0
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../config/writer'
|
4
|
+
require_relative '../config/constants'
|
5
|
+
|
6
|
+
module Committer
|
7
|
+
module Commands
|
8
|
+
class Setup
|
9
|
+
def self.execute(_args)
|
10
|
+
config_dir = Committer::Config::Constants::CONFIG_DIR
|
11
|
+
writer = Committer::Config::Writer.new(config_dir)
|
12
|
+
writer.setup
|
13
|
+
exit 0
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../git_helper'
|
4
|
+
|
5
|
+
module Committer
|
6
|
+
module Commands
|
7
|
+
class SetupGitHook
|
8
|
+
HOOK_PATH = '.git/hooks/prepare-commit-msg'
|
9
|
+
|
10
|
+
def self.execute(_args)
|
11
|
+
validate_git_repository
|
12
|
+
install_git_hook
|
13
|
+
display_success_message
|
14
|
+
exit 0
|
15
|
+
rescue StandardError => e
|
16
|
+
puts "Error: #{e.message}"
|
17
|
+
exit 1
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.validate_git_repository
|
21
|
+
validate_git_root
|
22
|
+
validate_git_directory_exists
|
23
|
+
validate_hook_doesnt_exist
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.validate_git_directory_exists
|
27
|
+
return if Dir.exist?('.git')
|
28
|
+
|
29
|
+
puts 'Error: Current directory is not a git repository.'
|
30
|
+
exit 1
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.validate_git_root
|
34
|
+
git_toplevel = Committer::GitHelper.repo_root
|
35
|
+
current_dir = Dir.pwd
|
36
|
+
return if git_toplevel == current_dir
|
37
|
+
|
38
|
+
puts 'Error: Please run this command from the root of your git repository.'
|
39
|
+
exit 1
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.validate_hook_doesnt_exist
|
43
|
+
return unless File.exist?(HOOK_PATH)
|
44
|
+
|
45
|
+
puts 'Error: prepare-commit-msg hook already exists.'
|
46
|
+
puts 'Please remove or rename the existing hook and try again.'
|
47
|
+
exit 1
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.install_git_hook
|
51
|
+
template_path = File.expand_path('../prepare-commit-msg', __dir__)
|
52
|
+
hook_content = File.read(template_path)
|
53
|
+
File.write(HOOK_PATH, hook_content)
|
54
|
+
File.chmod(0o755, HOOK_PATH)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.display_success_message
|
58
|
+
puts 'Git hook successfully installed!'
|
59
|
+
puts 'Now your commit messages will be automatically generated.'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'commands/setup'
|
4
|
+
require_relative 'commands/help'
|
5
|
+
require_relative 'commands/output_message'
|
6
|
+
require_relative 'commands/setup_git_hook'
|
7
|
+
require_relative 'commands/default'
|
8
|
+
|
9
|
+
module Committer
|
10
|
+
module Commands
|
11
|
+
def self.run(command, args)
|
12
|
+
case command
|
13
|
+
when 'setup'
|
14
|
+
Setup.execute(args)
|
15
|
+
when 'help', '--help', '-h'
|
16
|
+
Help.execute(args)
|
17
|
+
when 'output-message'
|
18
|
+
OutputMessage.execute(args)
|
19
|
+
when 'setup-git-hook'
|
20
|
+
SetupGitHook.execute(args)
|
21
|
+
else
|
22
|
+
Default.execute(args)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
require 'yaml'
|
5
|
+
require_relative 'config/accessor'
|
6
|
+
require_relative 'prompt_templates'
|
7
|
+
require_relative '../clients/claude_client'
|
8
|
+
require_relative 'git_helper'
|
9
|
+
|
10
|
+
module Committer
|
11
|
+
class CommitGenerator
|
12
|
+
attr_reader :diff, :commit_context
|
13
|
+
|
14
|
+
def initialize(diff, commit_context = nil)
|
15
|
+
@diff = diff
|
16
|
+
@commit_context = commit_context
|
17
|
+
end
|
18
|
+
|
19
|
+
def build_commit_prompt
|
20
|
+
format(template,
|
21
|
+
diff: @diff,
|
22
|
+
commit_context: @commit_context)
|
23
|
+
end
|
24
|
+
|
25
|
+
def template
|
26
|
+
if @commit_context.nil? || @commit_context.empty?
|
27
|
+
Committer::PromptTemplates.build_prompt_summary_only
|
28
|
+
else
|
29
|
+
Committer::PromptTemplates.build_prompt_summary_and_body
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.check_git_status
|
34
|
+
Committer::GitHelper.staged_diff
|
35
|
+
rescue Committer::Error => e
|
36
|
+
puts e.message
|
37
|
+
exit 1
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_response(response)
|
41
|
+
text = response.dig('content', 0, 'text')
|
42
|
+
|
43
|
+
# If user didn't provide context, response should only be a summary line
|
44
|
+
if @commit_context.nil? || @commit_context.empty?
|
45
|
+
{ summary: text.strip, body: nil }
|
46
|
+
else
|
47
|
+
# Split the response into summary and body
|
48
|
+
message_parts = text.split("\n\n", 2)
|
49
|
+
summary = message_parts[0].strip
|
50
|
+
body = message_parts[1]&.strip
|
51
|
+
|
52
|
+
# Wrap body text at 80 characters
|
53
|
+
body = body.gsub(/(.{1,80})(\s+|$)/, "\\1\n").strip if body
|
54
|
+
|
55
|
+
{ summary: summary, body: body }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def prepare_commit_message
|
60
|
+
client = Clients::ClaudeClient.new
|
61
|
+
|
62
|
+
prompt = build_commit_prompt
|
63
|
+
response = client.post(prompt)
|
64
|
+
parse_response(response)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committer
|
4
|
+
# Base error class for all Committer errors
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
# Configuration management for the Committer gem
|
8
|
+
class ConfigErrors
|
9
|
+
class BaseError < StandardError; end
|
10
|
+
|
11
|
+
# Request Processing Errors
|
12
|
+
class FormatError < BaseError; end
|
13
|
+
class NotSetup < BaseError; end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'singleton'
|
5
|
+
require_relative '../committer_errors'
|
6
|
+
require_relative 'constants'
|
7
|
+
require_relative '../git_helper'
|
8
|
+
|
9
|
+
module Committer
|
10
|
+
# Configuration management for the Committer gem
|
11
|
+
module Config
|
12
|
+
class Accessor
|
13
|
+
include Singleton
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@config = load_config
|
17
|
+
end
|
18
|
+
|
19
|
+
# Accessor for the loaded config
|
20
|
+
def [](key)
|
21
|
+
@config[key.to_sym] || @config[key.to_s]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Get the entire config hash
|
25
|
+
def to_h
|
26
|
+
@config.dup
|
27
|
+
end
|
28
|
+
|
29
|
+
def load_config
|
30
|
+
# Load configs from both locations and merge them
|
31
|
+
home_config = load_config_from_path(Committer::Config::Constants::CONFIG_FILE)
|
32
|
+
git_root_config = load_config_from_git_root
|
33
|
+
raise Committer::ConfigErrors::NotSetup if home_config.empty? && git_root_config.empty?
|
34
|
+
|
35
|
+
# Merge configs with git root taking precedence
|
36
|
+
home_config.merge(git_root_config)
|
37
|
+
end
|
38
|
+
|
39
|
+
def load_config_from_path(path)
|
40
|
+
return {} unless File.exist?(path)
|
41
|
+
|
42
|
+
result = YAML.load_file(path)
|
43
|
+
raise Committer::ConfigErrors::FormatError, 'Config file must be a YAML hash' unless result.is_a?(Hash)
|
44
|
+
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
def load_file_from_path(path)
|
49
|
+
return '' unless File.exist?(path)
|
50
|
+
|
51
|
+
File.read(path)
|
52
|
+
end
|
53
|
+
|
54
|
+
def load_formatting_rules
|
55
|
+
git_root = Committer::GitHelper.repo_root
|
56
|
+
unless git_root.empty?
|
57
|
+
formatting_rules_git_path = File.join(git_root, '.committer',
|
58
|
+
Committer::Config::Constants::FORMATTING_RULES_FILE_NAME)
|
59
|
+
end
|
60
|
+
|
61
|
+
git_path_contents = load_file_from_path(formatting_rules_git_path) if formatting_rules_git_path
|
62
|
+
|
63
|
+
return git_path_contents unless git_path_contents.empty?
|
64
|
+
|
65
|
+
home_path = File.join(Committer::Config::Constants::CONFIG_DIR,
|
66
|
+
Committer::Config::Constants::FORMATTING_RULES_FILE_NAME)
|
67
|
+
|
68
|
+
home_path_contents = load_file_from_path(home_path)
|
69
|
+
|
70
|
+
return home_path_contents unless home_path_contents.empty?
|
71
|
+
|
72
|
+
default_path = File.join(Committer::Config::Constants::DEFAULT_PROMPT_PATH,
|
73
|
+
Committer::Config::Constants::FORMATTING_RULES_FILE_NAME)
|
74
|
+
load_file_from_path(default_path)
|
75
|
+
end
|
76
|
+
|
77
|
+
def load_config_from_git_root
|
78
|
+
git_root = Committer::GitHelper.repo_root
|
79
|
+
return {} if git_root.empty?
|
80
|
+
|
81
|
+
git_config_file = File.join(git_root, '.committer', 'config.yml')
|
82
|
+
load_config_from_path(git_config_file)
|
83
|
+
rescue StandardError
|
84
|
+
{}
|
85
|
+
end
|
86
|
+
|
87
|
+
# Force reload configuration (useful for testing)
|
88
|
+
def reload
|
89
|
+
@config = load_config
|
90
|
+
end
|
91
|
+
|
92
|
+
# Class method for reload
|
93
|
+
def self.reload
|
94
|
+
instance.reload
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Committer
|
4
|
+
module Config
|
5
|
+
module Constants
|
6
|
+
CONFIG_DIR = File.join(Dir.home, '.committer')
|
7
|
+
CONFIG_FILE = File.join(CONFIG_DIR, 'config.yml')
|
8
|
+
FORMATTING_RULES_FILE_NAME = 'formatting_rules.txt'
|
9
|
+
DEFAULT_PROMPT_PATH = File.join(File.dirname(__FILE__), '..')
|
10
|
+
CONFIG_FILE_NAME = 'config.yml'
|
11
|
+
DEFAULT_CONFIG = {
|
12
|
+
'api_key' => nil,
|
13
|
+
'model' => 'claude-3-7-sonnet-20250219',
|
14
|
+
'scopes' => nil
|
15
|
+
}.freeze
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'fileutils'
|
5
|
+
require_relative '../committer_errors'
|
6
|
+
require_relative 'constants'
|
7
|
+
|
8
|
+
module Committer
|
9
|
+
module Config
|
10
|
+
class Writer
|
11
|
+
attr_reader :config_dir
|
12
|
+
|
13
|
+
def initialize(config_dir)
|
14
|
+
@config_dir = config_dir
|
15
|
+
end
|
16
|
+
|
17
|
+
def config_file
|
18
|
+
File.join(@config_dir, Committer::Config::Constants::CONFIG_FILE_NAME)
|
19
|
+
end
|
20
|
+
|
21
|
+
def setup
|
22
|
+
create_default_config
|
23
|
+
create_sample_formatting_rules
|
24
|
+
end
|
25
|
+
|
26
|
+
def write_config_file(file_path, contents)
|
27
|
+
FileUtils.mkdir_p(@config_dir)
|
28
|
+
if File.exist?(file_path)
|
29
|
+
puts "Config file already exists at #{config_file}, skipping write"
|
30
|
+
false
|
31
|
+
else
|
32
|
+
File.write(file_path, contents)
|
33
|
+
true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_sample_formatting_rules
|
38
|
+
default_formatting_rules = File.read(File.join(Committer::Config::Constants::DEFAULT_PROMPT_PATH,
|
39
|
+
Committer::Config::Constants::FORMATTING_RULES_FILE_NAME))
|
40
|
+
formatting_rules_file = File.join(@config_dir,
|
41
|
+
"#{Committer::Config::Constants::FORMATTING_RULES_FILE_NAME}.sample")
|
42
|
+
wrote_file = write_config_file(formatting_rules_file, default_formatting_rules)
|
43
|
+
nil unless wrote_file
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_default_config
|
47
|
+
wrote_file = write_config_file(config_file, Committer::Config::Constants::DEFAULT_CONFIG.to_yaml)
|
48
|
+
return unless wrote_file
|
49
|
+
|
50
|
+
puts 'Created config file at:'
|
51
|
+
puts config_file
|
52
|
+
puts "\nPlease edit this file to add your Anthropic API key."
|
53
|
+
puts 'Example config format:'
|
54
|
+
puts '---'
|
55
|
+
puts 'api_key: your_api_key_here'
|
56
|
+
puts 'model: claude-3-7-sonnet-20250219'
|
57
|
+
puts 'scopes:'
|
58
|
+
puts ' - feature'
|
59
|
+
puts ' - api'
|
60
|
+
puts ' - ui'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# Formatting rules for message
|
2
|
+
Message should be formatted according to conventional commits
|
3
|
+
|
4
|
+
## When scopes are available:
|
5
|
+
<type>(<scope>): <description>
|
6
|
+
|
7
|
+
# Formatting Rules
|
8
|
+
## When scopes are not available:
|
9
|
+
<type>: <description>
|
10
|
+
|
11
|
+
# Types:
|
12
|
+
- feat: A new feature
|
13
|
+
- fix: A bug fix
|
14
|
+
- docs: Documentation only changes
|
15
|
+
- style: Changes that do not affect the meaning of the code
|
16
|
+
- refactor: A code change that neither fixes a bug nor adds a feature
|
17
|
+
- perf: A code change that improves performance
|
18
|
+
- test: Adding missing tests or correcting existing tests
|
19
|
+
- chore: Changes to the build process or auxiliary tools
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
|
5
|
+
module Committer
|
6
|
+
# Helper class for git operations
|
7
|
+
class GitHelper
|
8
|
+
class << self
|
9
|
+
def commit(summary, body = nil)
|
10
|
+
if body
|
11
|
+
system('git', 'commit', '-m', summary, '-m', body, '-e')
|
12
|
+
else
|
13
|
+
system('git', 'commit', '-m', summary, '-e')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def repo_root
|
18
|
+
`git rev-parse --show-toplevel`.strip
|
19
|
+
end
|
20
|
+
|
21
|
+
def staged_diff
|
22
|
+
stdout, stderr, status = Open3.capture3('git diff --staged')
|
23
|
+
raise Committer::Error, "Failed to get git diff: #{stderr}" unless status.success?
|
24
|
+
|
25
|
+
stdout
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
# Check if parameters 2 and 3 are null
|
4
|
+
if [ -z "$2" ] && [ -z "$3" ]; then
|
5
|
+
# Check if parameter 1 is provided and is a valid file
|
6
|
+
if [ -z "$1" ]; then
|
7
|
+
echo "Error: No target file specified in parameter 1"
|
8
|
+
exit 1
|
9
|
+
fi
|
10
|
+
|
11
|
+
if [ ! -f "$1" ]; then
|
12
|
+
echo "Error: File '$1' does not exist"
|
13
|
+
exit 1
|
14
|
+
fi
|
15
|
+
|
16
|
+
# Check if committer command exists
|
17
|
+
if command -v committer &> /dev/null; then
|
18
|
+
# Execute committer command and capture its output
|
19
|
+
COMMITTER_OUTPUT=$(committer output-message "$REASON")
|
20
|
+
|
21
|
+
# Create a temporary file with the committer output at the top
|
22
|
+
# followed by the original content of the file
|
23
|
+
{
|
24
|
+
echo "$COMMITTER_OUTPUT"
|
25
|
+
cat "$1"
|
26
|
+
} > "$1.tmp"
|
27
|
+
|
28
|
+
# Replace the original file with the temporary file
|
29
|
+
mv "$1.tmp" "$1"
|
30
|
+
|
31
|
+
echo "Successfully added committer output to the top of $1"
|
32
|
+
else
|
33
|
+
echo "Committer command not found - no action taken"
|
34
|
+
fi
|
35
|
+
fi
|
@@ -2,82 +2,71 @@
|
|
2
2
|
|
3
3
|
module Committer
|
4
4
|
module PromptTemplates
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
%<scope_instruction>s
|
72
|
-
|
73
|
-
User's context for this change: %<commit_context>s
|
74
|
-
|
75
|
-
Git Diff:
|
76
|
-
```
|
77
|
-
%<diff>s
|
78
|
-
```
|
79
|
-
|
80
|
-
Respond ONLY with the commit message text (summary and body), nothing else.
|
81
|
-
PROMPT
|
5
|
+
FORMATTING_RULES_PATH = File.join(File.dirname(__FILE__), 'formatting_rules.txt')
|
6
|
+
|
7
|
+
def self.load_formatting_rules
|
8
|
+
File.read(FORMATTING_RULES_PATH)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load_scopes
|
12
|
+
scopes = Committer::Config::Accessor.instance[:scopes] || []
|
13
|
+
return 'DO NOT include a scope in your commit message' if scopes.empty?
|
14
|
+
|
15
|
+
scope_list = "\nScopes:\n#{scopes.map { |s| "- #{s}" }.join("\n")}"
|
16
|
+
|
17
|
+
"- Choose an appropriate scope from the list above if relevant to the change \n#{scope_list}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.commit_message_guidelines
|
21
|
+
<<~PROMPT
|
22
|
+
#{load_formatting_rules}
|
23
|
+
|
24
|
+
# Formatting rules with body:
|
25
|
+
<message>
|
26
|
+
|
27
|
+
<blank line>
|
28
|
+
<body with more detailed explanation>
|
29
|
+
|
30
|
+
#{load_scopes}
|
31
|
+
|
32
|
+
# Message Guidelines:
|
33
|
+
- Keep the summary under 70 characters
|
34
|
+
- Use imperative, present tense (e.g., "add" not "added" or "adds")
|
35
|
+
- Do not end the summary with a period
|
36
|
+
- Be concise but descriptive in the summary
|
37
|
+
|
38
|
+
# Body Guidelines:
|
39
|
+
- Add a blank line between summary and body
|
40
|
+
- Use the body to explain why the change was made, incorporating the user's context
|
41
|
+
- Wrap each line in the body at 80 characters maximum
|
42
|
+
- Break the body into multiple paragraphs if needed
|
43
|
+
|
44
|
+
Git Diff:
|
45
|
+
```
|
46
|
+
%<diff>s
|
47
|
+
```
|
48
|
+
PROMPT
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.build_prompt_summary_only
|
52
|
+
<<~PROMPT
|
53
|
+
Below is a git diff of staged changes. Please analyze it and create a commit message following the formatting rules format with ONLY a message line (NO body):
|
54
|
+
|
55
|
+
#{commit_message_guidelines}
|
56
|
+
|
57
|
+
Respond ONLY with the commit message line, nothing else.
|
58
|
+
PROMPT
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.build_prompt_summary_and_body
|
62
|
+
<<~PROMPT
|
63
|
+
Below is a git diff of staged changes. Please analyze it and create a commit message following the formatting rules format with a summary line and a detailed body:
|
64
|
+
|
65
|
+
#{commit_message_guidelines}
|
66
|
+
User's context for this change: %<commit_context>s
|
67
|
+
|
68
|
+
Respond ONLY with the commit message text (message and body), nothing else.
|
69
|
+
PROMPT
|
70
|
+
end
|
82
71
|
end
|
83
72
|
end
|
data/lib/committer/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: committer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sebastien Stettler
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-03-
|
11
|
+
date: 2025-03-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: httparty
|
@@ -36,7 +36,20 @@ files:
|
|
36
36
|
- README.md
|
37
37
|
- bin/committer
|
38
38
|
- lib/clients/claude_client.rb
|
39
|
-
- lib/committer/
|
39
|
+
- lib/committer/commands.rb
|
40
|
+
- lib/committer/commands/default.rb
|
41
|
+
- lib/committer/commands/help.rb
|
42
|
+
- lib/committer/commands/output_message.rb
|
43
|
+
- lib/committer/commands/setup.rb
|
44
|
+
- lib/committer/commands/setup_git_hook.rb
|
45
|
+
- lib/committer/commit_generator.rb
|
46
|
+
- lib/committer/committer_errors.rb
|
47
|
+
- lib/committer/config/accessor.rb
|
48
|
+
- lib/committer/config/constants.rb
|
49
|
+
- lib/committer/config/writer.rb
|
50
|
+
- lib/committer/formatting_rules.txt
|
51
|
+
- lib/committer/git_helper.rb
|
52
|
+
- lib/committer/prepare-commit-msg
|
40
53
|
- lib/committer/prompt_templates.rb
|
41
54
|
- lib/committer/version.rb
|
42
55
|
homepage: https://github.com/Hyper-Unearthing/committer
|
data/lib/committer/config.rb
DELETED
@@ -1,47 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'yaml'
|
4
|
-
require 'fileutils'
|
5
|
-
|
6
|
-
module Committer
|
7
|
-
# Configuration management for the Committer gem
|
8
|
-
class Config
|
9
|
-
CONFIG_DIR = File.join(Dir.home, '.committer')
|
10
|
-
CONFIG_FILE = File.join(CONFIG_DIR, 'config.yml')
|
11
|
-
DEFAULT_CONFIG = {
|
12
|
-
'api_key' => nil,
|
13
|
-
'model' => 'claude-3-7-sonnet-20250219',
|
14
|
-
'scopes' => nil
|
15
|
-
}.freeze
|
16
|
-
|
17
|
-
def self.load
|
18
|
-
create_default_config unless File.exist?(CONFIG_FILE)
|
19
|
-
begin
|
20
|
-
YAML.load_file(CONFIG_FILE) || DEFAULT_CONFIG
|
21
|
-
rescue StandardError => e
|
22
|
-
puts "Error loading config: #{e.message}"
|
23
|
-
DEFAULT_CONFIG
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def self.create_default_config
|
28
|
-
FileUtils.mkdir_p(CONFIG_DIR)
|
29
|
-
File.write(CONFIG_FILE, DEFAULT_CONFIG.to_yaml)
|
30
|
-
end
|
31
|
-
|
32
|
-
def self.setup
|
33
|
-
create_default_config
|
34
|
-
puts 'Created config file at:'
|
35
|
-
puts CONFIG_FILE
|
36
|
-
puts "\nPlease edit this file to add your Anthropic API key."
|
37
|
-
puts 'Example config format:'
|
38
|
-
puts '---'
|
39
|
-
puts 'api_key: your_api_key_here'
|
40
|
-
puts 'model: claude-3-7-sonnet-20250219'
|
41
|
-
puts 'scopes:'
|
42
|
-
puts ' - feature'
|
43
|
-
puts ' - api'
|
44
|
-
puts ' - ui'
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|