committer 0.3.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: 9dce742b36edfa9cef8ad7811bccfef3ca049a91a92cd05264b4004423a1afb9
4
- data.tar.gz: 50c8ff360e6905279969af7537e2b6e4cd4f918fec61a65ec1b9333e943906e0
3
+ metadata.gz: f495af607b9c66c6b3526af94a5d9792441dc84f6bf09c872586d70940ac6cb8
4
+ data.tar.gz: 3b203eedb9030d65417c74c41c2535ca76230836a612033852703476bcfc2e86
5
5
  SHA512:
6
- metadata.gz: a9b64954d73434cb9c095b32426f673fe544d23d1d1b0bf74da1ab29b700f77e1a7b3b2d5cd9d571d4e005b706d0369f08666317b521996d18145c6fd2224f6a
7
- data.tar.gz: e453d6be97c3ab39aa2fc19c13236477d3b6aed625841e2e2d6035d435adda196ab83353e4763788858e0e4ee328a6f01fd8423412b86679a373648f1b776f45
6
+ metadata.gz: 28b144c3f80ba381a8c3b00208d8ed92e9d8a47e7e2e21c86eb4c904699ecbd1711907a5bccd01227f6c7bb3c4a336adc199813c5590321e7d8b420891c35fb0
7
+ data.tar.gz: 597f5f6db243a56f05c285377b6b8cbaa44f1ffebc300ce2febabf7945179346a3db9d4f1afaadcb92e1ca9141cd61012dfdda2a32dfed18c72d0c52747ce71c
data/bin/committer CHANGED
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'bundler/setup'
5
- require_relative '../lib/committer/config'
5
+ require_relative '../lib/committer/config/accessor'
6
6
  require_relative '../lib/committer/commit_generator'
7
7
  require_relative '../lib/committer/commands'
8
8
 
@@ -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
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../git_helper'
4
+
3
5
  module Committer
4
6
  module Commands
5
7
  class Default
@@ -10,11 +12,7 @@ module Committer
10
12
  summary = commit_message[:summary]
11
13
  body = commit_message[:body]
12
14
  # 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
15
+ Committer::GitHelper.commit(summary, body)
18
16
  end
19
17
 
20
18
  def self.execute(_args)
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../config/writer'
4
+ require_relative '../config/constants'
5
+
3
6
  module Committer
4
7
  module Commands
5
8
  class Setup
6
9
  def self.execute(_args)
7
- Committer::Config.setup
10
+ config_dir = Committer::Config::Constants::CONFIG_DIR
11
+ writer = Committer::Config::Writer.new(config_dir)
12
+ writer.setup
8
13
  exit 0
9
14
  end
10
15
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../git_helper'
4
+
3
5
  module Committer
4
6
  module Commands
5
7
  class SetupGitHook
@@ -29,7 +31,7 @@ module Committer
29
31
  end
30
32
 
31
33
  def self.validate_git_root
32
- git_toplevel = `git rev-parse --show-toplevel`.strip
34
+ git_toplevel = Committer::GitHelper.repo_root
33
35
  current_dir = Dir.pwd
34
36
  return if git_toplevel == current_dir
35
37
 
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'open3'
4
3
  require 'httparty'
5
4
  require 'yaml'
6
- require_relative 'config'
5
+ require_relative 'config/accessor'
7
6
  require_relative 'prompt_templates'
8
7
  require_relative '../clients/claude_client'
8
+ require_relative 'git_helper'
9
9
 
10
10
  module Committer
11
11
  class CommitGenerator
@@ -17,38 +17,24 @@ module Committer
17
17
  end
18
18
 
19
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
20
  format(template,
28
21
  diff: @diff,
29
- scopes_section: scope_section,
30
- scope_instruction: scope_instruction,
31
22
  commit_context: @commit_context)
32
23
  end
33
24
 
34
25
  def template
35
26
  if @commit_context.nil? || @commit_context.empty?
36
- Committer::PromptTemplates::SUMMARY_ONLY
27
+ Committer::PromptTemplates.build_prompt_summary_only
37
28
  else
38
- Committer::PromptTemplates::SUMMARY_AND_BODY
29
+ Committer::PromptTemplates.build_prompt_summary_and_body
39
30
  end
40
31
  end
41
32
 
42
33
  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
34
+ Committer::GitHelper.staged_diff
35
+ rescue Committer::Error => e
36
+ puts e.message
37
+ exit 1
52
38
  end
53
39
 
54
40
  def parse_response(response)
@@ -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
@@ -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.3.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.3.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-07 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
@@ -43,7 +43,12 @@ files:
43
43
  - lib/committer/commands/setup.rb
44
44
  - lib/committer/commands/setup_git_hook.rb
45
45
  - lib/committer/commit_generator.rb
46
- - lib/committer/config.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
47
52
  - lib/committer/prepare-commit-msg
48
53
  - lib/committer/prompt_templates.rb
49
54
  - lib/committer/version.rb
@@ -1,48 +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
- # Use $stdout directly for better test capture
23
- $stdout.puts "Error loading config: #{e.message}"
24
- DEFAULT_CONFIG
25
- end
26
- end
27
-
28
- def self.create_default_config
29
- FileUtils.mkdir_p(CONFIG_DIR)
30
- File.write(CONFIG_FILE, DEFAULT_CONFIG.to_yaml)
31
- end
32
-
33
- def self.setup
34
- create_default_config
35
- puts 'Created config file at:'
36
- puts CONFIG_FILE
37
- puts "\nPlease edit this file to add your Anthropic API key."
38
- puts 'Example config format:'
39
- puts '---'
40
- puts 'api_key: your_api_key_here'
41
- puts 'model: claude-3-7-sonnet-20250219'
42
- puts 'scopes:'
43
- puts ' - feature'
44
- puts ' - api'
45
- puts ' - ui'
46
- end
47
- end
48
- end