#!/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_permissions_registry_file = commit_files.any? { |f| f.start_with?("config/initializers/permissions_registry") } if touches_permissions_registry_file comment "info", nil, "Your commit modifies the permissions registry file. Please review and follow the instructions in https://instructure.atlassian.net/wiki/spaces/CE/pages/85519794197/Users+Roles+and+Permissions, including adding an additional migration to backfill those permission(s)." end touches_brandable_variables_file = commit_files.any? { |f| f.start_with?("app/stylesheets/brandable_variables") } if touches_brandable_variables_file comment "warn", nil, "Changes were made to the brandable variables file. You you will need to rename the corresponding database migration file that runs brand_configs. See the following migration file for more info: #{BrandableCSS::MIGRATION_NAME.underscore}_predeploy.rb." 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