lib/semmy/changelog.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'unindent'

module Semmy
  module Changelog
    extend self

    class InsertPointNotFound < Error; end

    CloseSection = Struct.new(:config, :options) do
      def call(contents)
        contents.dup.tap do |result|
          result.gsub!(unreleased_section_matcher,
                       version_header) ||
            fail(InsertPointNotFound, <<-END.unindent)
              Could not find insert point for section heading in:

              #{contents}
            END
        end
      end

      private

      def unreleased_section_matcher
        /#{config.changelog_unreleased_section_heading}(\s*#{compare_link_matcher})?/
      end

      def compare_link_matcher
        # match markdown link of the form [...](...)
        /\[[^\]]+\]\([^)]+\)/
      end

      def version_header
        <<-END.unindent.chomp
          #{version_heading}

          #{current_date}

          #{compare_link_for_versions}
        END
      end

      def version_heading
        config.changelog_version_section_heading % {
          version: version
        }
      end

      def current_date
        options[:date].strftime('%Y-%m-%d')
      end

      def compare_link_for_versions
        Changelog.compare_link(config,
                               homepage: options[:homepage],
                               old_version_tag: old_ref,
                               new_version_tag: Changelog.version_tag(version))
      end

      def old_ref
        if VersionString.patch_level?(version)
          Changelog.version_tag(VersionString.previous_version(version))
        else
          VersionString.previous_stable_branch_name(version, config.stable_branch_name)
        end
      end

      def version
        options[:version]
      end
    end

    UpdateForMinor = Struct.new(:config, :options) do
      def call(contents)
        replace_starting_at(Changelog.version_line_matcher(config),
                            contents,
                            unreleased_section)
      end

      private

      def unreleased_section
        # Once Ruby < 2.3 support is dropped, this can be rewritten
        # as:
        #
        #     <<~END
        #       #{config.changelog_unreleased_section_heading}
        #
        #       #{compare_link_for_master}
        #
        #       #{config.changelog_unreleased_section_blank_slate}
        #
        #       #{link_to_changelog_on_previous_minor_stable_branch}
        #     END
        #
        # `unindent` cannot handle line breaks in interpolated values correctly.
        [
          config.changelog_unreleased_section_heading,
          compare_link_for_master,
          config.changelog_unreleased_section_blank_slate,
          link_to_changelog_on_previous_minor_stable_branch
        ].join("\n\n") << "\n"
      end

      def compare_link_for_master
        Changelog.compare_link(config,
                               homepage: options[:homepage],
                               old_version_tag: previous_stable_branch_name,
                               new_version_tag: 'master')
      end

      def link_to_changelog_on_previous_minor_stable_branch
        config.changelog_previous_changes_link % {
          branch: previous_stable_branch_name,
          url: Changelog.file_url(config,
                                  homepage: options[:homepage],
                                  branch: previous_stable_branch_name)
        }
      end

      def previous_stable_branch_name
        VersionString.previous_stable_branch_name(options[:version],
                                                  config.stable_branch_name)
      end

      def replace_starting_at(line_matcher, text, inserted_text)
        unless text =~ line_matcher
          fail(InsertPointNotFound, 'Insert point not found.')
        end

        [text.split(line_matcher).first, inserted_text].join
      end
    end

    InsertUnreleasedSection = Struct.new(:config) do
      def call(contents)
        insert_before(Changelog.version_line_matcher(config),
                      contents,
                      config.changelog_unreleased_section_heading << "\n")
      end

      private

      def insert_before(line_matcher, text, inserted_text)
        text.dup.tap do |result|
          unless (result.sub!(line_matcher, inserted_text + "\n\\0"))
            fail(InsertPointNotFound,
                 'Insert point not found.')
          end
        end
      end
    end

    ReplaceMinorStableBranchWithMajorStableBranch = Struct.new(:config, :options) do
      def call(contents)
        contents.gsub(minor_stable_branch(options[:version]),
                      major_stable_branch(options[:version]))
      end

      private

      def minor_stable_branch(version)
        VersionString.previous_stable_branch_name(version, config.stable_branch_name)
      end

      def major_stable_branch(version)
        config.stable_branch_name % VersionString.components(version).merge(minor: 'x')
      end
    end

    def version_tag(version)
      "v#{version}"
    end

    def version_line_matcher(config)
      Regexp.new(config.changelog_version_section_heading % {
                   version: '([0-9.]+)'
                 })
    end

    def compare_link(config, interpolations)
      "[Compare changes](#{compare_url(config, interpolations)})"
    end

    def compare_url(config, interpolations)
      config.compare_url % url_interpolations(config, interpolations)
    end

    def file_url(config, interpolations)
      config.file_url % url_interpolations(config, interpolations)
        .merge(path: config.changelog_path)
    end

    private

    def url_interpolations(config, interpolations)
      interpolations.merge(repository: repository_url(config,
                                                      interpolations[:homepage]))
    end

    def repository_url(config, homepage)
      if config.github_repository
        "https://github.com/#{config.github_repository}"
      else
        homepage
      end
    end
  end
end