151 lines
5.5 KiB
Ruby
151 lines
5.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative File.expand_path('../../lib/gitlab/danger/commit_linter', __dir__)
|
|
require_relative File.expand_path('../../lib/gitlab/danger/merge_request_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 %<max_commits_count>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?
|
|
# Locally, we assume the MR is set to be squashed so that the user only sees warnings instead of errors.
|
|
gitlab_danger.ci? ? gitlab.mr_json['squash'] : true
|
|
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::MergeRequestLinter.new(commit).lint
|
|
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) }
|
|
|
|
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)
|
|
commit_linters.delete(multi_line_commit_linter) # Don't show an error (here) and a warning (below)
|
|
elsif gitlab_danger.ci? # We don't have access to the MR title locally
|
|
title_linter = lint_mr_title(gitlab.mr_json['title'])
|
|
if title_linter.failed?
|
|
warn_or_fail_commits(title_linter)
|
|
end
|
|
end
|
|
else
|
|
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
|
|
end
|
|
|
|
failed_commit_linters = commit_linters.select { |commit_linter| commit_linter.failed? }
|
|
warn_or_fail_commits(failed_commit_linters, default_to_fail: !squash_mr?)
|
|
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_too_short, :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
|
|
|
|
# As part of https://gitlab.com/groups/gitlab-org/-/epics/4826 we are
|
|
# vendoring workhorse commits from the stand-alone gitlab-workhorse
|
|
# repo. There is no point in linting commits that we want to vendor as
|
|
# is.
|
|
def workhorse_changes?
|
|
git.diff.any? { |file| file.path.start_with?('workhorse/') }
|
|
end
|
|
|
|
lint_commits(git.commits) unless workhorse_changes?
|