From e8f879e9f5d53aea7cd29ef064e25930323cb857 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Fri, 3 Oct 2025 20:33:21 -0400 Subject: [PATCH 01/10] Use LSAN_OPTIONS instead of ASAN_OPTIONS in mkmf Newer versions of clang's LSAN uses LSAN_OPTIONS environment variable instead of ASAN_OPTIONS. --- lib/mkmf.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mkmf.rb b/lib/mkmf.rb index d0974b05436928..201732d274ebf4 100644 --- a/lib/mkmf.rb +++ b/lib/mkmf.rb @@ -419,7 +419,7 @@ def expand_command(commands, envs = libpath_env) # disable ASAN leak reporting - conftest programs almost always don't bother # to free their memory. - envs['ASAN_OPTIONS'] = "detect_leaks=0" unless ENV.key?('ASAN_OPTIONS') + envs['LSAN_OPTIONS'] = "detect_leaks=0" unless ENV.key?('LSAN_OPTIONS') return envs, expand[commands] end From 269ada2421818c0216064271fd22a497bf266552 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 3 Oct 2025 22:38:04 -0700 Subject: [PATCH 02/10] Migrate notify-slack-commits.rb to ruby/ruby from ruby/git.ruby-lang.org as of: https://github.com/ruby/git.ruby-lang.org/commit/b0dfa734297cc9aea33f24a1e29f8853cc5761e9 --- .github/workflows/check_misc.yml | 11 +++- tool/notify-slack-commits.rb | 87 ++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 tool/notify-slack-commits.rb diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 7c745acf568202..4b62a937cbaede 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -28,7 +28,16 @@ jobs: # Skip overwriting MATZBOT_AUTO_UPDATE_TOKEN checkout: '' # false (ref: https://github.com/actions/runner/issues/2238) - # Run this step first to make sure auto-style commits are pushed + # Run this step first to make the notification available before any other failure + - name: Notify commit to Slack + run: ruby tool/notify-slack-commits.rb "$GITHUB_OLD_SHA" "$GITHUB_NEW_SHA" refs/heads/master + env: + GITHUB_OLD_SHA: ${{ github.event.before }} + GITHUB_NEW_SHA: ${{ github.event.after }} + if: ${{ github.repository == 'ruby/ruby' && github.ref == 'refs/heads/master' && github.event_name == 'push' }} + continue-on-error: true # The next auto-style should always run + + # Run this step early to make sure auto-style commits are pushed - name: Auto-correct code styles run: | set -x diff --git a/tool/notify-slack-commits.rb b/tool/notify-slack-commits.rb new file mode 100644 index 00000000000000..39a1ab71628b01 --- /dev/null +++ b/tool/notify-slack-commits.rb @@ -0,0 +1,87 @@ +#!/usr/bin/env ruby + +require "net/https" +require "open3" +require "json" +require "digest/md5" + +SLACK_WEBHOOK_URLS = [ + ENV.fetch("SLACK_WEBHOOK_URL").chomp, # ruby-lang#alerts + ENV.fetch("SLACK_WEBHOOK_URL_COMMITS").chomp, # ruby-lang#commits + ENV.fetch("SLACK_WEBHOOK_URL_RUBY_JP").chomp, # ruby-jp#ruby-commits +] +GRAVATAR_OVERRIDES = { + "nagachika@b2dd03c8-39d4-4d8f-98ff-823fe69b080e" => "https://avatars0.githubusercontent.com/u/21976", + "noreply@github.com" => "https://avatars1.githubusercontent.com/u/9919", + "nurse@users.noreply.github.com" => "https://avatars1.githubusercontent.com/u/13423", + "svn-admin@ruby-lang.org" => "https://avatars1.githubusercontent.com/u/29403229", + "svn@b2dd03c8-39d4-4d8f-98ff-823fe69b080e" => "https://avatars1.githubusercontent.com/u/29403229", + "usa@b2dd03c8-39d4-4d8f-98ff-823fe69b080e" => "https://avatars2.githubusercontent.com/u/17790", + "usa@ruby-lang.org" => "https://avatars2.githubusercontent.com/u/17790", + "yui-knk@ruby-lang.org" => "https://avatars0.githubusercontent.com/u/5356517", + "znz@users.noreply.github.com" => "https://avatars3.githubusercontent.com/u/11857", +} + +def escape(s) + s.gsub(/[&<>]/, "&" => "&", "<" => "<", ">" => ">") +end + +ARGV.each_slice(3) do |oldrev, newrev, refname| + out, = Open3.capture2("git", "rev-parse", "--symbolic", "--abbrev-ref", refname) + branch = out.strip + + out, = Open3.capture2("git", "log", "--pretty=format:%H\n%h\n%cn\n%ce\n%ct\n%B", "--abbrev=10", "-z", "#{oldrev}..#{newrev}") + + attachments = [] + out.split("\0").reverse_each do |s| + sha, sha_abbr, committer, committeremail, committertime, body = s.split("\n", 6) + subject, body = body.split("\n", 2) + + # Append notes content to `body` if it's notes + if refname.match(%r[\Arefs/notes/\w+\z]) + # `--diff-filter=AM -M` to exclude rename by git's directory optimization + object = IO.popen(["git", "diff", "--diff-filter=AM", "-M", "--name-only", "#{sha}^..#{sha}"], &:read).chomp + if md = object.match(/\A(?\h{2})\/?(?\h{38})\z/) + body = [body, IO.popen(["git", "notes", "show", md[:prefix] + md[:rest]], &:read)].join + end + end + + gravatar = GRAVATAR_OVERRIDES.fetch(committeremail) do + "https://www.gravatar.com/avatar/#{ Digest::MD5.hexdigest(committeremail.downcase) }" + end + + attachments << { + title: "#{ sha_abbr } (#{ branch }): #{ escape(subject) }", + title_link: "https://github.com/ruby/ruby/commit/#{ sha }", + text: escape((body || "").strip), + footer: committer, + footer_icon: gravatar, + ts: committertime.to_i, + color: '#24282D', + } + end + + # 100 attachments cannot be exceeded. 20 is recommended. https://api.slack.com/docs/message-attachments + attachments.each_slice(20).each do |attachments_group| + payload = { attachments: attachments_group } + + #Net::HTTP.post( + # URI.parse(SLACK_WEBHOOK_URL), + # JSON.generate(payload), + # "Content-Type" => "application/json" + #) + responses = SLACK_WEBHOOK_URLS.map do |url| + uri = URI.parse(url) + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.start do + req = Net::HTTP::Post.new(uri.path) + req.set_form_data(payload: payload.to_json) + http.request(req) + end + end + + results = responses.map { |resp| "#{resp.code} (#{resp.body})" }.join(', ') + puts "#{results} -- #{payload.to_json}" + end +end From ba48e6c9ca91653cf791e85cc1b245876cda18a4 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 3 Oct 2025 22:41:32 -0700 Subject: [PATCH 03/10] Propagate secrets to environment variables --- .github/workflows/check_misc.yml | 3 +++ tool/notify-slack-commits.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 4b62a937cbaede..3275e4ccc32945 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -34,6 +34,9 @@ jobs: env: GITHUB_OLD_SHA: ${{ github.event.before }} GITHUB_NEW_SHA: ${{ github.event.after }} + SLACK_WEBHOOK_URL_ALERTS: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_URL_COMMITS: ${{ secrets.SLACK_WEBHOOK_URL_COMMITS }} + SLACK_WEBHOOK_URL_RUBY_JP: ${{ secrets.SLACK_WEBHOOK_URL_RUBY_JP }} if: ${{ github.repository == 'ruby/ruby' && github.ref == 'refs/heads/master' && github.event_name == 'push' }} continue-on-error: true # The next auto-style should always run diff --git a/tool/notify-slack-commits.rb b/tool/notify-slack-commits.rb index 39a1ab71628b01..73e22b9a03a83c 100644 --- a/tool/notify-slack-commits.rb +++ b/tool/notify-slack-commits.rb @@ -6,7 +6,7 @@ require "digest/md5" SLACK_WEBHOOK_URLS = [ - ENV.fetch("SLACK_WEBHOOK_URL").chomp, # ruby-lang#alerts + ENV.fetch("SLACK_WEBHOOK_URL_ALERTS").chomp, # ruby-lang#alerts ENV.fetch("SLACK_WEBHOOK_URL_COMMITS").chomp, # ruby-lang#commits ENV.fetch("SLACK_WEBHOOK_URL_RUBY_JP").chomp, # ruby-jp#ruby-commits ] From 54c716dad6a7ce8a350176397469792b32a0f27a Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 3 Oct 2025 22:49:46 -0700 Subject: [PATCH 04/10] Fetch more commits to fix notify-slack-commits --- .github/workflows/check_misc.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 3275e4ccc32945..31d6695d446e85 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -20,6 +20,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: + fetch-depth: 100 # for notify-slack-commits token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} - uses: ./.github/actions/setup/directories From 63de26c4ec9514d54545fe55d41f060c0acacf20 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 3 Oct 2025 22:50:49 -0700 Subject: [PATCH 05/10] Run notify-slack-commits before `make up` too --- .github/workflows/check_misc.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 31d6695d446e85..9bcc6800967060 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -23,13 +23,7 @@ jobs: fetch-depth: 100 # for notify-slack-commits token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} - - uses: ./.github/actions/setup/directories - with: - makeup: true - # Skip overwriting MATZBOT_AUTO_UPDATE_TOKEN - checkout: '' # false (ref: https://github.com/actions/runner/issues/2238) - - # Run this step first to make the notification available before any other failure + # Run this step first (even before `make up` in the next step) to make the notification available before any other failure - name: Notify commit to Slack run: ruby tool/notify-slack-commits.rb "$GITHUB_OLD_SHA" "$GITHUB_NEW_SHA" refs/heads/master env: @@ -41,6 +35,12 @@ jobs: if: ${{ github.repository == 'ruby/ruby' && github.ref == 'refs/heads/master' && github.event_name == 'push' }} continue-on-error: true # The next auto-style should always run + - uses: ./.github/actions/setup/directories + with: + makeup: true + # Skip overwriting MATZBOT_AUTO_UPDATE_TOKEN + checkout: '' # false (ref: https://github.com/actions/runner/issues/2238) + # Run this step early to make sure auto-style commits are pushed - name: Auto-correct code styles run: | From 5941659e9b860a0a4bc4c64ef990f05a9dcebbe6 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 3 Oct 2025 22:54:24 -0700 Subject: [PATCH 06/10] Change the webhook URL used for alerts That secret appears use a different configuration from the intended one. --- .github/workflows/check_misc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 9bcc6800967060..5ff6c0b5b0e337 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -29,7 +29,7 @@ jobs: env: GITHUB_OLD_SHA: ${{ github.event.before }} GITHUB_NEW_SHA: ${{ github.event.after }} - SLACK_WEBHOOK_URL_ALERTS: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_URL_ALERTS: ${{ secrets.SLACK_WEBHOOK_URL_ALERTS }} SLACK_WEBHOOK_URL_COMMITS: ${{ secrets.SLACK_WEBHOOK_URL_COMMITS }} SLACK_WEBHOOK_URL_RUBY_JP: ${{ secrets.SLACK_WEBHOOK_URL_RUBY_JP }} if: ${{ github.repository == 'ruby/ruby' && github.ref == 'refs/heads/master' && github.event_name == 'push' }} From ecc5ebc69a76f3a267a90b9af3d6754b3cc21265 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 3 Oct 2025 23:09:43 -0700 Subject: [PATCH 07/10] Migrate notes-github-pr to ruby/ruby (#14725) from ruby/git.ruby-lang.org as of: https://github.com/ruby/git.ruby-lang.org/commit/f3ed893e946ec66cac77af5859ac879c5983d3a3 --- .github/workflows/check_misc.yml | 7 ++ tool/notes-github-pr.rb | 146 +++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 tool/notes-github-pr.rb diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 5ff6c0b5b0e337..d4da4ecc27b62f 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -132,6 +132,13 @@ jobs: name: ${{ steps.docs.outputs.htmlout }} if: ${{ steps.docs.outcome == 'success' }} + - name: Push PR notes to GitHub + run: ruby tool/notify-github-pr.rb "$(pwd)" "$GITHUB_OLD_SHA" "$GITHUB_NEW_SHA" refs/heads/master + env: + GITHUB_OLD_SHA: ${{ github.event.before }} + GITHUB_NEW_SHA: ${{ github.event.after }} + if: ${{ github.repository == 'ruby/ruby' && github.ref == 'refs/heads/master' && github.event_name == 'push' }} + - uses: ./.github/actions/slack with: SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot diff --git a/tool/notes-github-pr.rb b/tool/notes-github-pr.rb new file mode 100644 index 00000000000000..bc888f548e03c5 --- /dev/null +++ b/tool/notes-github-pr.rb @@ -0,0 +1,146 @@ +#!/usr/bin/env ruby +# Add GitHub pull request reference / author info to git notes. + +require 'net/http' +require 'uri' +require 'tmpdir' +require 'json' +require 'yaml' + +# Conversion for people whose GitHub account name and SVN_ACCOUNT_NAME are different. +GITHUB_TO_SVN = { + 'amatsuda' => 'a_matsuda', + 'matzbot' => 'git', + 'jeremyevans' => 'jeremy', + 'znz' => 'kazu', + 'k-tsj' => 'ktsj', + 'nurse' => 'naruse', + 'ioquatix' => 'samuel', + 'suketa' => 'suke', + 'unak' => 'usa', +} + +SVN_TO_EMAILS = YAML.safe_load(File.read(File.expand_path('../config/email.yml', __dir__))) + +class GitHub + ENDPOINT = URI.parse('https://api.github.com') + + def initialize(access_token) + @access_token = access_token + end + + # https://developer.github.com/changes/2019-04-11-pulls-branches-for-commit/ + def pulls(owner:, repo:, commit_sha:) + resp = get("/repos/#{owner}/#{repo}/commits/#{commit_sha}/pulls", accept: 'application/vnd.github.groot-preview+json') + JSON.parse(resp.body) + end + + # https://developer.github.com/v3/pulls/#get-a-single-pull-request + def pull_request(owner:, repo:, number:) + resp = get("/repos/#{owner}/#{repo}/pulls/#{number}") + JSON.parse(resp.body) + end + + # https://developer.github.com/v3/users/#get-a-single-user + def user(username:) + resp = get("/users/#{username}") + JSON.parse(resp.body) + end + + private + + def get(path, accept: 'application/vnd.github.v3+json') + Net::HTTP.start(ENDPOINT.host, ENDPOINT.port, use_ssl: ENDPOINT.scheme == 'https') do |http| + headers = { 'Accept': accept, 'Authorization': "bearer #{@access_token}" } + http.get(path, headers).tap(&:value) + end + end +end + +module Git + class << self + def abbrev_ref(refname, repo_path:) + git('rev-parse', '--symbolic', '--abbrev-ref', refname, repo_path: repo_path).strip + end + + def rev_list(arg, first_parent: false, repo_path: nil) + git('rev-list', *[('--first-parent' if first_parent)].compact, arg, repo_path: repo_path).lines.map(&:chomp) + end + + def commit_message(sha) + git('log', '-1', '--pretty=format:%B', sha) + end + + def notes_message(sha) + git('log', '-1', '--pretty=format:%N', sha) + end + + def committer_name(sha) + git('log', '-1', '--pretty=format:%cn', sha) + end + + def committer_email(sha) + git('log', '-1', '--pretty=format:%cE', sha) + end + + private + + def git(*cmd, repo_path: nil) + env = {} + if repo_path + env['GIT_DIR'] = repo_path + end + out = IO.popen(env, ['git', *cmd], &:read) + unless $?.success? + abort "Failed to execute: git #{cmd.join(' ')}\n#{out}" + end + out + end + end +end + +github = GitHub.new(ENV.fetch("GITHUB_TOKEN")) + +repo_path, *rest = ARGV +rest.each_slice(3).map do |oldrev, newrev, refname| + branch = Git.abbrev_ref(refname, repo_path: repo_path) + next if branch != 'master' # we use pull requests only for master branches + + Dir.mktmpdir do |workdir| + # Clone a branch and fetch notes + depth = Git.rev_list("#{oldrev}..#{newrev}", repo_path: repo_path).size + 50 + system('git', 'clone', "--depth=#{depth}", "--branch=#{branch}", "file://#{repo_path}", workdir) + Dir.chdir(workdir) + system('git', 'fetch', 'origin', 'refs/notes/commits:refs/notes/commits') + + updated = false + Git.rev_list("#{oldrev}..#{newrev}", first_parent: true).each do |sha| + github.pulls(owner: 'ruby', repo: 'ruby', commit_sha: sha).each do |pull| + number = pull.fetch('number') + url = pull.fetch('html_url') + next unless url.start_with?('https://github.com/ruby/ruby/pull/') + + # "Merged" notes for "Squash and merge" + message = Git.commit_message(sha) + notes = Git.notes_message(sha) + if !message.include?(url) && !message.match(/[ (]##{number}[) ]/) && !notes.include?(url) + system('git', 'notes', 'append', '-m', "Merged: #{url}", sha) + updated = true + end + + # "Merged-By" notes for "Rebase and merge" + if Git.committer_name(sha) == 'GitHub' && Git.committer_email(sha) == 'noreply@github.com' + username = github.pull_request(owner: 'ruby', repo: 'ruby', number: number).fetch('merged_by').fetch('login') + email = github.user(username: username).fetch('email') + email ||= SVN_TO_EMAILS[GITHUB_TO_SVN.fetch(username, username)]&.first + system('git', 'notes', 'append', '-m', "Merged-By: #{username}#{(" <#{email}>" if email)}", sha) + updated = true + end + end + end + + if updated + system('git', 'push', 'origin', 'refs/notes/commits') + end + end +end From e40d3c5bd8d8c1a26a75717aa7fbe39622a715bb Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 3 Oct 2025 23:16:16 -0700 Subject: [PATCH 08/10] Fix the path of notes-github-pr --- .github/workflows/check_misc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index d4da4ecc27b62f..347e8a9b1984bf 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -133,7 +133,7 @@ jobs: if: ${{ steps.docs.outcome == 'success' }} - name: Push PR notes to GitHub - run: ruby tool/notify-github-pr.rb "$(pwd)" "$GITHUB_OLD_SHA" "$GITHUB_NEW_SHA" refs/heads/master + run: ruby tool/notes-github-pr.rb "$(pwd)" "$GITHUB_OLD_SHA" "$GITHUB_NEW_SHA" refs/heads/master env: GITHUB_OLD_SHA: ${{ github.event.before }} GITHUB_NEW_SHA: ${{ github.event.after }} From 4ea84bf58b3690c3e7e61cbf30ad6252863ce133 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 3 Oct 2025 23:29:56 -0700 Subject: [PATCH 09/10] Fix a missing reference to config/email.yml --- tool/notes-github-pr.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tool/notes-github-pr.rb b/tool/notes-github-pr.rb index bc888f548e03c5..2bea10575ba0ff 100644 --- a/tool/notes-github-pr.rb +++ b/tool/notes-github-pr.rb @@ -20,7 +20,8 @@ 'unak' => 'usa', } -SVN_TO_EMAILS = YAML.safe_load(File.read(File.expand_path('../config/email.yml', __dir__))) +EMAIL_YML_URL = 'https://raw.githubusercontent.com/ruby/git.ruby-lang.org/refs/heads/master/config/email.yml' +SVN_TO_EMAILS = YAML.safe_load(Net::HTTP.get_response(URI(EMAIL_YML_URL)).tap(&:value).body) class GitHub ENDPOINT = URI.parse('https://api.github.com') From 72f8e3e71b1e1e816f4f7fbaf7e4fafdeca87881 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 3 Oct 2025 23:38:55 -0700 Subject: [PATCH 10/10] Make sure GITHUB_TOKEN is set --- .github/workflows/check_misc.yml | 1 + tool/notes-github-pr.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 347e8a9b1984bf..b332eef541468f 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -137,6 +137,7 @@ jobs: env: GITHUB_OLD_SHA: ${{ github.event.before }} GITHUB_NEW_SHA: ${{ github.event.after }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} if: ${{ github.repository == 'ruby/ruby' && github.ref == 'refs/heads/master' && github.event_name == 'push' }} - uses: ./.github/actions/slack diff --git a/tool/notes-github-pr.rb b/tool/notes-github-pr.rb index 2bea10575ba0ff..e282386eba15d4 100644 --- a/tool/notes-github-pr.rb +++ b/tool/notes-github-pr.rb @@ -100,7 +100,7 @@ def git(*cmd, repo_path: nil) end end -github = GitHub.new(ENV.fetch("GITHUB_TOKEN")) +github = GitHub.new(ENV.fetch('GITHUB_TOKEN')) repo_path, *rest = ARGV rest.each_slice(3).map do |oldrev, newrev, refname|