# frozen_string_literal: true
require_relative “../../../gitlab/dangerfiles/commit_linter” require_relative “../../../gitlab/dangerfiles/merge_request_linter”
COMMIT_MESSAGE_GUIDELINES = “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 `%<job_name>s` job” MAX_COMMITS_COUNT = helper.config.max_commits_count MAX_COMMITS_COUNT_EXCEEDED_MESSAGE = <<~MSG This merge request includes more than %<max_commits_count>d commits. Each commit should meet the following criteria:
-
Have a well-written commit message.
-
Has all tests passing when used on its own (e.g. when using git checkout SHA).
-
Can be reverted on its own without also requiring the revert of commit that came before it.
-
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 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 danger_job_link
helper.ci? ? "[#{format(THE_DANGER_JOB_TEXT, job_name: ENV["CI_JOB_NAME"])}](#{ENV['CI_JOB_URL']})" : THE_DANGER_JOB_TEXT
end
# Perform various checks against commits. We're not using # 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::Dangerfiles::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? && helper.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 helper.draft_mr? || helper.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 helper.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::Dangerfiles::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 helper.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 helper.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: !helper.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, :details_too_many_changes, :details_line_too_long 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 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?