1
1
mirror of https://github.com/github/semantic.git synced 2024-12-22 22:31:36 +03:00
semantic/temp.rb

1117 lines
36 KiB
Ruby
Raw Normal View History

2017-02-10 08:31:55 +03:00
# rubocop:disable Style/FrozenStringLiteralComment
# rubocop:disable Rails/Render
class PullRequestsController < AbstractRepositoryController
areas_of_responsibility :code_collab, :pull_requests
include ShowPartial, PrefetchHelper, ProgressiveTimeline, DiscussionPreloadHelper,
ControllerMethods::Diffs
helper :compare
around_filter :select_up_to_date_database, only: :show_partial
before_filter :login_required, only: [:create, :comment, :merge_button, :sync, :dismiss_protip]
before_filter :login_required_redirect_for_public_repo, only: [:new]
before_filter :non_migrating_repository_required,
except: [:new, :show, :show_partial, :merge_button, :diff, :patch, :merge_button_matrix]
before_filter :check_for_empty_repository, only: [:create, :new]
before_filter :ensure_pull_head_pushable, only: [:cleanup, :undo_cleanup, :sync]
before_filter :content_authorization_required, only: [:create, :merge]
skip_before_filter :cap_pagination, unless: :robot?
layout :repository_layout
param_encoding :create, :base, "ASCII-8BIT"
param_encoding :create, :head, "ASCII-8BIT"
def new
redirect_to compare_path(current_repository, params[:range], true)
end
def create
repo = current_repository
return render_404 unless repo
return if reject_bully_for?(repo)
params[:pull_request] ||= {}
options = params.slice(:base, :head)
options[:user] = current_user
params[:issue] ||= {}
params[:issue][:title] = params[:pull_request][:title]
params[:issue][:body] = params[:pull_request][:body]
options[:issue] = build_issue
options[:collab_privs] = !!params[:collab_privs]
options[:reviewer_ids] = params[:reviewer_ids]
begin
@pull_request = PullRequest.create_for!(repo, options)
@comparison = @pull_request.comparison
@issue = @pull_request.issue
GitHub.instrument "pull_request.create", user: current_user
instrument_issue_creation_via_ui(@pull_request)
instrument_saved_reply_use(params[:saved_reply_id], "pull_request")
# Keep track of if a pull request targets the repo's
# default branch, or one in progress.
if options[:base].strip == "#{repo.owner}:#{repo.default_branch}"
GitHub.stats.increment("pullrequest.target.default")
else
GitHub.stats.increment("pullrequest.target.other")
end
if params[:quick_pull].present?
type = @pull_request.cross_repo? ? "cross" : "same"
GitHub.stats.increment("pullrequest.quick.total.create")
GitHub.stats.increment("pullrequest.quick.#{type}.create")
end
redirect_to pull_request_path(@pull_request, repo)
rescue ActiveRecord::RecordInvalid => e
flash[:error] = "Pull request creation failed. #{human_failure_message(e)}"
range = [options[:base], options[:head]].compact.join("...")
redirect_to compare_path(repo, range, true)
end
end
def show
@mobile_view_available = true
redirect_or_error = ensure_valid_pull_request
if performed?
return redirect_or_error
end
if params[:range]
env["pull_request.timeout_reason"] = "compute_diff"
if oids = parse_show_range_oid_components(@pull, params[:range])
oid1, oid2 = oids
else
return render template: "pull_requests/bad_range", status: :not_found
end
allowed_commits = @pull.changed_commit_oids
if !(oid1.nil? || allowed_commits.include?(oid1)) || !allowed_commits.include?(oid2)
return render template: "pull_requests/bad_range", status: :not_found
end
if oid1 && (oid1 == oid2 || !@pull.repository.rpc.descendant_of([[oid2, oid1]]).values.first)
return render template: "pull_requests/bad_range", status: :not_found
end
expected_canonical_range = oid1 ? "#{oid1}..#{oid2}" : "#{oid2}"
if params[:range] != expected_canonical_range
return redirect_to(range: expected_canonical_range)
end
if params[:tab] == "commits"
@specified_tab = "files"
oid1 = current_repository.commits.find(oid2).parent_oids.first
end
@comparison = @pull.historical_comparison
merge_base_oid = @comparison.compare_repository.rpc.best_merge_base(@pull.base_sha, oid2)
if merge_base_oid.nil?
return render template: "pull_requests/orphan_commit", status: :not_found, locals: { oid2: oid2 }
end
oid1 ||= @pull.compare_repository.rpc.best_merge_base(oid2, merge_base_oid) if merge_base_oid
unless @pull_comparison = PullRequest::Comparison.find(pull: @pull, start_commit_oid: oid1, end_commit_oid: oid2, base_commit_oid: merge_base_oid)
return render template: "pull_requests/bad_range", status: :not_found
end
load_diff
env["pull_request.timeout_reason"] = nil
else
@comparison = @pull.comparison
if specified_tab == "files"
env["pull_request.timeout_reason"] = "compute_diff"
start_oid, end_oid = @pull.merge_base, @pull.head_sha
if start_oid && end_oid
begin
start_commit, end_commit = @pull.compare_repository.commits.find([start_oid, end_oid])
@pull_comparison = PullRequest::Comparison.new(pull: @pull, start_commit: start_commit, end_commit: end_commit, base_commit: start_commit)
load_diff
rescue GitRPC::ObjectMissing
end
end
env["pull_request.timeout_reason"] = nil
end
end
prefetch_deferred do
if specified_tab == "files" && @pull_comparison && logged_in?
@pull_comparison.mark_as_seen(user: current_user)
mark_thread_as_read @pull.issue
elsif specified_tab == "discussion"
mark_thread_as_read @pull.issue
end
end
if prefetch_viewed?
return head(:no_content)
end
@labels = current_repository.sorted_labels
prepare_for_rendering(timeline: specified_tab == "discussion", pull_comparison: @pull_comparison)
unless "discussion" == specified_tab
override_analytics_location "/<user-name>/<repo-name>/pull_requests/show/#{specified_tab}"
end
if show_mobile_view? && (request_category != "raw")
return render_template_view("mobile/pull_requests/show", Mobile::PullRequests::ShowPageView, {
pull: @pull,
diffs: @pull_comparison.try(:diffs),
tab: specified_tab,
visible_timeline_items: visible_timeline_items,
showing_full_timeline: showing_full_timeline?
}, layout: "mobile/application")
end
respond_to do |format|
format.html do
if params[:shdds]
@syntax_highlighted_diffs_forced = true
render_to_string
head :no_content
else
if logged_in?
@current_review = @pull.latest_pending_review_for(current_user)
end
if specified_tab == "files"
if @pull_comparison.nil?
render "pull_requests/files_unavailable"
else
render "pull_requests/files"
end
elsif specified_tab == "commits"
render "pull_requests/commits"
else
render "pull_requests/conversation"
end
end
end
end
end
# Build enough of a pull request object to render the requested reviewer
# sidebar template on the pull request create page.
#
# Params: reviewer_ids - An array of User IDs.
#
# Returns an unsaved PullRequest.
def build_pull
pull =
if params[:range].present? && suggested_reviewers_enabled?
comparison = GitHub::Comparison.from_range(current_repository, params[:range])
if comparison.valid? && comparison.viewable_by?(current_user)
comparison.build_pull_request(user: current_user)
end
else
current_repository.pull_requests.build(user: current_user)
end
pull.issue = current_repository.issues.build
if current_repository.pushable_by?(current_user)
if reviewer_ids = params[:reviewer_ids]
reviewer_ids.select(&:present?).each do |id|
pull.review_requests.build(reviewer_id: id)
end
end
end
pull
end
TIMELINE_PARTIALS = %w[
pull_requests/timeline
pull_requests/timeline_marker
].freeze
VALID_REASONS = %w[
view-more-button
expose-fragment
].freeze
def show_partial
partial = params[:partial]
return head :not_found unless valid_partial?(partial)
GitHub.stats.time("pullrequest.show_partial_find") do
@pull =
if params[:id]
PullRequest.find_by_number_and_repo(params[:id].to_i, current_repository)
else
build_pull
end
end
return head :not_found unless @pull
if partial == "pull_requests/timeline"
if focused_timeline? && visible_timeline_items.empty?
return head :bad_request
end
reason = VALID_REASONS.include?(params[:reason]) ? params[:reason] : "other"
GitHub.stats.increment("pullrequest.show_partial.timeline.#{reason}.count")
end
if partial == "issues/sidebar/new/reviewers"
if suggested_reviewers_enabled?
suggested_reviewers = @pull.suggested_reviewers
end
else
prepare_for_rendering(timeline: TIMELINE_PARTIALS.include?(partial), pull_comparison: nil)
end
GitHub.stats.time("pullrequest.show_partial_render") do
respond_to do |format|
format.html do
render partial: partial, object: @pull, layout: false, locals: {
pull: @pull,
merge_type: params[:merge_type],
suggested_reviewers: suggested_reviewers
}
end
end
end
end
def short_method
"this just returns this string"
end
def show_partial_commit
partial = params[:partial]
return head :not_found unless valid_partial?(partial)
unless pull = PullRequest.find_by_number_and_repo(params[:id].to_i, current_repository)
return head :not_found
end
unless commit = current_commit
return head :not_found
end
Commit.prefill_combined_statuses([current_commit], current_repository)
GitHub.stats.time("pullrequest.show_partial_render") do
respond_to do |format|
format.html do
render partial: partial, object: commit, layout: false, locals: { pull: pull, commit: commit }
end
end
end
end
def show_partial_comparison
partial = params[:partial]
return head :not_found unless valid_partial?(partial)
unless pull = PullRequest.find_by_number_and_repo(params[:id].to_i, current_repository)
return head :not_found
end
unless pull_comparison = PullRequest::Comparison.find(pull: pull, start_commit_oid: params[:start_commit_oid], end_commit_oid: params[:end_commit_oid], base_commit_oid: params[:base_commit_oid])
return head :not_found
end
GitHub.stats.time("pullrequest.show_partial_render") do
respond_to do |format|
format.html do
render partial: partial, locals: { pull: pull, pull_comparison: pull_comparison }
end
end
end
end
def show_toc
pull = PullRequest.find_by_number_and_repo(params[:id], current_repository)
return head :not_found unless pull
return head :not_found unless valid_sha_param?(:sha1) &&
valid_sha_param?(:sha2) && params[:sha2].present? &&
valid_sha_param?(:base_sha)
diff_options = { base_sha: params[:base_sha] }
diff = GitHub::Diff.new(pull.compare_repository, params[:sha1], params[:sha2], diff_options)
respond_to do |format|
format.html do
render partial: "pull_requests/diffbar/toc_menu_items",
layout: false,
locals: {
summary_delta_views: diff.summary.deltas.map { |d| Diff::SummaryDeltaView.new(d) },
diff: diff
}
end
end
end
def cleanup
stats_key = ["pullrequest",
"delete_button",
@pull.cross_repo? ? "cross_repo" : "same_repo"].join(".")
# The branch was deleted by someone else before this user
# clicked the button. Send us to the bottom.
if !@pull.head_ref_exist?
raise Git::Ref::NotFound
end
result = @pull.cleanup_head_ref(current_user,
request_reflog_data("pull request branch delete button"))
GitHub.stats.increment("#{stats_key}.#{result ? "success" : "error"}")
if request.xhr?
render_head_ref_update
else
if result
flash[:notice] = "Branch deleted successfully."
else
flash[:error] = "Oops, something went wrong."
end
redirect_to pull_request_path(@pull)
end
# We can get here both from above where the branch has already
# been deleted before the button is clicked, or a race condition
# where the application code thinks the branch exists but by
# the time we execute the git command, someone else has already deleted it.
rescue Git::Ref::NotFound
GitHub.stats.increment("#{stats_key}.already_deleted")
render_head_ref_update
end
def undo_cleanup
stats_key = ["pullrequest",
"delete_button_undo",
@pull.cross_repo? ? "cross_repo" : "same_repo"].join(".")
if @pull.restore_head_ref(current_user,
request_reflog_data("pull request branch undo button"))
GitHub.stats.increment("#{stats_key}.success")
else
GitHub.stats.increment("#{stats_key}.error")
end
render_head_ref_update
end
def merge
@pull = current_repository.issues.find_by_number(params[:id].to_i).try(:pull_request)
return render_404 unless @pull
if params[:squash_commits] == "1"
merge_method = "squash"
elsif params[:do].present?
merge_method = params[:do]
else
if current_repository.merge_commit_allowed?
merge_method = "merge"
else
merge_method = "squash"
end
end
merge_method_allowed = case merge_method
when "merge"
current_repository.merge_commit_allowed?
when "squash"
current_repository.squash_merge_allowed?
when "rebase"
current_repository.rebase_merge_allowed?
else
false
end
if !merge_method_allowed
result, message = nil, "The selected merge method (#{merge_method}) is not allowed."
elsif @pull.git_merges_cleanly? and @pull.base_repository.pushable_by?(current_user)
GitHub.stats.increment("pullrequest.merge_button.click")
begin
result, message = @pull.merge(current_user,
message_title: params[:commit_title],
message: params[:commit_message],
reflog_data: request_reflog_data("pull request merge button"),
expected_head: params[:head_sha],
method: merge_method.to_sym)
rescue Git::Ref::HookFailed => e
@hook_out = e.message
result, message = nil, "Pre-receive hooks failed. See below for details."
end
if merge_method == "squash"
@pull.base_repository.set_sticky_merge_method(current_user, "squash")
elsif merge_method == "merge"
@pull.base_repository.set_sticky_merge_method(current_user, "merge_commit")
end
else
result, message = nil, "We couldnt merge this pull request. Reload the page before trying again."
end
if request.xhr?
if result
GitHub.dogstats.histogram("pull_request.merged.requested_reviewers.count", @pull.requested_reviewers.size)
respond_to do |format|
format.json do
prepare_for_rendering(timeline: true, pull_comparison: nil)
render_immediate_partials @pull, :timeline_marker, :sidebar, :merging, :form_actions
end
end
else
GitHub.stats.increment("pullrequest.merge_button.error")
respond_to do |format|
format.html do
render status: :unprocessable_entity, partial: "pull_requests/merging_error", locals: {
title: "Merge attempt failed",
message: (message || "We couldnt merge this pull request."),
hook_output: @hook_out
}
end
end
# render :plain => message, :status => :unprocessable_entity
end
else
if result
GitHub.dogstats.histogram("pull_request.merged.requested_reviewers.count", @pull.requested_reviewers.size)
redirect_to pull_request_path(@pull) + "#merged-event"
else
flash[:error] = message
GitHub.stats.increment("pullrequest.merge_button.error")
redirect_to pull_request_path(@pull)
end
end
end
def change_base
@pull = find_pull_request
return render_404 if !@pull || @pull.merged? ||
!@pull.issue.can_modify?(current_user) ||
!params[:new_base].present?
new_base = URI.decode(params[:new_base])
begin
@pull.change_base_branch(current_user, new_base)
flash[:notice] = "Updated base branch to #{new_base}."
rescue PullRequest::BadComparison, PullRequest::AlreadyExists => e
flash[:error] = e.ui_message
end
redirect_to pull_request_path(@pull)
end
def update_branch
@pull = find_pull_request
return render_404 unless @pull
update_method = params[:update_method] == "rebase" ? :rebase : :merge
begin
@pull.merge_base_into_head(
user: current_user,
method: update_method,
expected_head_oid: params[:expected_head_oid]
)
current_repository.set_sticky_update_method(current_user, update_method)
if request.xhr?
respond_to do |format|
format.json do
prepare_for_rendering(timeline: true, pull_comparison: nil)
render_immediate_partials(@pull, :timeline_marker, :merging)
end
end
else
redirect_to pull_request_path(@pull) + "#partial-pull-merging"
end
rescue GitHub::UIError => e
if request.xhr?
respond_to do |format|
format.html do
render status: :unprocessable_entity, partial: "pull_requests/merging_error", locals: {
title: "Update branch attempt failed",
message: e.ui_message
}
end
end
else
flash[:error] = e.ui_message
redirect_to pull_request_path(@pull) + "#partial-pull-merging"
end
end
end
def revert
@pull = find_pull_request
return render_404 unless @pull && @pull.revertable_by?(current_user)
stats_key = ["pullrequest",
"revert_button",
@pull.cross_repo? ? "cross_repo" : "same_repo"].join(".")
begin
revert_branch, error = @pull.revert(current_user, request_reflog_data("pull request revert button"))
if revert_branch
GitHub.stats.increment("#{stats_key}.success")
base_label, head_label =
if revert_branch.repository == @pull.base_repository
[@pull.base_ref_name, revert_branch.name]
else
["#{@pull.base_label(username_qualified: true)}", "#{revert_branch.repository.owner.login}:#{revert_branch.name}"]
end
flash[:pull_request] = {
title: revert_branch.target.message,
body: "Reverts #{@pull.base_repository.name_with_owner}##{@pull.number}"
}
redirect_to(compare_path(@pull.base_repository, "#{base_label}...#{head_label}", true))
else
if error == :merge_conflict
GitHub.stats.increment("#{stats_key}.merge_conflict")
else
GitHub.stats.increment("#{stats_key}.error")
end
flash[:error] = "Sorry, this pull request couldnt be reverted automatically. It may have \
already been reverted, or the content may have changed since it was merged."
redirect_to pull_request_path(@pull)
end
rescue Git::Ref::HookFailed => e
flash[:hook_out] = e.message
flash[:hook_message] = "Pull request could not be reverted."
redirect_to pull_request_path(@pull)
end
end
def merge_button
pull = current_repository.issues.find_by_number(params[:id].to_i).try(:pull_request)
return render_404 if pull.nil?
merge_state = pull.cached_merge_state(viewer: current_user)
respond_to do |format|
format.html do
if merge_state.unknown?
head :accepted
elsif alt_merge_box_ui_enabled?
render partial: "pull_requests/alt_ui/merge_button", locals: { pull: pull }
else
render partial: "pull_requests/merge_button", locals: { pull: pull }
end
end
format.json do
render json: {mergeable_state: merge_state.status}.to_json
end
end
end
def diff
# This might be a request for a redirect to the PR for a branch name ending in .diff,
# or might be a request for a numbered PR in .diff format.
diff_ref = "#{params[:id]}.diff"
if current_repository.heads.include?(diff_ref)
return redirect_or_404(diff_ref)
end
redirect_or_error = ensure_valid_pull_request
if performed?
return redirect_or_error
else
redirect_to build_pull_request_diff_url
end
end
def patch
# This might be a request for a redirect to the PR for a branch name ending in .patch,
# or might be a request for a numbered PR in .patch format.
patch_ref = "#{params[:id]}.patch"
if current_repository.heads.include?(patch_ref)
return redirect_or_404(patch_ref)
end
redirect_or_error = ensure_valid_pull_request
if performed?
return redirect_or_error
else
redirect_to build_pull_request_patch_url
end
end
def comment
return if reject_bully?
@pull = current_repository.issues.find_by_number(params[:id].to_i).try(:pull_request)
return render_404 unless @pull
issue = @pull.issue
valid = true
comment_body = params[:comment][:body]
if !comment_body.nil? && !can_skip_creating_comment?
comment = issue.comments.build(body: comment_body)
comment.user = current_user
comment.repository = current_repository
valid &= comment.save
elsif params[:comment_and_close] == "1"
comment = issue.comment_and_close(current_user, comment_body)
valid &= comment if comment_body.present?
GitHub.dogstats.histogram("pull_request.closed.requested_reviewers.count", @pull.requested_reviewers.size)
elsif params[:comment_and_open] == "1"
comment = issue.comment_and_open(current_user, comment_body)
valid &= comment if comment_body.present?
end
mark_thread_as_read issue
if valid && comment_body.present?
GitHub.instrument "comment.create", user: current_user
instrument_saved_reply_use(params[:saved_reply_id], "pull_request_comment")
end
respond_to do |format|
format.json do
if valid
prepare_for_rendering(timeline: true, pull_comparison: nil)
render_immediate_partials @pull, :timeline_marker, :sidebar, :merging, :form_actions, :title
else
errors = comment.errors.map { |attr, msg| msg }
render json: { errors: errors }, status: :unprocessable_entity
end
end
format.html do
if valid
anchor = comment ? "#issuecomment-#{comment.id}" : ""
redirect_to pull_request_path(@pull) + anchor
else
flash[:error] = comment.errors.full_messages.to_sentence
redirect_to :back
end
end
end
end
def dismiss_protip
current_user.dismiss_notice("continuous_integration_tip")
head :ok
end
if Rails.env.development?
def merge_button_matrix
if mobile?
return render(template: "pull_requests/merge_button_matrix_mobile.html", layout: "mobile/application")
end
end
end
def set_collab
pull = current_repository.issues.find_by_number(params[:id].to_i).try(:pull_request)
return render_404 if !pull || !pull.head_repository.repository.pushable_by?(current_user)
if !!params[:collab_privs]
pull.fork_collab = :allowed
else
pull.fork_collab = :denied
end
pull.save!
redirect_to pull_request_path(pull)
end
def resolve_conflicts
redirect_or_error = ensure_valid_pull_request
unless logged_in? && @pull.head_repository && @pull.head_repository.pushable_by?(current_user, ref: @pull.head_ref_name)
return render_404
end
return redirect_to pull_request_path(@pull) unless @pull.conflict_resolvable?
if performed?
return redirect_or_error
end
render "pull_requests/resolve_conflicts", locals: { pull: @pull }
end
def valid?
end
protected
helper_method :tab_specified?
def tab_specified?(tab_name)
specified_tab.to_s == tab_name.to_s
end
def id?
end
helper_method :pull_request_subscribe_enabled?
def pull_request_subscribe_enabled?
Rails.development? || Rails.test? || preview_features?
end
def specified_tab
@specified_tab || params[:tab].presence || "discussion"
end
helper_method :specified_tab
def valid_tab?
params[:tab].blank? || %w{commits files tasks}.include?(params[:tab])
end
def val
end
def reject_bully_for?(repo)
if blocked_by_owner?(repo.owner_id)
flash[:error] = "You can't perform that action at this time."
redirect_to repo.permalink
true
end
end
def reject_bully?
reject_bully_for? current_repository
end
def working_predicate?
end
def tree_name
if @pull && @pull.open?
@pull.head_ref_name
elsif @pull
@pull.head_sha
else
super
end
end
private
def find_pull_request
PullRequest.find_by_number_and_repo(params[:id].to_i, current_repository,
include: [{issue: { comments: :user }}])
end
# Private: For a given ref name, redirect to the appropriate pull request
# path if one exists, or 404 otherwise.
#
# Returns the redirect or render_404 result.
def redirect_or_404(ref)
# redirect to number version if ref is a branch name,
# redirect to new if ref is a branch with no pull request
# 404 otherwise
if pull = current_repository.pull_requests.for_branch(ref).last
redirect_to pull_request_path(pull)
elsif ref =~ /[:.]/ || current_repository.heads.include?(ref)
redirect_to new_pull_request_path(range: ref)
else
render_404
end
end
# Private: Validate the requested pull request ID. If necessary redirect to
# a more appropriate URL or return a 404 if the PR isn't/shouldn't be
# available.
def ensure_valid_pull_request
if params[:id] =~ /\D/
return redirect_or_404(params[:id])
end
@pull = find_pull_request
return redirect_to(issue_path(id: params[:id])) unless @pull
return redirect_to(pull_request_path @pull) unless valid_tab?
return render_404 if @pull.hide_from_user?(current_user)
@prose_url_hints = { tab: "files" }
@pull.set_diff_options(
use_summary: true,
ignore_whitespace: ignore_whitespace?
)
end
# Validates the provided parameter is a valid sha, or nil
def valid_sha_param?(param_name)
param = params[param_name]
param.nil? || param =~ /[0-9a-f]{40}/
end
def load_diff
@pull_comparison.ignore_whitespace = ignore_whitespace?
@pull_comparison.diff_options[:use_summary] = true
# load diff data
if !show_mobile_view?
@pull_comparison.diff_options[:top_only] = true
GitHub.dogstats.time("diff.load.initial", tags: dogstats_request_tags) do
@pull_comparison.diffs.apply_auto_load_single_entry_limits!
@pull_comparison.diffs.load_diff(timeout: request_time_left / 2)
end
else
@pull_comparison.diffs.load_diff(timeout: request_time_left / 2)
end
end
# Preload data needed for rendering the PR.
#
# timeline - Boolean specifying whether the PR's timeline will be rendered.
# pull_comparison - PullRequest::Comparison specifying whether the PR's diff will be rendered.
#
# Returns nothing.
def prepare_for_rendering(timeline:, pull_comparison:)
prepare_for_rendering_timeline_items(timeline: timeline, pull_comparison: pull_comparison)
prepare_for_rendering_diffs(timeline: timeline, pull_comparison: pull_comparison)
end
# Preload data needed for rendering timeline items.
#
# timeline - Boolean specifying whether the PR's timeline will be rendered.
# pull_comparison - PullRequest::Comparison specifying whether the PR's diff
# (which contains timeline items corresponding to live review threads) will be rendered.
#
# Returns nothing.
def prepare_for_rendering_timeline_items(timeline:, pull_comparison:)
timeline_items = visible_timeline_items
if pull_comparison
# Add in the review threads that are shown on the Files changed tab.
# Note that some of these might be also be shown on the Discussion tab,
# but the AR objects will actually be distinct so it's still worth
# prefilling/warming both objects.
timeline_items = timeline_items.dup
threads = pull_comparison.review_threads(viewer: current_user)
timeline_items.concat(threads.to_a)
end
@pull.prefill_timeline_associations(timeline_items, preload_diff_entries: timeline)
@pull.warm_timeline_caches(timeline_items)
preload_discussion_group_data(timeline_items)
end
# Preload data needed for rendering diffs.
#
# timeline - Boolean specifying whether the PR's timeline (which contains
# diffs for review threads) will be rendered.
# pull_comparison - PullRequest::Comparison specifying whether the PR's diff will be rendered.
#
# Returns nothing.
def prepare_for_rendering_diffs(timeline:, pull_comparison:)
return unless syntax_highlighted_diffs_enabled?
diffs_to_highlight = []
if timeline
visible_timeline_items.each do |item|
next unless item.is_a?(PullRequestReviewThread)
next unless item.diff_entry
diffs_to_highlight << item.diff_entry
end
end
if pull_comparison
diffs_to_highlight.concat(pull_comparison.diffs.to_a)
end
SyntaxHighlightedDiff.new(@pull.comparison.compare_repository, current_user).highlight!(diffs_to_highlight)
end
def can_skip_creating_comment?
params[:comment_and_close].present? ||
params[:comment_and_open].present?
end
# If it's empty, you can't issue a pull request. There will be no
# base SHA to merge against.
def check_for_empty_repository
if current_repository.empty?
redirect_to current_repository
end
end
def ensure_pull_head_pushable
@pull = current_repository.issues.find_by_number(params[:id].to_i).try(:pull_request)
if @pull.nil? || !@pull.head_repository.pushable_by?(current_user)
render_404
end
end
# Reload the pull so the deletable/restorable status is current,
# then render updates for the event list and the merge/delete buttons.
def render_head_ref_update
@pull.reload
respond_to do |format|
format.json do
prepare_for_rendering(timeline: true, pull_comparison: nil)
render_immediate_partials(@pull, :timeline_marker, :merging, :form_actions)
end
end
end
# Internal: Provide a better failure message than simply taking the validation errors
#
# e - the ActiveRecord::RecordInvalid exception from the creation failure
#
# Returns a String
def human_failure_message(e)
pr = e.record
# e.record can be an Issue, raised in PullRequest.create_for.
return e.message unless pr.is_a?(PullRequest)
if bad_branches = pr.missing_refs
if bad_branches.length == 1
"The #{bad_branches.first} branch doesnt exist."
else
"The #{bad_branches.join(' and ')} branches dont exist."
end
elsif [:base_ref, :head_ref].any? { |attr| pr.errors[attr].include?(GitHub::Validations::Unicode3Validator::ERROR_MESSAGE) }
"Branch names cannot contain unicode characters above 0xffff."
else
e.message
end
end
# If there are this many timeline items or fewer, we'll render the timeline
# inline when the user is viewing the Files tab. If there are more than this
# many items, we'll leave the timeline out and load it as needed to try to
# avoid timing out.
MAXIMUM_TIMELINE_SIZE_FOR_INLINE_RENDER = 149
def render_discussion_page?
return @render_discussion_page if defined?(@render_discussion_page)
@render_discussion_page =
tab_specified?("discussion") || (!show_mobile_view? && @pull.timeline_children_for(current_user).count <= MAXIMUM_TIMELINE_SIZE_FOR_INLINE_RENDER)
end
helper_method :render_discussion_page?
def render_files_page?
return @render_files_page if defined?(@render_files_page)
@render_files_page = tab_specified?("files") || (!show_mobile_view? && !@pull.corrupt? && !@pull.large_diff?)
end
helper_method :render_files_page?
def pull_request_authorization_token
current_user.signed_auth_token expires: 60.seconds.from_now,
scope: pull_request_authorization_token_scope_key
end
def build_pull_request_diff_url
route_options ||= {}
if GitHub.prs_content_domain?
route_options[:host] = GitHub.prs_content_host_name
end
if current_repository.private?
route_options[:token] = pull_request_authorization_token
end
pull_request_raw_diff_url(route_options)
end
def build_pull_request_patch_url
route_options ||= {}
if GitHub.prs_content_domain?
route_options[:host] = GitHub.prs_content_host_name
end
if current_repository.private?
route_options[:token] = pull_request_authorization_token
end
pull_request_raw_patch_url(route_options)
end
def request_reflog_data(via)
super(via).merge({ pr_author_login: @pull.safe_user.login })
end
def content_authorization_required
authorize_content(:pull_request, repo: current_repository)
end
# Internal: Extract OID components from PR range.
#
# /github/github/pull/123/files/abc123..def456
# /github/github/pull/123/files/def456
#
# pull - Current PullRequest
# range - String range parameter
#
# If a complete range is given, a pair of resolved String OIDs will be
# returned. If only one end sha is given, nil and a resolved String OID
# will be returned. Otherwise nil is returned if no range was matched.
def parse_show_range_oid_components(pull, range)
if m = range.to_s.match(/\A(?<sha1>[a-fA-F0-9]{7,40})\.\.(?<sha2>[a-fA-F0-9]{7,40}|HEAD)\z/)
sha2 = m[:sha2] == "HEAD" ? pull.head_sha : m[:sha2]
result = pull.repository.rpc.expand_shas([m[:sha1], sha2], "commit")
sha1, sha2 = result[m[:sha1]], result[sha2]
[sha1, sha2] if sha1 && sha2
elsif m = range.to_s.match(/^(?<sha2>[a-fA-F0-9]{7,40})$/)
result = pull.repository.rpc.expand_shas([m[:sha2]], "commit")
sha2 = result[m[:sha2]]
return [nil, sha2] if sha2
end
end
def visible_timeline_items
return [] unless render_discussion_page?
super
end
def showing_full_timeline?
return false unless render_discussion_page?
super
end
def timeline_owner
@pull
end
MobileCommitsQuery = parse_query <<-'GRAPHQL'
query($ids: [ID!]!) {
nodes(ids: $ids) {
... on Commit {
id
committer { date }
...Views::Mobile::Commits::GroupedCommit::Commit
}
}
}
GRAPHQL
# XXX: Adhoc GraphQL loader for PullRequest.changedChanges connection.
#
# Load required GraphQL data for each Commit in PullRequest#changed_commits and
# wrap it in a fake connection to be compatible with mobile/commits/list template.
def load_mobile_pull_request_commits
data = platform_execute(MobileCommitsQuery, variables: { "ids" => @pull.changed_commits.map(&:global_relay_id) })
history = { "edges" => data.nodes.compact.map { |commit| { "node" => commit.to_h } }, "pageInfo" => {} }
end
helper_method :load_mobile_pull_request_commits
end