conventional_commits 0.2.23 → 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: 47ee970c18a0739f838c1de2019974654bd6f4698e364d864a807bebd579c442
4
- data.tar.gz: d75c131ba2000b7aebdcc6b9c8164a5fb17a8c7b7c238c1863ec560a2f8fa693
3
+ metadata.gz: 8ef389be49918b12a03941a89178003c8b6ba9962583516728948bb1c61d5557
4
+ data.tar.gz: e4e5c3dd45bb0e3c140b9519dcd76f432fc7812012402dde8a4a643da8318c9a
5
5
  SHA512:
6
- metadata.gz: 9ce7cfee46f6f0954543e056b346c68c3a9fd0428ed9a89cd8241df9e04873a39428b3214274fdd2880d4ba71ef6b08d58b7387dfc96d31bf7a22c2c26886c77
7
- data.tar.gz: f29c2b32b96c9d6cf1a9d3121f4ca993956ca410c53d5e0f453dce35a917e71e48db95d9b779dd4baad87c5c57fd78359b074260e96fcc9fbd85bbaccaf4fb0c
6
+ metadata.gz: d8435aa0ae03a4ab74880debbec1f7a925b39a972337a17432234f64907e97388b82d48692f58ac16f2d7566dd0f0d96e365ef2ef86940e5ea6372fd59eb5ad8
7
+ data.tar.gz: 5cc8f961cc6ce9296f0348fe6d200c53edbfb10996320065d3b3ae045125177204aa3a01a1be062b861c95561dc828e0c19236a1400bd0015f8d15f353df2971
@@ -5,4 +5,23 @@
5
5
  language: ruby
6
6
  language_version: 3.2.2
7
7
  always_run: true
8
- pass_filenames: false
8
+ pass_filenames: false
9
+
10
+ - id: validate_message
11
+ name: Message Validation
12
+ description: "Validate commit message"
13
+ entry: "conventional_commits validate_commit_msg"
14
+ language: ruby
15
+ language_version: 3.2.2
16
+ pass_filenames: false
17
+ always_run: true
18
+
19
+ - id: prepare_message
20
+ name: Prepare Commit Message
21
+ description: "Preparing commit message"
22
+ entry: "conventional_commits prepare_commit_msg"
23
+ language: ruby
24
+ language_version: 3.2.2
25
+ pass_filenames: false
26
+ args: [--source=$PRE_COMMIT_COMMIT_MSG_SOURCE]
27
+ always_run: true
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- conventional_commits (0.2.21)
4
+ conventional_commits (0.3.0)
5
5
  open3
6
6
  thor
7
7
  yaml
@@ -104,6 +104,8 @@ GEM
104
104
  PLATFORMS
105
105
  arm64-darwin-23
106
106
  arm64-darwin-24
107
+ arm64-darwin-25
108
+ x86_64-linux
107
109
 
108
110
  DEPENDENCIES
109
111
  aruba
data/README.md CHANGED
@@ -55,8 +55,29 @@ to create a new branch
55
55
  - `pattern` - is used to define the separators between in future it will be used to define the order as well
56
56
  - `type` - is used for mapping combination of short names for the type of request. Helps to avoid inconsistency.
57
57
 
58
+ ### Validating in CI
59
+ The hooks validate the current branch and `.git/COMMIT_EDITMSG`. In CI, pass them explicitly:
60
+
61
+ $ conventional_commits validate_branch --branch "$GITHUB_HEAD_REF"
62
+ $ conventional_commits validate_commit_msg --msg "$(git log -1 --format=%B "$SHA")"
63
+
64
+ With a detached `HEAD` and no `--branch`, `validate_branch` falls back to `$GITHUB_HEAD_REF`.
65
+
58
66
  ### Release
59
- Not implemented yet. It will contain tools to increment version number based on the rules
67
+ `next_version` prints the next version from the commits since the latest version tag
68
+ (`X.Y.Z` or `vX.Y.Z`), following `release.rules`:
69
+
70
+ $ conventional_commits next_version
71
+ 0.3.1
72
+
73
+ - Each commit's type is matched against the rules' types; aliases from `type` count as their
74
+ main type, so a `ref:` commit matches a rule listing `refactor`.
75
+ - A breaking change — `type!:` in the subject or a `BREAKING CHANGE:` footer — uses the rule
76
+ for `breaking`.
77
+ - The biggest bump of all the commits wins (`major` > `minor` > `patch`); `version: none`, or a
78
+ type without a rule, produces no release.
79
+ - It prints nothing when no commit calls for a release, and `--initial` (default `0.1.0`) when
80
+ there is no version tag yet.
60
81
 
61
82
 
62
83
  ## Development
@@ -17,4 +17,5 @@ require "helpers/file_extension"
17
17
  require "commit/commit_message_parser"
18
18
  require "commit/commit_message_validator"
19
19
  require "hooks/hooks_installer"
20
+ require "release/next_version_calculator"
20
21
  ConventionalCommits::BranchNameCLI.start(ARGV)
@@ -2,10 +2,12 @@
2
2
 
3
3
  module ConventionalCommits
4
4
  class BranchNameGenerator
5
- attr_reader :reader
5
+ attr_reader :reader, :main_reader
6
6
 
7
- def initialize(reader: reader = Configuration::BranchConfigurationReader.new)
7
+ def initialize(reader: reader = Configuration::BranchConfigurationReader.new,
8
+ main_reader: Configuration::MainConfigurationReader.new)
8
9
  @reader = reader
10
+ @main_reader = main_reader
9
11
  end
10
12
 
11
13
  def generate_name_for(_input, path: Configuration::DEFAULT_CONFIGURATION_PATH)
@@ -34,12 +36,31 @@ module ConventionalCommits
34
36
  components
35
37
  end
36
38
 
39
+ # A branch is valid when the commit message generator can build a subject from it: the
40
+ # type must be one the configuration allows, and the description must not be empty.
37
41
  def is_valid_branch(_input, path: Configuration::DEFAULT_CONFIGURATION_PATH)
38
- !branch_name_components(_input, path:).empty?
42
+ components = branch_name_components(_input, path:)
43
+ validate_description(components[:description], branch: _input)
44
+ validate_type(components[:type], path:)
45
+ true
39
46
  end
40
47
 
41
48
  private
42
49
 
50
+ def validate_description(description, branch:)
51
+ return unless description.to_s.strip.empty?
52
+
53
+ raise GenericError, "The branch #{branch} has no description"
54
+ end
55
+
56
+ def validate_type(type, path:)
57
+ type_configuration = main_reader.get_configuration(path:).type
58
+ return if type_configuration.is_allowed(type.to_s.downcase)
59
+
60
+ raise GenericError,
61
+ "The type #{type} is not allowed. Allowed types #{type_configuration.all_types}"
62
+ end
63
+
43
64
  def find_delimiters_from_pattern(pattern: string)
44
65
  delimiters = pattern.gsub(/<scope>/, "<replace>")
45
66
  .gsub(/<type>/, "<replace>")
@@ -24,44 +24,60 @@ module ConventionalCommits
24
24
  option :source, type: :string, required: false
25
25
  option :cfg_path, type: :string, required: false
26
26
  def prepare_commit_msg
27
- source = options["source"] || ""
28
- puts source
27
+ source = options["source"] || ENV["PRE_COMMIT_COMMIT_MSG_SOURCE"] || ""
29
28
  msg_path = options["msg_path"] || Configuration::DEFAULT_COMMIT_MSG_PATH
30
29
  cfg_path = options["cfg_path"] || Configuration::DEFAULT_CONFIGURATION_PATH
31
30
 
31
+ return if Configuration.git_generated_message?(msg_path)
32
+
32
33
  generator = ConventionalCommits::CommitMessageGenerator.new
33
34
  unless generator.should_preserve_original_message(source:)
34
35
  puts "Generating message"
35
36
  name = generator.prepare_message_template_for_type(type: source, cfg_path:, msg_file_path: msg_path)
36
37
  File.write_to_file(msg_path, name)
37
38
  end
38
-
39
39
  rescue StandardError => e
40
40
  raise Thor::Error, e.message
41
41
  end
42
42
 
43
43
  desc "validate_branch", "validates if the branch conforms to the rules"
44
44
  option :cfg_path, type: :string, required: false
45
+ option :branch, type: :string, required: false,
46
+ desc: "Branch to validate; defaults to the current branch, then $GITHUB_HEAD_REF"
45
47
  def validate_branch_name
46
48
  cfg_path = options["cfg_path"] || Configuration::DEFAULT_CONFIGURATION_PATH
47
49
  generator = ConventionalCommits::BranchNameGenerator.new
48
- name = ConventionalCommits::Git.new.current_branch_name
49
- generator.is_valid_branch(name.strip, path: cfg_path.strip)
50
+ generator.is_valid_branch(branch_name_to_validate, path: cfg_path.strip)
50
51
  rescue StandardError => e
51
52
  raise Thor::Error, e.message
52
53
  end
53
54
 
54
- desc "validate_commit_msg", "validates if the branch conforms to the rules"
55
+ desc "validate_commit_msg", "validates if the commit message conforms to the rules"
55
56
  option :msg_path, type: :string, required: false
56
- option :msg, type: :string, required: false
57
+ option :msg, type: :string, required: false, desc: "Message text to validate instead of a file"
57
58
  option :cfg_path, type: :string, required: false
58
59
  def validate_commit_msg
59
-
60
60
  msg_path = options["msg_path"] || Configuration::DEFAULT_COMMIT_MSG_PATH
61
61
  cfg_path = options["cfg_path"] || Configuration::DEFAULT_CONFIGURATION_PATH
62
62
  validator = ConventionalCommits::CommitMessageValidator.new
63
63
 
64
- validator.validate_commit_msg_from_file(commit_msg_path: msg_path, cfg_path:)
64
+ if options["msg"]
65
+ validator.validate_commit_msg(message: options["msg"], cfg_path:)
66
+ else
67
+ validator.validate_commit_msg_from_file(commit_msg_path: msg_path, cfg_path:)
68
+ end
69
+ rescue StandardError => e
70
+ raise Thor::Error, e.message
71
+ end
72
+
73
+ desc "next_version", "prints the next release version from the commits since the latest version tag"
74
+ option :cfg_path, type: :string, required: false
75
+ option :initial, type: :string, required: false, desc: "Version to print when there is no version tag yet (default 0.1.0)"
76
+ def next_version
77
+ cfg_path = options["cfg_path"] || Configuration::DEFAULT_CONFIGURATION_PATH
78
+ version = ConventionalCommits::NextVersionCalculator.new
79
+ .next_version(cfg_path:, initial: options["initial"] || "0.1.0")
80
+ puts version if version
65
81
  rescue StandardError => e
66
82
  raise Thor::Error, e.message
67
83
  end
@@ -73,5 +89,18 @@ module ConventionalCommits
73
89
  rescue StandardError => e
74
90
  raise Thor::Error, e.message
75
91
  end
92
+
93
+ no_commands do
94
+ # A detached HEAD (e.g. a CI checkout of a pull request) has no current branch, so fall
95
+ # back to the pull request's head branch that GitHub Actions provides.
96
+ def branch_name_to_validate
97
+ name = [options["branch"], ConventionalCommits::Git.new.current_branch_name, ENV["GITHUB_HEAD_REF"]]
98
+ .map { |candidate| candidate.to_s.strip }
99
+ .find { |candidate| !candidate.empty? }
100
+ raise GenericError, "Cannot determine the branch name (HEAD is detached); pass --branch" if name.nil?
101
+
102
+ name
103
+ end
104
+ end
76
105
  end
77
106
  end
@@ -4,10 +4,20 @@ module ConventionalCommits
4
4
  class CommitMessageValidator
5
5
  def validate_commit_msg_from_file(commit_msg_path: Configuration::DEFAULT_COMMIT_MSG_PATH,
6
6
  cfg_path: Configuration::DEFAULT_CONFIGURATION_PATH)
7
- return if commit_msg_path != Configuration::DEFAULT_COMMIT_MSG_PATH
7
+ return if Configuration.git_generated_message?(commit_msg_path)
8
+
9
+ parser = CommitMessageParser.new
10
+ validate(parser.message_components(commit_msg_path:, cfg_path:))
11
+ end
12
+
13
+ def validate_commit_msg(message:, cfg_path: Configuration::DEFAULT_CONFIGURATION_PATH)
8
14
  parser = CommitMessageParser.new
9
- components = parser.message_components(commit_msg_path:, cfg_path:)
15
+ validate(parser.message_components_from_string(commit_msg: message, cfg_path:))
16
+ end
17
+
18
+ private
10
19
 
20
+ def validate(components)
11
21
  raise GenericError, "The Message is Invalid" if components.empty?
12
22
 
13
23
  if components[:body].include?(Configuration::DEFAULT_COMMIT_BODY_TEMPLATE)
data/lib/configuration.rb CHANGED
@@ -6,5 +6,11 @@ module ConventionalCommits
6
6
  DEFAULT_COMMIT_MSG_PATH = ".git/COMMIT_EDITMSG"
7
7
  DEFAULT_COMMIT_BODY_TEMPLATE = "[Describe your work, and put an empty string after]"
8
8
  MAX_ELEMENTS_IN_PATTERN = 4
9
+ # Messages git writes itself for merges and squashes; hooks receive them and leave them as is.
10
+ GIT_GENERATED_MESSAGE_FILES = %w[MERGE_MSG SQUASH_MSG].freeze
11
+
12
+ def self.git_generated_message?(path)
13
+ GIT_GENERATED_MESSAGE_FILES.include?(File.basename(path.to_s))
14
+ end
9
15
  end
10
16
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConventionalCommits
4
- VERSION = "0.2.23"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/git/git.rb CHANGED
@@ -10,6 +10,16 @@ module ConventionalCommits
10
10
  run_system_command "git symbolic-ref --short HEAD"
11
11
  end
12
12
 
13
+ def tags
14
+ run_system_command("git tag --list").split("\n").map(&:strip).reject(&:empty?)
15
+ end
16
+
17
+ # Full messages of the commits reachable from HEAD but not from `ref`, newest first.
18
+ def commit_messages_since(ref)
19
+ run_system_command("git log --format=%B%x00 #{ref}..HEAD")
20
+ .split("\x00").map(&:strip).reject(&:empty?)
21
+ end
22
+
13
23
  def run_system_command(_cmd)
14
24
  Open3.popen3(_cmd) { |_stdin, stdout, _stderr, _wait_thr| stdout.read }
15
25
  end
@@ -2,12 +2,13 @@
2
2
 
3
3
  module ConventionalCommits
4
4
  module Configuration
5
- # Configuration for branch name policies
5
+ # A release rule: which commit types produce which version bump
6
6
  class ReleaseRule
7
7
  attr_reader :types, :version
8
8
 
9
+ # Accepts both `types: [..]` and the singular `type: ..` used for the breaking rule.
9
10
  def initialize(options = {})
10
- @types = options["types"] || []
11
+ @types = Array(options["types"] || options["type"])
11
12
  @version = options["version"] || "none"
12
13
  end
13
14
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConventionalCommits
4
+ # Works out the next release version from the commits since the latest version tag, using the
5
+ # `release.rules` of the configuration: each commit's type (aliases included) maps to a bump,
6
+ # and the biggest bump wins. A breaking change (`type!:` or a `BREAKING CHANGE:` footer) uses
7
+ # the rule for the `breaking` type.
8
+ class NextVersionCalculator
9
+ VERSION_TAG = /\Av?\d+\.\d+\.\d+\z/
10
+ SUBJECT = /\A(?<type>[A-Za-z]+)(?:\([^)]*\))?(?<breaking>!)?:/
11
+ BREAKING_FOOTER = /^BREAKING[ -]CHANGE:/
12
+ BREAKING_TYPE = "breaking"
13
+ BUMPS = %w[none patch minor major].freeze
14
+
15
+ attr_reader :git, :reader
16
+
17
+ def initialize(git: Git.new, reader: Configuration::MainConfigurationReader.new)
18
+ @git = git
19
+ @reader = reader
20
+ end
21
+
22
+ # The next version as "X.Y.Z", `initial` when there is no version tag yet, or nil when no
23
+ # commit since the latest tag calls for a release.
24
+ def next_version(cfg_path: Configuration::DEFAULT_CONFIGURATION_PATH, initial: "0.1.0")
25
+ tag = latest_version_tag
26
+ return initial if tag.nil?
27
+
28
+ config = reader.get_configuration(path: cfg_path)
29
+ bumps = git.commit_messages_since(tag).map { |message| bump_for(message, config) }
30
+ bumped(tag, bumps.max_by { |bump| BUMPS.index(bump) } || "none")
31
+ end
32
+
33
+ private
34
+
35
+ def latest_version_tag
36
+ git.tags.grep(VERSION_TAG).max_by { |tag| Gem::Version.new(tag.delete_prefix("v")) }
37
+ end
38
+
39
+ def bump_for(message, config)
40
+ subject = SUBJECT.match(message.lines.first.to_s.strip)
41
+ return "none" if subject.nil?
42
+
43
+ if subject[:breaking] || message.match?(BREAKING_FOOTER)
44
+ rule_bump(BREAKING_TYPE, config) || type_bump(subject[:type], config)
45
+ else
46
+ type_bump(subject[:type], config)
47
+ end
48
+ end
49
+
50
+ def type_bump(type, config)
51
+ rule_bump(main_type(type.downcase, config), config) || "none"
52
+ end
53
+
54
+ def rule_bump(type, config)
55
+ rule = config.release.rules.find do |candidate|
56
+ candidate.types.map { |rule_type| main_type(rule_type.to_s, config) }.include?(type)
57
+ end
58
+ rule && validated(rule.version)
59
+ end
60
+
61
+ # Rule types and commit types may be aliases ("feature", "ref"); both sides compare as the
62
+ # configured main type. `breaking` and unknown types stay as they are.
63
+ def main_type(type, config)
64
+ main = config.type.main_type(type)
65
+ main.to_s.empty? ? type : main
66
+ end
67
+
68
+ def validated(bump)
69
+ raise GenericError, "Unknown release version '#{bump}', expected one of #{BUMPS}" unless BUMPS.include?(bump)
70
+
71
+ bump
72
+ end
73
+
74
+ def bumped(tag, bump)
75
+ return nil if bump == "none"
76
+
77
+ major, minor, patch = tag.delete_prefix("v").split(".").map(&:to_i)
78
+ case bump
79
+ when "major" then "#{major + 1}.0.0"
80
+ when "minor" then "#{major}.#{minor + 1}.0"
81
+ else "#{major}.#{minor}.#{patch + 1}"
82
+ end
83
+ end
84
+ end
85
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: conventional_commits
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.23
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Swift-Gurus
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-05-04 00:00:00.000000000 Z
11
+ date: 2026-10-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: open3
@@ -129,6 +129,7 @@ files:
129
129
  - lib/models/release_configuration.rb
130
130
  - lib/models/release_rule.rb
131
131
  - lib/models/type_configuration.rb
132
+ - lib/release/next_version_calculator.rb
132
133
  - sig/conventional_commit/branch_name_generator.rbs
133
134
  - sig/conventional_commits.rbs
134
135
  - sig/conventional_commits/branch_name_cli.rbs