From 9a7091410bf5bad50f452a65541eebc55f1be070 Mon Sep 17 00:00:00 2001 From: ydah <13041216+ydah@users.noreply.github.com> Date: Mon, 9 Jan 2023 00:02:05 +0900 Subject: [PATCH 1/2] Move towards changelog generation --- .github/CONTRIBUTING.md | 19 ++- .github/PULL_REQUEST_TEMPLATE.md | 2 +- tasks/changelog.rake | 36 ++++++ tasks/changelog.rb | 204 +++++++++++++++++++++++++++++++ tasks/cut_release.rake | 50 +++++--- 5 files changed, 295 insertions(+), 16 deletions(-) create mode 100644 tasks/changelog.rake create mode 100644 tasks/changelog.rb diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 56d27053b..93980bf13 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -18,11 +18,26 @@ If you encounter problems or have ideas for improvements or new features, please 2. Create a feature branch. 3. Make sure to add tests. 4. Make sure the test suite passes (run `rake`). -5. Add a [changelog](https://github.com/rubocop/rubocop-rspec/blob/master/CHANGELOG.md) entry. +5. Add an entry to the [Changelog](CHANGELOG.md) by creating a file `changelog/{type}_{some_description}.md`. See [changelog entry format](#changelog-entry-format) for details. 6. Commit your changes. 7. Push to the branch. 8. Create new Pull Request. +### Changelog entry format + +Here are a few examples: + +``` +- [#1514](https://github.com/rubocop/rubocop-rspec/issue/1514): Fix a false positive for `RSpec/PendingWithoutReason` when not inside example. ([@ydah]) +``` + +- Create one file `changelog/{type}_{some_description}.md`, where `type` is `new` (New feature), `fix` or `change`, and `some_description` is unique to avoid conflicts. Task `changelog:fix` (or `:new` or `:change`) can help you. +- Mark it up in [Markdown syntax][1]. +- The entry line should start with `- ` (an hyphen and a space). +- If the change has a related GitHub issue (e.g. a bug fix for a reported issue), put a link to the issue as `[#1514](https://github.com/rubocop/rubocop-rspec/issues/1514): `. +- Describe the brief of the change. The sentence should end with a punctuation. +- At the end of the entry, add an implicit link to your GitHub user page as `([@username])`. + ### Spell Checking We are running [codespell](https://github.com/codespell-project/codespell) with [GitHub Actions](https://github.com/rubocop/rubocop-rspec/blob/master/.github/workflows/codespell.yml) to check spelling and @@ -60,3 +75,5 @@ $ mdformat . --number - Common pitfalls: - If your cop inspects code outside of an example, check for false positives when similarly named variables are used inside of the example. - If your cop inspects code inside of an example, check that it works when the example is empty (empty `describe`, `it`, etc.). + +[1]: https://daringfireball.net/projects/markdown/syntax diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8695fedf6..5bdc715a4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,7 +8,7 @@ Before submitting the PR make sure the following are checked: - [ ] Squashed related commits together. - [ ] Added tests. - [ ] Updated documentation. -- [ ] Added an entry to the `CHANGELOG.md` if the new code introduces user-observable changes. +- [ ] Added an entry (file) to the [changelog folder](https://github.com/rubocop/rubocop-committee/blob/master/changelog/) named `{change_type}_{change_description}.md` if the new code introduces user-observable changes. See [changelog entry format](https://github.com/rubocop/rubocop/blob/master/CONTRIBUTING.md#changelog-entry-format) for details. - [ ] The build (`bundle exec rake`) passes (be sure to run this locally, since it may produce updated documentation that you will need to commit). If you have created a new cop: diff --git a/tasks/changelog.rake b/tasks/changelog.rake new file mode 100644 index 000000000..881086e79 --- /dev/null +++ b/tasks/changelog.rake @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +autoload :Changelog, "#{__dir__}/changelog" + +namespace :changelog do + %i[new fix change].each do |type| + desc "Create a Changelog entry (#{type})" + task type, [:id] do |_task, args| + ref_type = :pull if args[:id] + path = Changelog::Entry.new(type: type, ref_id: args[:id], + ref_type: ref_type).write + cmd = "git add #{path}" + system cmd + puts "Entry '#{path}' created and added to git index" + end + end + + desc 'Merge entries and delete them' + task :merge do + raise 'No entries!' unless Changelog.pending? + + Changelog.new.merge!.and_delete! + cmd = "git commit -a -m 'Update Changelog'" + puts cmd + system cmd + end + + desc 'Check to see if there are any entries left' + task :check_clean do + next unless Changelog.pending? + + puts '*** Pending changelog entries!' + puts 'Do `bundle exec rake changelog:merge`' + exit(1) + end +end diff --git a/tasks/changelog.rb b/tasks/changelog.rb new file mode 100644 index 000000000..1d6d95766 --- /dev/null +++ b/tasks/changelog.rb @@ -0,0 +1,204 @@ +# frozen_string_literal: true + +if RUBY_VERSION < '2.6' + puts 'Changelog utilities available only for Ruby 2.6+' + exit(1) +end + +# Changelog utility +class Changelog # rubocop:disable Metrics/ClassLength + ENTRIES_PATH = 'changelog/' + FIRST_HEADER = /#{Regexp.escape("## Master (Unreleased)\n")}/m.freeze + CONTRIBUTORS_HEADER = + /#{Regexp.escape("\n\n")}/m.freeze + ENTRIES_PATH_TEMPLATE = "#{ENTRIES_PATH}%s_%s.md" + TYPE_REGEXP = /#{Regexp.escape(ENTRIES_PATH)}([a-z]+)_/.freeze + TYPE_TO_HEADER = { new: 'New features', fix: 'Bug fixes', + change: 'Changes' }.freeze + HEADER = /### (.*)/.freeze + PATH = 'CHANGELOG.md' + REF_URL = 'https://github.com/rubocop/rubocop-rspec' + MAX_LENGTH = 40 + CONTRIBUTOR = '[@%s]: https://github.com/%s' + SIGNATURE = Regexp.new(format(Regexp.escape('[@%s]'), user: '([\w-]+)')) + EOF = "\n" + + # New entry + Entry = Struct.new(:type, :body, :ref_type, :ref_id, :user, + keyword_init: true) do + def initialize(type:, body: last_commit_title, ref_type: nil, ref_id: nil, + user: github_user) + id, body = extract_id(body) + ref_id ||= id || 'x' + ref_type ||= id ? :issues : :pull + super + end + + def write + FileUtils.mkdir_p(ENTRIES_PATH) + File.write(path, content) + path + end + + def path + format(ENTRIES_PATH_TEMPLATE, type: type, name: str_to_filename(body)) + end + + def content + period = '.' unless body.end_with? '.' + "- #{ref}: #{body}#{period} ([@#{user}])\n" + end + + def ref + "[##{ref_id}](#{REF_URL}/#{ref_type}/#{ref_id})" + end + + def last_commit_title + `git log -1 --pretty=%B`.lines.first.chomp + end + + def extract_id(body) + /^\[Fix(?:es)? #(\d+)\] (.*)/.match(body)&.captures || [nil, body] + end + + def str_to_filename(str) + str + .split + .reject(&:empty?) + .map { |s| prettify(s) } + .inject do |result, word| + s = "#{result}_#{word}" + return result if s.length > MAX_LENGTH + + s + end + end + + def github_user + user = `git config --global credential.username`.chomp + if user.empty? + warn 'Set your username with ' \ + '`git config --global credential.username "myusernamehere"`' + end + + user + end + + private + + def prettify(str) + str.gsub!(/\W/, '_') + + # Separate word boundaries by `_`. + str.gsub!(/([A-Z]+)(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) do + (Regexp.last_match(1) || Regexp.last_match(2)) << '_' + end + + str.gsub!(/\A_+|_+\z/, '') + str.downcase! + str + end + end + + def self.pending? + entry_paths.any? + end + + def self.entry_paths + Dir["#{ENTRIES_PATH}*"] + end + + def self.read_entries + entry_paths.to_h { |path| [path, File.read(path)] } + end + + attr_reader :header, :rest + + def initialize(content: File.read(PATH), entries: Changelog.read_entries) + require 'strscan' + + parse(content) + @entries = entries + end + + def and_delete! + @entries.each_key { |path| File.delete(path) } + end + + def merge! + File.write(PATH, merge_content) + self + end + + def unreleased_content + entry_map = parse_entries(@entries) + merged_map = merge_entries(entry_map) + merged_map.flat_map do |header, things| + ["### #{header}\n", *things, ''] + end.join("\n") + end + + def merge_content + merged_content = [@header, unreleased_content, @changes.chomp, + *all_contributors].join("\n") + + merged_content << EOF + end + + def all_contributors + ( + @contributors.split(/\R/) + + new_contributors + ).uniq.sort + end + + def new_contributors + contributors + .map { |user| format(CONTRIBUTOR, link: user.downcase, user: user) } + end + + def contributors + contributors = @entries.values.flat_map do |entry| + entry.match(/\. \((?.+)\)\n/)[:contributors].split(',') + end + + contributors.join.scan(SIGNATURE).flatten + end + + private + + def merge_entries(entry_map) + all = @unreleased.merge(entry_map) { |_k, v1, v2| v1.concat(v2) } + canonical = TYPE_TO_HEADER.values.to_h { |v| [v, nil] } + canonical.merge(all).compact + end + + def parse(content) + ss = StringScanner.new(content) + @header = ss.scan_until(FIRST_HEADER) + @unreleased = parse_release(ss.scan_until(/\n(?=## )/m)) + @changes = ss.scan_until(CONTRIBUTORS_HEADER) + @contributors = ss.rest + end + + # @return [Hash]] + def parse_release(unreleased) + unreleased + .lines + .map(&:chomp) + .reject(&:empty?) + .slice_before(HEADER) + .to_h do |header, *entries| + [HEADER.match(header)[1], entries] + end + end + + def parse_entries(path_content_map) + changes = Hash.new { |h, k| h[k] = [] } + path_content_map.each do |path, content| + header = TYPE_TO_HEADER.fetch(TYPE_REGEXP.match(path)[1].to_sym) + changes[header].concat(content.lines.map(&:chomp)) + end + changes + end +end diff --git a/tasks/cut_release.rake b/tasks/cut_release.rake index 130b67768..71456fed5 100644 --- a/tasks/cut_release.rake +++ b/tasks/cut_release.rake @@ -3,18 +3,29 @@ require 'bump' namespace :cut_release do - def update_file(path) - content = File.read(path) - File.write(path, yield(content)) - end - %w[major minor patch pre].each do |release_type| - desc "Cut a new #{release_type} release and update documents." - task release_type do + desc "Cut a new #{release_type} release and create release notes." + task release_type => 'changelog:check_clean' do run(release_type) end end + def add_header_to_changelog(version) + update_file('CHANGELOG.md') do |changelog| + changelog.sub("## Master (Unreleased)\n\n", + '\0' "## #{version} (#{Time.now.strftime('%F')})\n\n") + end + end + + def update_antora_yml(new_version) + antora_metadata = File.read('docs/antora.yml') + + File.open('docs/antora.yml', 'w') do |f| + f << antora_metadata.sub('version: ~', + "version: '#{version_sans_patch(new_version)}'") + end + end + def version_sans_patch(version) version.split('.').take(2).join('.') end @@ -28,6 +39,22 @@ namespace :cut_release do RuboCop::ConfigLoader.default_configuration = nil # invalidate loaded conf end + def new_version_changes + changelog = File.read('CHANGELOG.md') + _, _, new_changes, _older_changes = changelog.split(/^## .*$/, 4) + new_changes + end + + def update_file(path) + content = File.read(path) + File.write(path, yield(content)) + end + + def user_links(text) + names = text.scan(/\[@(\S+)\]/).map(&:first).uniq + names.map { |name| "[@#{name}]: https://github.com/#{name}" }.join("\n") + end + def update_docs(version) update_file('docs/antora.yml') do |antora_metadata| antora_metadata.sub('version: ~', @@ -35,13 +62,6 @@ namespace :cut_release do end end - def add_header_to_changelog(version) - update_file('CHANGELOG.md') do |changelog| - changelog.sub("## Master (Unreleased)\n\n", - '\0' "## #{version} (#{Time.now.strftime('%F')})\n\n") - end - end - def run(release_type) old_version = Bump::Bump.current Bump::Bump.run(release_type, commit: false, bundle: false, tag: false) @@ -50,7 +70,9 @@ namespace :cut_release do update_cop_versions(new_version) `bundle exec rake generate_cops_documentation` update_docs(new_version) if %w[major minor].include?(release_type) + add_header_to_changelog(new_version) + update_antora_yml(new_version) puts "Changed version from #{old_version} to #{new_version}." end From d126236ef47b32e0c22e0d892b16a80716ed5513 Mon Sep 17 00:00:00 2001 From: ydah <13041216+ydah@users.noreply.github.com> Date: Mon, 9 Jan 2023 00:10:32 +0900 Subject: [PATCH 2/2] Move CHANGELOG unreleased items to file --- CHANGELOG.md | 6 ------ ..._rspec_predicate_matcher_using_include_and_respond_to.md | 1 + ...ive_for_rspec_pending_without_reason_argument_methods.md | 1 + ...e_for_rspec_pending_without_reason_not_inside_example.md | 1 + changelog/new_add_new_rspec_capybara_match_style_cop.md | 1 + .../new_add_new_rspec_rails_minitest_assertions_cop.md | 1 + 6 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 changelog/fix_fix_a_false_ negative_for_rspec_predicate_matcher_using_include_and_respond_to.md create mode 100644 changelog/fix_fix_a_false_positive_for_rspec_pending_without_reason_argument_methods.md create mode 100644 changelog/fix_fix_a_false_positive_for_rspec_pending_without_reason_not_inside_example.md create mode 100644 changelog/new_add_new_rspec_capybara_match_style_cop.md create mode 100644 changelog/new_add_new_rspec_rails_minitest_assertions_cop.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 49a0441cb..363a2dc77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,6 @@ ## Master (Unreleased) -- Fix a false positive for `RSpec/PendingWithoutReason` when pending/skip is argument of methods. ([@ydah]) -- Add new `RSpec/Capybara/MatchStyle` cop. ([@ydah]) -- Add new `RSpec/Rails/MinitestAssertions` cop. ([@ydah]) -- Fix a false positive for `RSpec/PendingWithoutReason` when not inside example. ([@ydah]) -- Fix a false negative for `RSpec/PredicateMatcher` when using `include` and `respond_to`. ([@ydah]) - ## 2.16.0 (2022-12-13) - Add new `RSpec/FactoryBot/FactoryNameStyle` cop. ([@ydah]) diff --git a/changelog/fix_fix_a_false_ negative_for_rspec_predicate_matcher_using_include_and_respond_to.md b/changelog/fix_fix_a_false_ negative_for_rspec_predicate_matcher_using_include_and_respond_to.md new file mode 100644 index 000000000..a3b651be7 --- /dev/null +++ b/changelog/fix_fix_a_false_ negative_for_rspec_predicate_matcher_using_include_and_respond_to.md @@ -0,0 +1 @@ +- [#1532](https://github.com/rubocop/rubocop-rspec/pull/1532): Fix a false negative for `RSpec/PredicateMatcher` when using `include` and `respond_to`. ([@ydah]) diff --git a/changelog/fix_fix_a_false_positive_for_rspec_pending_without_reason_argument_methods.md b/changelog/fix_fix_a_false_positive_for_rspec_pending_without_reason_argument_methods.md new file mode 100644 index 000000000..120d9ebb6 --- /dev/null +++ b/changelog/fix_fix_a_false_positive_for_rspec_pending_without_reason_argument_methods.md @@ -0,0 +1 @@ +- [#1516](https://github.com/rubocop/rubocop-rspec/pull/1516): Fix a false positive for `RSpec/PendingWithoutReason` when pending/skip is argument of methods. ([@ydah]) diff --git a/changelog/fix_fix_a_false_positive_for_rspec_pending_without_reason_not_inside_example.md b/changelog/fix_fix_a_false_positive_for_rspec_pending_without_reason_not_inside_example.md new file mode 100644 index 000000000..841f20b74 --- /dev/null +++ b/changelog/fix_fix_a_false_positive_for_rspec_pending_without_reason_not_inside_example.md @@ -0,0 +1 @@ +- [#1514](https://github.com/rubocop/rubocop-rspec/issue/1514): Fix a false positive for `RSpec/PendingWithoutReason` when not inside example. ([@ydah]) diff --git a/changelog/new_add_new_rspec_capybara_match_style_cop.md b/changelog/new_add_new_rspec_capybara_match_style_cop.md new file mode 100644 index 000000000..49876b296 --- /dev/null +++ b/changelog/new_add_new_rspec_capybara_match_style_cop.md @@ -0,0 +1 @@ +- [#1456](https://github.com/rubocop/rubocop-rspec/pull/1456): Add new `RSpec/Capybara/MatchStyle` cop. ([@ydah]) diff --git a/changelog/new_add_new_rspec_rails_minitest_assertions_cop.md b/changelog/new_add_new_rspec_rails_minitest_assertions_cop.md new file mode 100644 index 000000000..5ce18f2ed --- /dev/null +++ b/changelog/new_add_new_rspec_rails_minitest_assertions_cop.md @@ -0,0 +1 @@ +- [#1485](https://github.com/rubocop/rubocop-rspec/issue/1485): Add new `RSpec/Rails/MinitestAssertions` cop. ([@ydah])