committer 0.2.2 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19704907042388baa2b8ede51ebe80710b9cbe5f6095fd09b0fb7cb997496382
4
- data.tar.gz: ecaab24d0b58301c41a9677cf09fd1ae5ef23c3930c911c6cdf9820abacc52f9
3
+ metadata.gz: 9dce742b36edfa9cef8ad7811bccfef3ca049a91a92cd05264b4004423a1afb9
4
+ data.tar.gz: 50c8ff360e6905279969af7537e2b6e4cd4f918fec61a65ec1b9333e943906e0
5
5
  SHA512:
6
- metadata.gz: 1a6eb3eaabe0723082df95e113c7cbeb47521a16839ae04cdf6bfa146a3747b68b77aeac2462954220eeefd2cf709d73a8e2df9ab63b6b265d3c9f4304706357
7
- data.tar.gz: 1c98170b2c2b194bb739c96278e3150e005d1f00df9c69c1c21219b26e7f2511d710894eb1001dfe973103e23efa22e91aabaa0f27fd06e53387aeca1b10e626
6
+ metadata.gz: a9b64954d73434cb9c095b32426f673fe544d23d1d1b0bf74da1ab29b700f77e1a7b3b2d5cd9d571d4e005b706d0369f08666317b521996d18145c6fd2224f6a
7
+ data.tar.gz: e453d6be97c3ab39aa2fc19c13236477d3b6aed625841e2e2d6035d435adda196ab83353e4763788858e0e4ee328a6f01fd8423412b86679a373648f1b776f45
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
5
  require_relative '../lib/committer/config'
9
- require_relative '../lib/committer/prompt_templates'
10
- require_relative '../lib/clients/claude_client'
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)
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committer
4
+ module Commands
5
+ class Default
6
+ def self.execute_commit(diff, commit_context)
7
+ commit_generator = Committer::CommitGenerator.new(diff, commit_context)
8
+ commit_message = commit_generator.prepare_commit_message
9
+
10
+ summary = commit_message[:summary]
11
+ body = commit_message[:body]
12
+ # Create git commit with the suggested message and open in editor
13
+ if body
14
+ system('git', 'commit', '-m', summary, '-m', body, '-e')
15
+ else
16
+ system('git', 'commit', '-m', summary, '-e')
17
+ end
18
+ end
19
+
20
+ def self.execute(_args)
21
+ diff = Committer::CommitGenerator.check_git_status
22
+
23
+ if diff.empty?
24
+ puts 'No changes are staged for commit.'
25
+ exit 0
26
+ end
27
+
28
+ # Prompt user for commit context
29
+ puts 'Why are you making this change? (Press Enter to skip)'
30
+ commit_context = gets.chomp
31
+ execute_commit(diff, commit_context)
32
+ rescue Clients::ClaudeClient::ConfigError, StandardError => e
33
+ puts "Error: #{e.message}"
34
+ exit 1
35
+ end
36
+ end
37
+ end
38
+ 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committer
4
+ module Commands
5
+ class Setup
6
+ def self.execute(_args)
7
+ Committer::Config.setup
8
+ exit 0
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Committer
4
+ module Commands
5
+ class SetupGitHook
6
+ HOOK_PATH = '.git/hooks/prepare-commit-msg'
7
+
8
+ def self.execute(_args)
9
+ validate_git_repository
10
+ install_git_hook
11
+ display_success_message
12
+ exit 0
13
+ rescue StandardError => e
14
+ puts "Error: #{e.message}"
15
+ exit 1
16
+ end
17
+
18
+ def self.validate_git_repository
19
+ validate_git_root
20
+ validate_git_directory_exists
21
+ validate_hook_doesnt_exist
22
+ end
23
+
24
+ def self.validate_git_directory_exists
25
+ return if Dir.exist?('.git')
26
+
27
+ puts 'Error: Current directory is not a git repository.'
28
+ exit 1
29
+ end
30
+
31
+ def self.validate_git_root
32
+ git_toplevel = `git rev-parse --show-toplevel`.strip
33
+ current_dir = Dir.pwd
34
+ return if git_toplevel == current_dir
35
+
36
+ puts 'Error: Please run this command from the root of your git repository.'
37
+ exit 1
38
+ end
39
+
40
+ def self.validate_hook_doesnt_exist
41
+ return unless File.exist?(HOOK_PATH)
42
+
43
+ puts 'Error: prepare-commit-msg hook already exists.'
44
+ puts 'Please remove or rename the existing hook and try again.'
45
+ exit 1
46
+ end
47
+
48
+ def self.install_git_hook
49
+ template_path = File.expand_path('../prepare-commit-msg', __dir__)
50
+ hook_content = File.read(template_path)
51
+ File.write(HOOK_PATH, hook_content)
52
+ File.chmod(0o755, HOOK_PATH)
53
+ end
54
+
55
+ def self.display_success_message
56
+ puts 'Git hook successfully installed!'
57
+ puts 'Now your commit messages will be automatically generated.'
58
+ end
59
+ end
60
+ end
61
+ 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,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'httparty'
5
+ require 'yaml'
6
+ require_relative 'config'
7
+ require_relative 'prompt_templates'
8
+ require_relative '../clients/claude_client'
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
+ scopes = Committer::Config.load
21
+ scope_section = scopes.empty? ? '' : "\nScopes:\n#{scopes.map { |s| "- #{s}" }.join("\n")}"
22
+ scope_instruction = if scopes.empty?
23
+ '- DO NOT include a scope in your commit message'
24
+ else
25
+ '- Choose an appropriate scope from the list above if relevant to the change'
26
+ end
27
+ format(template,
28
+ diff: @diff,
29
+ scopes_section: scope_section,
30
+ scope_instruction: scope_instruction,
31
+ commit_context: @commit_context)
32
+ end
33
+
34
+ def template
35
+ if @commit_context.nil? || @commit_context.empty?
36
+ Committer::PromptTemplates::SUMMARY_ONLY
37
+ else
38
+ Committer::PromptTemplates::SUMMARY_AND_BODY
39
+ end
40
+ end
41
+
42
+ def self.check_git_status
43
+ stdout, stderr, status = Open3.capture3('git diff --staged')
44
+
45
+ unless status.success?
46
+ puts 'Error executing git diff --staged:'
47
+ puts stderr
48
+ exit 1
49
+ end
50
+
51
+ stdout
52
+ end
53
+
54
+ def parse_response(response)
55
+ text = response.dig('content', 0, 'text')
56
+
57
+ # If user didn't provide context, response should only be a summary line
58
+ if @commit_context.nil? || @commit_context.empty?
59
+ { summary: text.strip, body: nil }
60
+ else
61
+ # Split the response into summary and body
62
+ message_parts = text.split("\n\n", 2)
63
+ summary = message_parts[0].strip
64
+ body = message_parts[1]&.strip
65
+
66
+ # Wrap body text at 80 characters
67
+ body = body.gsub(/(.{1,80})(\s+|$)/, "\\1\n").strip if body
68
+
69
+ { summary: summary, body: body }
70
+ end
71
+ end
72
+
73
+ def prepare_commit_message
74
+ client = Clients::ClaudeClient.new
75
+
76
+ prompt = build_commit_prompt
77
+ response = client.post(prompt)
78
+ parse_response(response)
79
+ end
80
+ end
81
+ end
@@ -19,7 +19,8 @@ module Committer
19
19
  begin
20
20
  YAML.load_file(CONFIG_FILE) || DEFAULT_CONFIG
21
21
  rescue StandardError => e
22
- puts "Error loading config: #{e.message}"
22
+ # Use $stdout directly for better test capture
23
+ $stdout.puts "Error loading config: #{e.message}"
23
24
  DEFAULT_CONFIG
24
25
  end
25
26
  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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Committer
4
- VERSION = '0.2.2'
4
+ VERSION = '0.3.2'
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.3.2
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-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -36,7 +36,15 @@ files:
36
36
  - README.md
37
37
  - bin/committer
38
38
  - lib/clients/claude_client.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
39
46
  - lib/committer/config.rb
47
+ - lib/committer/prepare-commit-msg
40
48
  - lib/committer/prompt_templates.rb
41
49
  - lib/committer/version.rb
42
50
  homepage: https://github.com/Hyper-Unearthing/committer