canvas-lms/script/lint_commit_message

171 lines
6.4 KiB
Ruby
Executable File

#!/usr/bin/env ruby
# frozen_string_literal: true
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
# rubocop:disable Style/GlobalVars yeah a global, I'm being lazy and refactoring to an object
def comment(severity, line, message, link_to_guidelines: false)
if ENV["GERRIT_REFSPEC"]
if link_to_guidelines
message += "\n\nHere are some guidelines to keep our git log pretty and useful: http://chris.beams.io/posts/git-commit/"
end
# NOTE: add 6 to all line numbers, cuz parent/author/commit/dates/etc
payload = { path: "/COMMIT_MSG", position: (line || 1) + 6, severity:, message: }
`gergich comment #{Shellwords.escape(payload.to_json)}`
warn "line #{line}: [#{severity}]: #{message}"
elsif line
warn $lines[line - 1]
warn " ^: [#{severity}]: #{message}"
else
warn "[#{severity}]: #{message}"
end
end
commit_message = case ARGV[0]
when "--stdin" then $stdin.read
when nil then git("show --pretty=format:%B -s")
else File.read(ARGV[0])
end
exit if commit_message.match?(/\A\[?wip($|[\] :-])/i)
lines = commit_message.split("\n")
$lines = lines.dup
subject = lines.shift
# rubocop:enable Style/GlobalVars
if lines.shift != ""
comment "error", 2, "add a blank line after the subject line", link_to_guidelines: 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})",
link_to_guidelines: 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", link_to_guidelines: 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\")",
link_to_guidelines: 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", link_to_guidelines: true
end
BODY_IDEAL_LINE_LENGTH = 75
long_lines = lines.each_with_index.select do |line, _i|
line.size > BODY_IDEAL_LINE_LENGTH
end
unless long_lines.empty?
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)" : ""),
link_to_guidelines: true
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")
if ENV["GERRIT_REFSPEC"]
puts "detected file changes"
puts commit_files
end
touches_db_migrate_file = commit_files.any? { |f| f.start_with?("db/migrate/") }
if touches_db_migrate_file
comment "info", nil, "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."
end
touches_lti_variable_expander_file = commit_files.any? { |f| f.start_with?("lib/lti/variable_expander") }
touches_lti_variable_expander_docs = commit_files.any? { |f| f.start_with?("doc/api/tools_variable_substitutions") }
if touches_lti_variable_expander_file && !touches_lti_variable_expander_docs
comment "warn", nil, "Changes were made to the variable expander file. The docs are created from the comments and there are no document changes. Do you need to update the docs also? If so, run `rake doc:api` and commit the changes. Otherwise (if you didn't modify the descriptions of any variable expansions, or if you added an internal-only expansion using @internal) you may ignore this warning."
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", nil, "you should really reference a ticket ლ(ٱ٥ٱლ)"
end
end
only_affects_specs = commit_files.all? { |f| f.start_with?("spec/") }
if only_affects_specs
comment "warn", nil, "since your commit only affects specs, please prefix your commit message with \"spec: \""
exit
end
flag_name = (commit_message =~ /^flag\s{0,3}=\s{0,3}([a-zA-Z][\w-]+)\s{0,3}$/)
unless flag_name
comment "warn", nil, "ლ(ಠ益ಠლ) 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", nil, "y u no add test plan? ლ(ಠ益ಠლ)"
end
skip_eslint_flag = commit_message.include? "[skip-eslint]"
if skip_eslint_flag
comment "error",
nil,
"[skip-eslint] should only be used for large restructuring changes where no real code changes are actually made"
end