canvas-lms/script/lint_commit_message

145 lines
5.3 KiB
Ruby
Executable File

#!/usr/bin/env ruby
require 'shellwords'
require 'json'
require 'jira_ref_parser'
require 'yaml'
GERGICH_GIT_PATH=ENV.fetch("GERGICH_GIT_PATH", ".")
def git(command)
Dir.chdir(GERGICH_GIT_PATH) do
`git #{command}`
end
end
def comment(severity, line, message, link_to_guidelines = false)
$stderr.puts "line #{line}: [#{severity}]: #{message}"
if ENV['GERRIT_REFSPEC']
message += "\n\nHere are some guidelines to keep our git log pretty and useful: http://chris.beams.io/posts/git-commit/" if link_to_guidelines
# NOTE: add 6 to all line numbers, cuz parent/author/commit/dates/etc
payload = {path: "/COMMIT_MSG", position: line + 6, severity: severity, message: message}
`gergich comment #{Shellwords.escape(payload.to_json)}`
end
end
commit_message = ARGV[0] == "--stdin" ? STDIN.read : git("show --pretty=format:%B -s")
exit if commit_message =~ /\A\[?wip($|[\] :-])/i
lines = commit_message.split(/\n/)
subject = lines.shift
if lines.shift != ""
comment "error", 2, "add a blank line after the subject line", true
end
SUBJECT_MAX_LINE_LEN = 75
SUBJECT_IDEAL_LINE_LEN = 65
length_exemption_regex = /^Revert/
if subject.size > SUBJECT_MAX_LINE_LEN && subject !~ length_exemption_regex
comment "error", 1, "subject line can't exceed #{SUBJECT_MAX_LINE_LEN} chars (ideally keep it well under #{SUBJECT_IDEAL_LINE_LEN})", true
elsif subject.size > SUBJECT_IDEAL_LINE_LEN && subject !~ length_exemption_regex
comment "warn", 1, "subject line shouldn't exceed #{SUBJECT_IDEAL_LINE_LEN} chars", true
end
# not perfect (won't get irregular verbs, only checks first word), but
# close enough... does the right thing on most existing commits
maybe_wrong_tense_regex = /\A(spec: )?[a-z]+([^e]ed|[^aijoqsu]s|ing) /i
actually_ok_regex = /\A
(spec:\s+)?
(
# *ing words
(brand|br|[^ ]*learn|masquerad|upcom|shard|word|grad)ing |
# *ed words
(brand|fail|batch|emb|persist|enrich|(un)?publish|moderat|mut|grad)ed |
# *s words
(.*(ion|ment)|observer|alway|outcome|rail|user|spec|student|quizze|alert|plugin|term)s
)
/xi
maybe_start_with_wrong_tense = subject =~ maybe_wrong_tense_regex &&
subject !~ actually_ok_regex
if maybe_start_with_wrong_tense
comment "warn", 1, "make sure your commit message has an imperative verb (e.g. \"add\" instead of \"adds\", \"added\", or \"adding\")", true
end
has_the_word_i = subject =~ /(\A| )i('m)? /i
if has_the_word_i
comment "warn", 1, "just say what the commit does, and not in the first person :P", true
end
BODY_IDEAL_LINE_LENGTH = 75
long_lines = lines.each_with_index.select do |line, i|
line.size > BODY_IDEAL_LINE_LENGTH
end
if long_lines.size > 0
comment "warn", long_lines.first[1] + 3,
"try to keep all lines under #{BODY_IDEAL_LINE_LENGTH} characters" +
(long_lines.size > 1 ? " (note that #{long_lines.size - 1} other long lines follow this one)" : ""), true
end
is_a_selinimum_test = commit_message.split("[selinimum:ignore_intermediate_commits]")
if is_a_selinimum_test.size > 1
line = is_a_selinimum_test[0].count("\n") + 1
comment "error", line, "since you're telling selinimum to ignore intermediate commits, this commit may not be safe to merge"
end
starts_with_spec = subject =~ /\Aspec: /i
exit if starts_with_spec # we're trusting you here, don't be evil
commit_files = git("show --pretty=format:"" --name-only").split("\n")
puts "detected file changes"
puts commit_files
only_affects_specs = commit_files.all? { |f| f =~ %r{\Aspec/} }
if only_affects_specs
comment "warn", 1, "since your commit only affects specs, please prefix your commit message with \"spec: \""
exit
end
touches_db_migrate_file = commit_files.any? { |f| f =~ %r{\Adb\/migrate/}}
if touches_db_migrate_file
comment "warn", 1, "Your commit modifies migration files. Please review and follow the instructions in https://instructure.atlassian.net/wiki/spaces/CE/pages/49643724/Rails+Migrations, including completing an additional migration review."
exit
end
user_config_file = File.expand_path("../../gems/dr_diff/config/gergich_user_config.yml", __FILE__)
if File.exists?(user_config_file)
config = YAML.load_file(user_config_file) || {}
user_list = config['only_report_errors'] || []
exit if user_list.include?(ENV['GERRIT_EVENT_ACCOUNT_EMAIL'])
end
gh_issue = (commit_message =~ /(?:refs|fixes|closes|resolves|references) gh-\d+/i)
SAFE_PARTS_REGEX = /UTF\-8/
TICKET_REGEX = /([A-Z]+\-[0-9]+)/
issue_ids = JiraRefParser.scan_message_for_issue_ids(commit_message)
if issue_ids.empty? && !gh_issue
parts = commit_message.gsub(SAFE_PARTS_REGEX, "")
.split(TICKET_REGEX)
if parts.size > 1
jira_hook_words = JiraRefParser::RefKeywords + JiraRefParser::FixKeywords
comment "warn", parts.first.count("\n") + 1, "the jira hooks won't link this commit to #{parts[1]} unless you use one of the following keywords: #{jira_hook_words.join(", ")}"
else
comment "warn", 3, "you should really reference a ticket ლ(ٱ٥ٱლ)"
end
end
flag_name = (commit_message =~ /^flag\s{0,3}=\s{0,3}([a-zA-Z][\w\-]+)\s{0,3}$/)
unless flag_name
comment "warn", 1, "ლ(ಠ益ಠლ) y u no add release flag? https://instructure.atlassian.net/wiki/spaces/CE/pages/603586589/Commit+Message+Release+Flags"
end
has_a_test_plan = commit_message =~ /test[ -]plan/i
unless has_a_test_plan
comment "warn", 3, "y u no add test plan? ლ(ಠ益ಠლ)"
end