Top Level Namespace

Defined Under Namespace

Modules: Gergich, Logging

Constant Summary collapse

GERGICH_REVIEW_LABEL =
ENV.fetch("GERGICH_REVIEW_LABEL", "Code-Review")
GERGICH_USER =
ENV.fetch("GERGICH_USER", "gergich")
GERGICH_GIT_PATH =
ENV.fetch("GERGICH_GIT_PATH", ".")
GergichError =
Class.new(StandardError)
CI_TEST_ARGS =
{
  "comment" => [
    [
      { path: "foo.rb", position: 3, severity: "error", message: "ಠ_ಠ" },
      { path: "/COMMIT_MSG", position: 1, severity: "info", message: "cool story bro" },
      { path: "/COMMIT_MSG", severity: "info", message: "lol",
        position: { start_line: 1, start_character: 1, end_line: 1, end_character: 2 } },
      { path: "/COMMIT_MSG", severity: "info", message: "more",
        position: { start_line: 1, start_character: 5, end_line: 1, end_character: 5 } }
    ].to_json
  ],
  "label" => ["Code-Review", 1],
  "message" => ["this is a test"],
  "capture" => ["rubocop", format("echo %<output>s", output: Shellwords.escape(<<~OUTPUT))]
    bin/gergich:47:8: C: Prefer double-quoted strings
    if ENV['DEBUG']
           ^^^^^^^
    1 file inspected, 35 offenses detected, 27 offenses auto-correctable
  OUTPUT
}.freeze
MASTER_BOUNCER_REVIEW_LABEL =
ENV.fetch("GERGICH_REVIEW_LABEL", "Code-Review")
PROJECT =
ENV["GERRIT_PROJECT"] || error("no GERRIT_PROJECT set")
MAIN_BRANCH =

on Jenkins GERRIT_MAIN_BRANCH env var will default to empty string if not set

ENV["GERRIT_MAIN_BRANCH"]
WARN_DISTANCE =

TODO: time-based thresholds

ENV.fetch("MASTER_BOUNCER_WARN_DISTANCE", 50).to_i
ERROR_DISTANCE =
ENV.fetch("MASTER_BOUNCER_ERROR_DISTANCE", 100).to_i

Instance Method Summary collapse

Instance Method Details

#error(text) ⇒ Object



24
25
26
27
28
# File 'lib/gergich/cli.rb', line 24

def error(text)
  $stderr.puts "\e[31mError:\e[0m #{text}"
  $stderr.puts usage
  exit 1
end

#help_command(commands) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/gergich/cli.rb', line 30

def help_command(commands)
  {
    action: ->(subcommand = "help") {
      subcommand_info = commands[subcommand]
      if !subcommand_info
        error "Unrecognized command `#{subcommand}`"
      elsif (help_text = subcommand_info[:help])
        info help_text.respond_to?(:call) ? help_text.call : help_text
      else
        error "No help available for `#{subcommand}`"
      end
    },
    help: -> {
      indentation = commands.keys.map(&:size).max
      commands_help = commands
        .to_a
        .sort_by(&:first)
        .map { |key, data|
          "#{key.ljust(indentation)} - #{data[:summary]}" if data[:summary]
        }
        .compact
      usage(commands_help.join("\n"))
    }
  }
end

#info(text) ⇒ Object



9
10
11
12
# File 'lib/gergich/cli.rb', line 9

def info(text)
  puts text
  exit
end

#maybe_bounce_commit!(commit) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/gergich/cli/master_bouncer.rb', line 29

def maybe_bounce_commit!(commit)
  draft = Gergich::Draft.new(commit)
  draft.reset!

  distance = Gergich.git("rev-list origin/#{MAIN_BRANCH} ^#{commit.ref} --count").to_i
  detail = "#{distance} commits behind #{MAIN_BRANCH}"

  score = 0
  message = nil
  if distance > ERROR_DISTANCE
    score = -2
    message = "This commit is probably not safe to merge (#{detail}). You'll " \
              "need to rebase it to ensure all the tests still pass."
  elsif distance > WARN_DISTANCE
    score = -1
    message = "This commit may not be safe to merge (#{detail}). Please " \
              "rebase to make sure all the tests still pass."
  end

  review = Gergich::Review.new(commit, draft)
  current_score = review.current_score

  puts "#{detail}, " + (score == current_score ?
                        "score still #{score}" :
                        "changing score from #{current_score} to #{score}")

  # since we run on a daily cron, we might be checking the same patchset
  # many times, so bail if nothing has changed
  return if score == current_score

  draft.add_label MASTER_BOUNCER_REVIEW_LABEL, score
  draft.add_message message if message

  # otherwise we always publish ... even in the score=0 case it's
  # important, as we might be undoing a previous negative score.
  # similarly, over time the same patchset will become more out of date,
  # so we allow_repost (so to speak) so we can add increasingly negative
  # reviews
  review.publish!(allow_repost: true)
end

#potentially_mergeable_changesObject



19
20
21
22
23
24
25
26
27
# File 'lib/gergich/cli/master_bouncer.rb', line 19

def potentially_mergeable_changes
  url = "/changes/?q=status:open+" \
        "p:#{PROJECT}+" \
        "label:Verified=1+" \
        "branch:#{MAIN_BRANCH}" \
        "&o=CURRENT_REVISION"
  changes = Gergich::API.get(url)
  changes.reject { |c| c["subject"] =~ /\Awip($|\W)/i }
end

#run_app(commands) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/gergich/cli.rb', line 68

def run_app(commands)
  commands["help"] = help_command(commands)
  command = ARGV.shift || "help"

  if commands[command]
    begin
      action = commands[command][:action]
      run_command(action)
    rescue GergichError
      error $ERROR_INFO.message
    rescue StandardError
      error "Unhandled exception: #{$ERROR_INFO}\n#{$ERROR_INFO.backtrace.join("\n")}"
    end
  else
    error "Unrecognized command `#{command}`"
  end
end

#run_ci_test!(all_commands) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/gergich/cli/gergich.rb', line 27

def run_ci_test!(all_commands)
  ENV["GERGICH_CHANGE_NAME"] = "0"
  commands_to_test = all_commands - %w[citest reset publish status]
  commands_to_test << "status" # put it at the end, so we maximize the stuff it tests

  commands = commands_to_test.map { |command| [command, CI_TEST_ARGS[command] || []] }
  commands.concat(all_commands.map { |command| ["help", [command]] })

  # after running our test commands, reset and publish frd:
  commands << ["reset"]
  # Note that while gergich should always be able to vote on these labels, he may be used to
  # vone on other branches depending on project usage and project-specific permissions
  commands << ["label", ["Lint-Review", 1]]
  commands << ["label", ["Code-Review", 1]] # Not all projects have Lint-Review
  commands << ["message", ["\`gergich citest\` checks out :thumbsup: :mj:"]]
  commands << ["publish"]

  commands.each do |command, args = []|
    arglist = args.map { |arg| Shellwords.escape(arg.to_s) }
    output = `bundle exec gergich #{command} #{arglist.join(" ")} 2>&1`
    unless $CHILD_STATUS.success?
      error "`gergich citest` failed on step `#{command}`:\n\n#{output.gsub(/^/, '  ')}\n"
    end
  end
end

#run_command(action) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
# File 'lib/gergich/cli.rb', line 56

def run_command(action)
  params = action.parameters
  params.each_with_index do |(type, name), i|
    error "No <#{name}> specified" if i >= ARGV.size && type == :req
  end
  if ARGV.size > params.size
    extra_args = ARGV[params.size, ARGV.size].map { |a| "`#{Shellwords.escape(a)}`" }
    error "Extra arg(s) #{extra_args.join(' ')}"
  end
  action.call(*ARGV)
end

#script_nameObject



14
15
16
# File 'lib/gergich/cli.rb', line 14

def script_name
  $PROGRAM_NAME.sub(%r{.*/}, "")
end

#usage(content = nil) ⇒ Object



18
19
20
21
22
# File 'lib/gergich/cli.rb', line 18

def usage(content = nil)
  "Usage: #{script_name} <command> [<args>...]\n" +
    (content ? "\n#{content}\n\n" : "") +
    "Tip: run `#{script_name} help <command>` for more info"
end