gitlab-ce/gems/gitlab-housekeeper/lib/gitlab/housekeeper/gitlab_client.rb

198 lines
6.6 KiB
Ruby

# frozen_string_literal: true
require 'httparty'
require 'json'
module Gitlab
module Housekeeper
class GitlabClient
Error = Class.new(StandardError)
def initialize
@token = ENV.fetch("HOUSEKEEPER_GITLAB_API_TOKEN")
@base_uri = 'https://gitlab.com/api/v4'
end
# This looks at the system notes of the merge request to detect if it has been updated by anyone other than the
# current housekeeper user. If it has then it assumes that they did this for a reason and we can skip updating
# this detail of the merge request. Otherwise we assume we should generate it again using the latest output.
def non_housekeeper_changes(
source_project_id:,
source_branch:,
target_branch:,
target_project_id:
)
existing_merge_request = get_existing_merge_request(
source_project_id: source_project_id,
source_branch: source_branch,
target_branch: target_branch,
target_project_id: target_project_id
)
return [] if existing_merge_request.nil?
merge_request_notes = get_merge_request_notes(
target_project_id: target_project_id,
iid: existing_merge_request['iid']
)
changes = Set.new
merge_request_notes.each do |note|
next false unless note["system"]
next false if note["author"]["id"] == current_user_id
case note['body']
when /^changed title from/
changes << :title
when /^changed the description$/
changes << :description
when /added \d+ commit/
changes << :code
when /assigned to|unassigned/
changes << :assignees
when /requested review from|removed review request for/
changes << :reviewers
end
end
resource_label_events = get_merge_request_resource_label_events(
target_project_id: target_project_id,
iid: existing_merge_request['iid']
)
resource_label_events.each do |event|
next if event.dig("user", "id") == current_user_id
# Labels are routinely added by both humans and bots, so addition events aren't cause for concern.
# However, if labels have been removed it may mean housekeeper added an incorrect label, and we shouldn't
# re-add them.
#
# TODO: Inspect the actual labels housekeeper wants to add, and add if they haven't previously been removed.
changes << :labels if event["action"] == "remove"
end
changes.to_a
end
def create_or_update_merge_request(
change:,
source_project_id:,
source_branch:,
target_branch:,
target_project_id:
)
existing_merge_request = get_existing_merge_request(
source_project_id: source_project_id,
source_branch: source_branch,
target_branch: target_branch,
target_project_id: target_project_id
)
if existing_merge_request
update_existing_merge_request(
change: change,
existing_iid: existing_merge_request['iid'],
target_project_id: target_project_id
)
else
create_merge_request(
change: change,
source_project_id: source_project_id,
source_branch: source_branch,
target_branch: target_branch,
target_project_id: target_project_id
)
end
end
def get_existing_merge_request(source_project_id:, source_branch:, target_branch:, target_project_id:)
data = request(:get, "/projects/#{target_project_id}/merge_requests", query: {
state: :opened,
source_branch: source_branch,
target_branch: target_branch,
source_project_id: source_project_id
})
return nil if data.empty?
raise Error, "More than one matching MR exists: iids: #{data.pluck('iid').join(',')}" unless data.size == 1
data.first
end
private
def get_merge_request_notes(target_project_id:, iid:)
request(:get, "/projects/#{target_project_id}/merge_requests/#{iid}/notes", query: { per_page: 100 })
end
def get_merge_request_resource_label_events(target_project_id:, iid:)
request(:get, "/projects/#{target_project_id}/merge_requests/#{iid}/resource_label_events",
query: { per_page: 100 })
end
def current_user_id
@current_user_id ||= request(:get, "/user")['id']
end
def create_merge_request(
change:,
source_project_id:,
source_branch:,
target_branch:,
target_project_id:
)
request(:post, "/projects/#{source_project_id}/merge_requests", body: {
title: change.title,
description: change.mr_description,
labels: Array(change.labels).join(','),
source_branch: source_branch,
target_branch: target_branch,
target_project_id: target_project_id,
remove_source_branch: true,
assignee_ids: usernames_to_ids(change.assignees),
reviewer_ids: usernames_to_ids(change.reviewers),
squash: true
})
end
def update_existing_merge_request(change:, existing_iid:, target_project_id:)
body = {}
body[:title] = change.title if change.update_required?(:title)
body[:description] = change.mr_description if change.update_required?(:description)
body[:add_labels] = Array(change.labels).join(',') if change.update_required?(:labels)
body[:assignee_ids] = usernames_to_ids(change.assignees) if change.update_required?(:assignees)
body[:reviewer_ids] = usernames_to_ids(change.reviewers) if change.update_required?(:reviewers)
return if body.empty?
request(:put, "/projects/#{target_project_id}/merge_requests/#{existing_iid}", body: body)
end
def usernames_to_ids(usernames)
Array(usernames).map do |username|
data = request(:get, "/users", query: { username: username })
data[0]['id']
end
end
def request(method, path, query: {}, body: {})
response = HTTParty.public_send(method, "#{@base_uri}#{path}", query: query, body: body.to_json, headers: { # rubocop:disable GitlabSecurity/PublicSend
'Private-Token' => @token,
'Content-Type' => 'application/json'
})
unless (200..299).cover?(response.code)
raise Error,
"Failed with response code: #{response.code} and body:\n#{response.body}"
end
JSON.parse(response.body)
end
end
end
end