diff --git a/.github/workflows/auto_review_pr.yml b/.github/workflows/auto_review_pr.yml new file mode 100644 index 00000000000000..c8095dfd8e34a0 --- /dev/null +++ b/.github/workflows/auto_review_pr.yml @@ -0,0 +1,33 @@ +name: Auto Review PR +on: + pull_request_target: + types: [opened, ready_for_review, reopened] + branches: [master] + +permissions: + contents: read + +jobs: + auto-review-pr: + name: Auto Review PR + runs-on: ubuntu-latest + if: ${{ github.repository == 'ruby/ruby' && github.base_ref == 'master' }} + + permissions: + pull-requests: write + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@ab177d40ee5483edb974554986f56b33477e21d0 # v1.265.0 + with: + ruby-version: '3.4' + bundler: none + + - name: Auto Review PR + run: ruby tool/auto_review_pr.rb "$GITHUB_PR_NUMBER" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/sync_default_gems.yml b/.github/workflows/sync_default_gems.yml index 2f0b4d8865eb4f..1a9b43ec5241a9 100644 --- a/.github/workflows/sync_default_gems.yml +++ b/.github/workflows/sync_default_gems.yml @@ -56,7 +56,7 @@ jobs: - name: Push run: | - git pull --ff-only origin ${GITHUB_REF#refs/heads/} + git pull --rebase origin ${GITHUB_REF#refs/heads/} git push origin ${GITHUB_REF#refs/heads/} if: ${{ steps.sync.outputs.update }} diff --git a/common.mk b/common.mk index fe8ef3b19efe43..093fea6b3fb56c 100644 --- a/common.mk +++ b/common.mk @@ -624,15 +624,20 @@ html: PHONY main srcs-doc @echo Generating RDoc HTML files $(Q) $(RDOC) --op "$(HTMLOUT)" $(RDOC_GEN_OPTS) $(RDOCFLAGS) . +RDOC_COVERAGE_EXCLUDES = -x ^ext/json -x ^ext/openssl -x ^ext/psych \ + -x ^lib/bundler -x ^lib/rubygems \ + -x ^lib/did_you_mean -x ^lib/error_highlight -x ^lib/syntax_suggest + rdoc-coverage: PHONY main srcs-doc @echo Generating RDoc coverage report - $(Q) $(RDOC) --quiet -C $(RDOCFLAGS) . + $(Q) $(RDOC) --quiet -C $(RDOCFLAGS) $(RDOC_COVERAGE_EXCLUDES) . undocumented: PHONY main srcs-doc - $(Q) $(RDOC) --quiet -C $(RDOCFLAGS) . | \ + $(Q) $(RDOC) --quiet -C $(RDOCFLAGS) $(RDOC_COVERAGE_EXCLUDES) . | \ sed -n \ -e '/^ *# in file /{' -e 's///;N;s/\n/: /p' -e '}' \ - -e 's/^ *\(.*[^ ]\) *# in file \(.*\)/\2: \1/p' | sort + -e 's/^ *\(.*[^ ]\) *# in file \(.*\)/\2: \1/p' | \ + sort -t: -k1,1 -k2n,2 RDOCBENCHOUT=/tmp/rdocbench diff --git a/ext/coverage/lib/coverage.rb b/ext/coverage/lib/coverage.rb index f1923ef366cbf3..4bd20e22cbadbe 100644 --- a/ext/coverage/lib/coverage.rb +++ b/ext/coverage/lib/coverage.rb @@ -1,6 +1,11 @@ require "coverage.so" module Coverage + # call-seq: + # line_stub(file) -> array + # + # A simple helper function that creates the "stub" of line coverage + # from a given source code. def self.line_stub(file) lines = File.foreach(file).map { nil } iseqs = [RubyVM::InstructionSequence.compile_file(file)] diff --git a/ext/objspace/object_tracing.c b/ext/objspace/object_tracing.c index 0156642ef25897..63eec6739320f9 100644 --- a/ext/objspace/object_tracing.c +++ b/ext/objspace/object_tracing.c @@ -411,6 +411,13 @@ object_allocations_reporter(FILE *out, void *ptr) fprintf(out, "== object_allocations_reporter: END\n"); } +/* + * call-seq: trace_object_allocations_debug_start + * + * Starts tracing object allocations for GC debugging. + * If you encounter the BUG "... is T_NONE" (and so on) on your + * application, please try this method at the beginning of your app. + */ static VALUE trace_object_allocations_debug_start(VALUE self) { diff --git a/gc/mmtk/mmtk.c b/gc/mmtk/mmtk.c index 9dd3129e01664e..cc0b1afd4d2051 100644 --- a/gc/mmtk/mmtk.c +++ b/gc/mmtk/mmtk.c @@ -1022,16 +1022,20 @@ rb_gc_impl_shutdown_call_finalizer(void *objspace_ptr) gc_run_finalizers(objspace); } - struct MMTk_RawVecOfObjRef registered_candidates = mmtk_get_all_obj_free_candidates(); - for (size_t i = 0; i < registered_candidates.len; i++) { - VALUE obj = (VALUE)registered_candidates.ptr[i]; - - if (rb_gc_shutdown_call_finalizer_p(obj)) { - rb_gc_obj_free(objspace_ptr, obj); - RBASIC(obj)->flags = 0; + unsigned int lev = RB_GC_VM_LOCK(); + { + struct MMTk_RawVecOfObjRef registered_candidates = mmtk_get_all_obj_free_candidates(); + for (size_t i = 0; i < registered_candidates.len; i++) { + VALUE obj = (VALUE)registered_candidates.ptr[i]; + + if (rb_gc_shutdown_call_finalizer_p(obj)) { + rb_gc_obj_free(objspace_ptr, obj); + RBASIC(obj)->flags = 0; + } } + mmtk_free_raw_vec_of_obj_ref(registered_candidates); } - mmtk_free_raw_vec_of_obj_ref(registered_candidates); + RB_GC_VM_UNLOCK(lev); gc_run_finalizers(objspace); } diff --git a/lib/net/http.rb b/lib/net/http.rb index 43e3349ac0e1b1..7efb468e782e06 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -1825,6 +1825,8 @@ def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_use_ss } end + # :startdoc: + class << HTTP # Returns true if self is a class which was created by HTTP::Proxy. def proxy_class? @@ -1948,6 +1950,7 @@ def edit_path(path) path end end + # :startdoc: # # HTTP operations diff --git a/tool/auto_review_pr.rb b/tool/auto_review_pr.rb new file mode 100755 index 00000000000000..c63640354d3593 --- /dev/null +++ b/tool/auto_review_pr.rb @@ -0,0 +1,104 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'json' +require 'net/http' +require 'uri' +require_relative './sync_default_gems' + +class GitHubAPIClient + def initialize(token) + @token = token + end + + def get(path) + response = Net::HTTP.get_response(URI("https://api.github.com#{path}"), { + 'Authorization' => "token #{@token}", + 'Accept' => 'application/vnd.github.v3+json', + }).tap(&:value) + JSON.parse(response.body, symbolize_names: true) + end + + def post(path, body = {}) + body = JSON.dump(body) + response = Net::HTTP.post(URI("https://api.github.com#{path}"), body, { + 'Authorization' => "token #{@token}", + 'Accept' => 'application/vnd.github.v3+json', + 'Content-Type' => 'application/json', + }).tap(&:value) + JSON.parse(response.body, symbolize_names: true) + end +end + +class AutoReviewPR + REPO = 'ruby/ruby' + + COMMENT_USER = 'github-actions[bot]' + COMMENT_PREFIX = 'The following files are maintained in the following upstream repositories:' + COMMENT_SUFFIX = 'Please file a pull request to the above instead. Thank you!' + + def initialize(client) + @client = client + end + + def review(pr_number) + # Fetch the list of files changed by the PR + changed_files = @client.get("/repos/#{REPO}/pulls/#{pr_number}/files").map { it.fetch(:filename) } + + # Build a Hash: { upstream_repo => files, ... } + upstream_repos = changed_files.group_by { |file| find_upstream_repo(file) } + upstream_repos.delete(nil) # exclude no-upstream files + upstream_repos.delete('prism') if changed_files.include?('prism_compile.c') # allow prism changes in this case + if upstream_repos.empty? + puts "Skipped: The PR ##{pr_number} doesn't have upstream repositories." + return + end + + # Check if the PR is already reviewed + existing_comments = @client.get("/repos/#{REPO}/issues/#{pr_number}/comments") + existing_comments.map! { [it.fetch(:user).fetch(:login), it.fetch(:body)] } + if existing_comments.any? { |user, comment| user == COMMENT_USER && comment.start_with?(COMMENT_PREFIX) } + puts "Skipped: The PR ##{pr_number} already has an automated review comment." + return + end + + # Post a comment + comment = format_comment(upstream_repos) + result = @client.post("/repos/#{REPO}/issues/#{pr_number}/comments", { body: comment }) + puts "Success: #{JSON.pretty_generate(result)}" + end + + private + + def find_upstream_repo(file) + SyncDefaultGems::REPOSITORIES.each do |repo_name, repository| + repository.mappings.each do |_src, dst| + if file.start_with?(dst) + return repo_name + end + end + end + nil + end + + # upstream_repos: { upstream_repo => files, ... } + def format_comment(upstream_repos) + comment = +'' + comment << "#{COMMENT_PREFIX}\n\n" + + upstream_repos.each do |upstream_repo, files| + comment << "* https://github.com/ruby/#{upstream_repo}\n" + files.each do |file| + comment << " * #{file}\n" + end + end + + comment << "\n#{COMMENT_SUFFIX}" + comment + end +end + +pr_number = ARGV[0] || abort("Usage: #{$0} ") +client = GitHubAPIClient.new(ENV.fetch('GITHUB_TOKEN')) + +AutoReviewPR.new(client).review(pr_number) diff --git a/tool/sync_default_gems.rb b/tool/sync_default_gems.rb index b2509f0062b0b5..997cd5eddc8f16 100755 --- a/tool/sync_default_gems.rb +++ b/tool/sync_default_gems.rb @@ -61,6 +61,7 @@ def lib((upstream, branch), gemspec_in_subdir: false) ]) end + # Note: tool/auto_review_pr.rb also depends on this constant. REPOSITORIES = { "io-console": repo("ruby/io-console", [ ["ext/io/console", "ext/io/console"],