committer 0.2.1 → 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: 131648bca91f63a2fd32dc1dde7048f70ce3e3d0bc919d6737ff2ec098914fc2
4
- data.tar.gz: dd1734f574490501b34589c8494eae3edf8db202c97372b2b961088100280855
3
+ metadata.gz: 9dce742b36edfa9cef8ad7811bccfef3ca049a91a92cd05264b4004423a1afb9
4
+ data.tar.gz: 50c8ff360e6905279969af7537e2b6e4cd4f918fec61a65ec1b9333e943906e0
5
5
  SHA512:
6
- metadata.gz: 03fbecc6488b914a31d8793015a4a0268ffd1c961bde13ff9df8440a2aa950ea904379c46262922439ac61d2976cdb5f2322747133b2bee0ce5371ad50aa45d5
7
- data.tar.gz: 3e1dc9c66848b9039b233ed44ed46d16e6ed48c5c8f66129d25a23ee420cb00613d6164b4a4a0ea4ab3b9e18ac7357b8efd5e6e08e8b69bc9c4ab1279c9671e0
6
+ metadata.gz: a9b64954d73434cb9c095b32426f673fe544d23d1d1b0bf74da1ab29b700f77e1a7b3b2d5cd9d571d4e005b706d0369f08666317b521996d18145c6fd2224f6a
7
+ data.tar.gz: e453d6be97c3ab39aa2fc19c13236477d3b6aed625841e2e2d6035d435adda196ab83353e4763788858e0e4ee328a6f01fd8423412b86679a373648f1b776f45
data/README.md CHANGED
@@ -4,35 +4,34 @@ An AI-powered git commit message generator using Claude.
4
4
 
5
5
  ## Overview
6
6
 
7
- 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.
7
+ The goal of committer is to make it easier to write beautiful commits.
8
8
 
9
- ## Installation
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
- ### Install from RubyGems
11
+ ## What Makes a Good Commit
12
12
 
13
- ```bash
14
- gem install committer
15
- ```
13
+ A good commit should:
14
+
15
+ - Have a summary that clearly describes the change
16
+ - Explain WHY the change was made, not just what changed
16
17
 
17
- ### Manual Installation
18
+ When a future developer uses git blame to understand a line of code, they should immediately understand why the change was made. This context is invaluable for maintaining and evolving the codebase effectively.
18
19
 
19
- 1. Install Bundler if you haven't already:
20
+ ## How Committer Helps
20
21
 
21
- ```sh
22
- gem install bundler
23
- ```
22
+ Committer analyzes your code changes and generates commit messages that:
24
23
 
25
- 2. Install the project dependencies:
24
+ 1. Provide a clean, descriptive summary of the change
25
+ 2. Include context about why the change was necessary
26
+ 3. Follow [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) format for consistency
26
27
 
27
- ```sh
28
- bundle install
29
- ```
28
+ ## Installation
30
29
 
31
- 3. Link the executable:
32
- ```bash
33
- gem build committer.gemspec
34
- gem install committer-0.1.0.gem
35
- ```
30
+ ### Install from RubyGems
31
+
32
+ ```bash
33
+ gem install committer
34
+ ```
36
35
 
37
36
  ## Configuration
38
37
 
@@ -44,13 +43,19 @@ committer setup
44
43
 
45
44
  This will create a template config file at `~/.committer/config.yml`.
46
45
 
47
- Next, edit this file to add your Anthropic API key and optionally change the model:
46
+ Next, edit this file to add your Anthropic API key and optionally change the model or configure commit scopes:
48
47
 
49
48
  ```yaml
50
49
  api_key: your_anthropic_api_key_here
51
50
  model: claude-3-7-sonnet-20250219
51
+ scopes:
52
+ - feature
53
+ - api
54
+ - ui
52
55
  ```
53
56
 
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`).
58
+
54
59
  You only need to do this setup once.
55
60
 
56
61
  ## Usage
@@ -64,21 +69,62 @@ committer
64
69
  This will:
65
70
 
66
71
  1. Get the diff of your staged changes
67
- 2. Send it to Claude for analysis
68
- 3. Generate a commit message in conventional format
69
- 4. Open your default git editor with the suggested message
70
- 5. Allow you to edit the message if needed or simply save to confirm
72
+ 2. Ask you for optional context about why you're making the change
73
+ 3. Send the diff and context to Claude for analysis
74
+ 4. Generate a commit message in [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) format (with scope if configured)
75
+ 5. Open your default git editor with the suggested message
76
+ 6. Allow you to edit the message if needed or simply save to confirm
71
77
 
72
78
  ## Commands
73
79
 
74
80
  - `committer` - Generate commit message for staged changes
75
81
  - `committer setup` - Create the config file template
76
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
77
122
 
78
- You can also run it directly through git:
123
+ Committer uses RSpec for testing. To run the tests:
79
124
 
80
125
  ```bash
81
- git smart-commit
126
+ bundle install
127
+ bundle exec rake spec
82
128
  ```
83
129
 
84
130
  ## License
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.1'
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.1
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
@@ -29,16 +29,22 @@ description: A tool that uses Claude API to generate conventional commit message
29
29
  email:
30
30
  - sebastien@managerbot.dev
31
31
  executables:
32
- - git-smart-commit
33
32
  - committer
34
33
  extensions: []
35
34
  extra_rdoc_files: []
36
35
  files:
37
36
  - README.md
38
37
  - bin/committer
39
- - bin/git-smart-commit
40
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
41
46
  - lib/committer/config.rb
47
+ - lib/committer/prepare-commit-msg
42
48
  - lib/committer/prompt_templates.rb
43
49
  - lib/committer/version.rb
44
50
  homepage: https://github.com/Hyper-Unearthing/committer
data/bin/git-smart-commit DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # This is a git integration script that allows users to run:
5
- # git smart-commit
6
- # which will forward to the committer command
7
-
8
- require_relative 'committer'