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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19704907042388baa2b8ede51ebe80710b9cbe5f6095fd09b0fb7cb997496382
4
- data.tar.gz: ecaab24d0b58301c41a9677cf09fd1ae5ef23c3930c911c6cdf9820abacc52f9
3
+ metadata.gz: f495af607b9c66c6b3526af94a5d9792441dc84f6bf09c872586d70940ac6cb8
4
+ data.tar.gz: 3b203eedb9030d65417c74c41c2535ca76230836a612033852703476bcfc2e86
5
5
  SHA512:
6
- metadata.gz: 1a6eb3eaabe0723082df95e113c7cbeb47521a16839ae04cdf6bfa146a3747b68b77aeac2462954220eeefd2cf709d73a8e2df9ab63b6b265d3c9f4304706357
7
- data.tar.gz: 1c98170b2c2b194bb739c96278e3150e005d1f00df9c69c1c21219b26e7f2511d710894eb1001dfe973103e23efa22e91aabaa0f27fd06e53387aeca1b10e626
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
- require 'open3'
6
- require 'httparty'
7
- require 'yaml'
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
- case command
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.load
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
- SUMMARY_ONLY = <<~PROMPT
6
- Below is a git diff of staged changes. Please analyze it and create a commit message following the Conventional Commits format with ONLY a summary line (NO body):
7
-
8
- Format when scopes are available: <type>(<scope>): <description>
9
- Format when no scopes are available: <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
20
- %<scopes_section>s
21
- Guidelines:
22
- - Keep the summary under 70 characters
23
- - Use imperative, present tense (e.g., "add" not "added" or "adds")
24
- - Do not end the summary with a period
25
- - Be concise but descriptive in the summary
26
- %<scope_instruction>s
27
-
28
- Git Diff:
29
- ```
30
- %<diff>s
31
- ```
32
-
33
- Respond ONLY with the commit message summary line, nothing else.
34
- PROMPT
35
-
36
- SUMMARY_AND_BODY = <<~PROMPT
37
- Below is a git diff of staged changes. Please analyze it and create a commit message following the Conventional Commits format with a summary line and a detailed body:
38
-
39
- Format when scopes are available:
40
- <type>(<scope>): <description>
41
-
42
- <blank line>
43
- <body with more detailed explanation>
44
-
45
- Format when no scopes are available:
46
- <type>: <description>
47
-
48
- <blank line>
49
- <body with more detailed explanation>
50
-
51
-
52
- Types:
53
- - feat: A new feature
54
- - fix: A bug fix
55
- - docs: Documentation only changes
56
- - style: Changes that do not affect the meaning of the code
57
- - refactor: A code change that neither fixes a bug nor adds a feature
58
- - perf: A code change that improves performance
59
- - test: Adding missing tests or correcting existing tests
60
- - chore: Changes to the build process or auxiliary tools
61
- %<scopes_section>s
62
- Guidelines:
63
- - Keep the first line (summary) under 70 characters
64
- - Use imperative, present tense (e.g., "add" not "added" or "adds")
65
- - Do not end the summary with a period
66
- - Be concise but descriptive in the summary
67
- - Add a blank line between summary and body
68
- - Use the body to explain why the change was made, incorporating the user's context
69
- - Wrap each line in the body at 80 characters maximum
70
- - Break the body into multiple paragraphs if needed
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Committer
4
- VERSION = '0.2.2'
4
+ VERSION = '0.4.0'
5
5
  end
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.2.2
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-06 00:00:00.000000000 Z
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/config.rb
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
@@ -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