# frozen_string_literal: true # These endpoints partially mimic Github API behavior in order to successfully # integrate with Jira Development Panel. # Endpoints returning an empty list were temporarily added to avoid 404's # during Jira's DVCS integration. # module API module V3 class Github < ::API::Base NO_SLASH_URL_PART_REGEX = %r{[^/]+}.freeze ENDPOINT_REQUIREMENTS = { namespace: NO_SLASH_URL_PART_REGEX, project: NO_SLASH_URL_PART_REGEX, username: NO_SLASH_URL_PART_REGEX }.freeze # Used to differentiate Jira Cloud requests from Jira Server requests # Jira Cloud user agent format: Jira DVCS Connector Vertigo/version # Jira Server user agent format: Jira DVCS Connector/version JIRA_DVCS_CLOUD_USER_AGENT = 'Jira DVCS Connector Vertigo' GITALY_TIMEOUT_CACHE_KEY = 'api:v3:Gitaly-timeout-cache-key' GITALY_TIMEOUT_CACHE_EXPIRY = 1.day include PaginationParams feature_category :integrations before do authorize_jira_user_agent!(request) authenticate! end helpers do params :project_full_path do requires :namespace, type: String requires :project, type: String end def authorize_jira_user_agent!(request) not_found! unless Gitlab::Jira::Middleware.jira_dvcs_connector?(request.env) end def update_project_feature_usage_for(project) # Prevent errors on GitLab Geo not allowing # UPDATE statements to happen in GET requests. return if Gitlab::Database.read_only? project.log_jira_dvcs_integration_usage(cloud: jira_cloud?) end def jira_cloud? request.env['HTTP_USER_AGENT'].include?(JIRA_DVCS_CLOUD_USER_AGENT) end def find_project_with_access(params) project = find_project!( ::Gitlab::Jira::Dvcs.restore_full_path(**params.slice(:namespace, :project).symbolize_keys) ) not_found! unless can?(current_user, :read_code, project) project end # rubocop: disable CodeReuse/ActiveRecord def find_merge_requests merge_requests = authorized_merge_requests.reorder(updated_at: :desc) paginate(merge_requests) end # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord def find_merge_request_with_access(id, access_level = :read_merge_request) merge_request = authorized_merge_requests.find_by(id: id) not_found! unless can?(current_user, access_level, merge_request) merge_request end # rubocop: enable CodeReuse/ActiveRecord def authorized_merge_requests MergeRequestsFinder.new(current_user, authorized_only: !current_user.can_read_all_resources?) .execute.with_jira_integration_associations end def authorized_merge_requests_for_project(project) MergeRequestsFinder .new(current_user, authorized_only: !current_user.can_read_all_resources?, project_id: project.id) .execute.with_jira_integration_associations end # rubocop: disable CodeReuse/ActiveRecord def find_notes(noteable) # They're not presented on Jira Dev Panel ATM. A comments count with a # redirect link is presented. notes = paginate(noteable.notes.user.reorder(nil)) notes.select { |n| n.readable_by?(current_user) } end # rubocop: enable CodeReuse/ActiveRecord # Returns an empty Array instead of the Commit diff files for a period # of time after a Gitaly timeout, to mitigate frequent Gitaly timeouts # for some Commit diffs. def diff_files(commit) cache_key = [ GITALY_TIMEOUT_CACHE_KEY, commit.project.id, commit.cache_key ].join(':') return [] if Rails.cache.read(cache_key).present? begin commit.diffs.diff_files rescue GRPC::DeadlineExceeded => error # Gitaly fails to load diffs consistently for some commits. The other information # is still valuable for Jira. So we skip the loading and respond with a 200 excluding diffs # Remove this when https://gitlab.com/gitlab-org/gitaly/-/issues/3741 is fixed. Rails.cache.write(cache_key, 1, expires_in: GITALY_TIMEOUT_CACHE_EXPIRY) Gitlab::ErrorTracking.track_exception(error) [] end end end resource :orgs do get ':namespace/repos' do present [] end end resource :user do get :repos do present [] end end resource :users do params do use :pagination end get ':namespace/repos' do namespace = Namespace.find_by_full_path(params[:namespace]) not_found!('Namespace') unless namespace projects = current_user.can_read_all_resources? ? Project.all : current_user.authorized_projects projects = projects.in_namespace(namespace.self_and_descendants) projects_cte = Project.wrap_with_cte(projects) .eager_load_namespace_and_owner .with_route present paginate(projects_cte), with: ::API::Github::Entities::Repository, root_namespace: namespace.root_ancestor end get ':username' do forbidden! unless can?(current_user, :read_users_list) user = UsersFinder.new(current_user, { username: params[:username] }).execute.first not_found! unless user present user, with: ::API::Github::Entities::User end end # Jira dev panel integration weirdly requests for "/-/jira/pulls" instead # "/api/v3/repos///pulls". This forces us into # returning _all_ Merge Requests from authorized projects (user is a member), # instead just the authorized MRs from a project. # Jira handles the filtering, presenting just MRs mentioning the Jira # issue ID on the MR title / description. resource :repos do # Keeping for backwards compatibility with old Jira integration instructions # so that users that do not change it will not suddenly have a broken integration get '/-/jira/pulls' do present find_merge_requests, with: ::API::Github::Entities::PullRequest end get '/-/jira/events' do present [] end params do use :project_full_path end # TODO Remove the custom Apdex SLO target `urgency: :low` when this endpoint has been optimised. # https://gitlab.com/gitlab-org/gitlab/-/issues/337269 get ':namespace/:project/pulls', urgency: :low do user_project = find_project_with_access(params) merge_requests = authorized_merge_requests_for_project(user_project) present paginate(merge_requests), with: ::API::Github::Entities::PullRequest end params do use :project_full_path end get ':namespace/:project/pulls/:id' do merge_request = find_merge_request_with_access(params[:id]) present merge_request, with: ::API::Github::Entities::PullRequest end # In Github, each Merge Request is automatically also an issue. # Therefore we return its comments here. # It'll present _just_ the comments counting with a link to GitLab on # Jira dev panel, not the actual note content. get ':namespace/:project/issues/:id/comments' do merge_request = find_merge_request_with_access(params[:id]) present find_notes(merge_request), with: ::API::Github::Entities::NoteableComment end # This refer to "review" comments but Jira dev panel doesn't seem to # present it accordingly. get ':namespace/:project/pulls/:id/comments' do present [] end # Commits are not presented within "Pull Requests" modal on Jira dev # panel. get ':namespace/:project/pulls/:id/commits' do present [] end # Self-hosted Jira (tested on 7.11.1) requests this endpoint right # after fetching branches. get ':namespace/:project/events' do user_project = find_project_with_access(params) merge_requests = authorized_merge_requests_for_project(user_project) present paginate(merge_requests), with: ::API::Github::Entities::PullRequestEvent end params do use :project_full_path use :pagination end # TODO Remove the custom Apdex SLO target `urgency: :low` when this endpoint has been optimised. # https://gitlab.com/gitlab-org/gitlab/-/issues/337268 get ':namespace/:project/branches', urgency: :low do user_project = find_project_with_access(params) update_project_feature_usage_for(user_project) next [] unless user_project.repo_exists? branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name)) present paginate(branches), with: ::API::Github::Entities::Branch, project: user_project end params do use :project_full_path end get ':namespace/:project/commits/:sha' do user_project = find_project_with_access(params) commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit present commit, with: ::API::Github::Entities::RepoCommit, diff_files: diff_files(commit) end end end end end