# frozen_string_literal: true require_relative File.expand_path('../../lib/gitlab/danger/commit_linter', __dir__) COMMIT_MESSAGE_GUIDELINES = "https://docs.gitlab.com/ee/development/contributing/merge_request_workflow.html#commit-messages-guidelines" MORE_INFO = "For more information, take a look at our [Commit message guidelines](#{COMMIT_MESSAGE_GUIDELINES})." THE_DANGER_JOB_TEXT = "the `danger-review` job" MAX_COMMITS_COUNT = 10 MAX_COMMITS_COUNT_EXCEEDED_MESSAGE = <<~MSG This merge request includes more than %d commits. Each commit should meet the following criteria: 1. Have a well-written commit message. 1. Has all tests passing when used on its own (e.g. when using git checkout SHA). 1. Can be reverted on its own without also requiring the revert of commit that came before it. 1. Is small enough that it can be reviewed in isolation in under 30 minutes or so. If this merge request contains commits that do not meet this criteria and/or contains intermediate work, please rebase these commits into a smaller number of commits or split this merge request into multiple smaller merge requests. MSG def gitlab_danger @gitlab_danger ||= GitlabDanger.new(helper.gitlab_helper) end def fail_commit(commit, message, more_info: true) self.fail(build_message(commit, message, more_info: more_info)) end def warn_commit(commit, message, more_info: true) self.warn(build_message(commit, message, more_info: more_info)) end def build_message(commit, message, more_info: true) [message].tap do |full_message| full_message << ". #{MORE_INFO}" if more_info full_message.unshift("#{commit.sha}: ") if commit.sha end.join end def squash_mr? gitlab_danger.ci? ? gitlab.mr_json['squash'] : false end def wip_mr? gitlab_danger.ci? ? gitlab.mr_json['work_in_progress'] : false end def danger_job_link gitlab_danger.ci? ? "[#{THE_DANGER_JOB_TEXT}](#{ENV['CI_JOB_URL']})" : THE_DANGER_JOB_TEXT end # Perform various checks against commits. We're not using # https://github.com/jonallured/danger-commit_lint because its output is not # very helpful, and it doesn't offer the means of ignoring merge commits. def lint_commit(commit) linter = Gitlab::Danger::CommitLinter.new(commit) # For now we'll ignore merge commits, as getting rid of those is a problem # separate from enforcing good commit messages. return linter if linter.merge? # We ignore revert commits as they are well structured by Git already return linter if linter.revert? # If MR is set to squash, we ignore fixup commits return linter if linter.fixup? && squash_mr? if linter.fixup? msg = "Squash or fixup commits must be squashed before merge, or enable squash merge option and re-run #{danger_job_link}." if wip_mr? || squash_mr? warn_commit(commit, msg, more_info: false) else fail_commit(commit, msg, more_info: false) end # Makes no sense to process other rules for fixup commits, they trigger just more noise return linter end # Fail if a suggestion commit is used and squash is not enabled if linter.suggestion? unless squash_mr? fail_commit(commit, "If you are applying suggestions, enable squash in the merge request and re-run #{danger_job_link}.", more_info: false) end return linter end linter.lint end def lint_mr_title(mr_title) commit = Struct.new(:message, :sha).new(mr_title) Gitlab::Danger::CommitLinter.new(commit).lint_subject("merge request title") end def count_non_fixup_commits(commit_linters) commit_linters.count { |commit_linter| !commit_linter.fixup? } end def lint_commits(commits) commit_linters = commits.map { |commit| lint_commit(commit) } failed_commit_linters = commit_linters.select { |commit_linter| commit_linter.failed? } warn_or_fail_commits(failed_commit_linters, default_to_fail: !squash_mr?) if count_non_fixup_commits(commit_linters) > MAX_COMMITS_COUNT self.warn(format(MAX_COMMITS_COUNT_EXCEEDED_MESSAGE, max_commits_count: MAX_COMMITS_COUNT)) end if squash_mr? multi_line_commit_linter = commit_linters.detect { |commit_linter| !commit_linter.merge? && commit_linter.multi_line? } if multi_line_commit_linter && multi_line_commit_linter.failed? warn_or_fail_commits(multi_line_commit_linter) else title_linter = lint_mr_title(gitlab.mr_json['title']) if title_linter.failed? warn_or_fail_commits(title_linter) end end end end def warn_or_fail_commits(failed_linters, default_to_fail: true) level = default_to_fail ? :fail : :warn Array(failed_linters).each do |linter| linter.problems.each do |problem_key, problem_desc| case problem_key when :subject_above_warning warn_commit(linter.commit, problem_desc) else self.__send__("#{level}_commit", linter.commit, problem_desc) # rubocop:disable GitlabSecurity/PublicSend end end end end lint_commits(git.commits)