diff --git a/common/lib/dependabot/clients/azure.rb b/common/lib/dependabot/clients/azure.rb index cc2f5ef5615..442fd96e6b3 100644 --- a/common/lib/dependabot/clients/azure.rb +++ b/common/lib/dependabot/clients/azure.rb @@ -136,7 +136,7 @@ def pull_requests(source_branch, target_branch) def create_commit(branch_name, base_commit, commit_message, files, author_details) - content = { + content = { refUpdates: [ { name: "refs/heads/" + branch_name, oldObjectId: base_commit } ], @@ -146,7 +146,7 @@ def create_commit(branch_name, base_commit, commit_message, files, author: author_details, changes: files.map do |file| { - changeType: file_exists?(base_commit, file.path) ? "edit": "add", + changeType: file_exists?(base_commit, file.path) ? "edit" : "add", item: { path: file.path }, newContent: { content: Base64.encode64(file.content), @@ -185,6 +185,28 @@ def create_pull_request(pr_name, source_branch, target_branch, "/pullrequests?api-version=5.0", content.to_json) end + def update_pull_request(pull_request_id:, status: nil, pr_name: nil, description: nil, + completion_options: nil, merge_options: nil, + auto_complete_setby_user_id: nil, target_branch: nil) + content = { + autoCompleteSetBy: ({ id: auto_complete_setby_user_id } unless + auto_complete_setby_user_id.nil? || auto_complete_setby_user_id.strip.empty?), + title: pr_name, + description: truncate_pr_description(description), + status: status, + completionOptions: completion_options, + mergeOptions: merge_options, + targetRefName: ("refs/heads/" + target_branch unless target_branch.nil? || target_branch.strip.empty?) + }.compact + + return if content.empty? + + patch(source.api_endpoint + + source.organization + "/" + source.project + + "/_apis/git/repositories/" + source.unscoped_repo + + "/pullrequests/" + pull_request_id.to_s + "?api-version=5.0", content.to_json) + end + def pull_request(pull_request_id) response = get(source.api_endpoint + source.organization + "/" + source.project + @@ -252,6 +274,26 @@ def post(url, json) response end + def patch(url, json) + response = Excon.patch( + url, + body: json, + user: credentials&.fetch("username", nil), + password: credentials&.fetch("password", nil), + idempotent: true, + **SharedHelpers.excon_defaults( + headers: auth_header.merge( + { + "Content-Type" => "application/json" + } + ) + ) + ) + raise NotFound if response.status == 404 + + response + end + private def retry_connection_failures @@ -279,23 +321,24 @@ def auth_header_for(token) end end - def file_exists?(commit, path) # Get the file base and directory name dir = File.dirname(path) basename = File.basename(path) - # Fetch the contents for the dir and check if there exists any file that matches basename. + # Fetch the contents for the dir and check if there exists any file that matches basename. # We ignore any sub-dir paths by rejecting "tree" gitObjectType (which is what ADO uses to specify a directory.) - fetch_repo_contents(commit, dir) - .reject { |f| f["gitObjectType"] == "tree" } - .one? { |f| f["relativePath"] == basename} - + fetch_repo_contents(commit, dir). + reject { |f| f["gitObjectType"] == "tree" }. + one? { |f| f["relativePath"] == basename } rescue Dependabot::Clients::Azure::NotFound # ADO throws exception if dir not found. Return false false end + def truncate_pr_description(pr_description) + return unless pr_description + # Azure DevOps only support descriptions up to 4000 characters in UTF-16 # encoding. # https://developercommunity.visualstudio.com/content/problem/608770/remove-4000-character-limit-on-pull-request-descri.html diff --git a/common/lib/dependabot/pull_request_creator/branch_namer.rb b/common/lib/dependabot/pull_request_creator/branch_namer.rb index 8de04918537..d0e12224991 100644 --- a/common/lib/dependabot/pull_request_creator/branch_namer.rb +++ b/common/lib/dependabot/pull_request_creator/branch_namer.rb @@ -34,15 +34,7 @@ def new_branch_name tr("@", "") end - dep = dependencies.first - - if library? && ref_changed?(dep) && new_ref(dep) - "#{dependency_name_part}-#{new_ref(dep)}" - elsif library? - "#{dependency_name_part}-#{sanitized_requirement(dep)}" - else - "#{dependency_name_part}-#{new_version(dep)}" - end + "#{dependency_name_part}-#{branch_version_suffix}" end # Some users need branch names without slashes @@ -98,6 +90,18 @@ def dependency_set @dependency_set end + def branch_version_suffix + dep = dependencies.first + + if library? && ref_changed?(dep) && new_ref(dep) + new_ref(dep) + elsif library? + sanitized_requirement(dep) + else + new_version(dep) + end + end + def sanitized_requirement(dependency) new_library_requirement(dependency). delete(" "). diff --git a/common/lib/dependabot/pull_request_updater/azure.rb b/common/lib/dependabot/pull_request_updater/azure.rb index d2a506b7c9a..693d967f946 100644 --- a/common/lib/dependabot/pull_request_updater/azure.rb +++ b/common/lib/dependabot/pull_request_updater/azure.rb @@ -27,6 +27,21 @@ def update update_source_branch end + def update_pr_elements(elements = {}) + return unless elements + + azure_client_for_source.update_pull_request( + pull_request_id: pull_request_number, + status: elements[:status], + pr_name: elements[:pr_name], + description: elements[:description], + completion_options: elements[:completion_options], + merge_options: elements[:merge_options], + auto_complete_setby_user_id: elements[:auto_complete_setby_user_id], + target_branch: elements[:target_branch] + ) + end + private def azure_client_for_source diff --git a/common/spec/dependabot/clients/azure_spec.rb b/common/spec/dependabot/clients/azure_spec.rb index e93e9d15233..4a7e174a013 100644 --- a/common/spec/dependabot/clients/azure_spec.rb +++ b/common/spec/dependabot/clients/azure_spec.rb @@ -210,6 +210,60 @@ end end + describe "#update_pull_request" do + subject(:update_pull_request) do + client.update_pull_request( + pull_request_id: pull_request_id, + auto_complete_setby_user_id: auto_complete_setby_user_id, + pr_name: pr_name + ) + end + + let(:pull_request_id) { 1 } + let(:auto_complete_setby_user_id) { "id" } + let(:pr_name) { "Test PR" } + let(:update_pull_request_url) { repo_url + "/pullrequests/" + pull_request_id.to_s + "?api-version=5.0" } + + it "returns nil when there are no parameters to update" do + response = client.update_pull_request(pull_request_id: pull_request_id) + + expect(response).to be_nil + end + + context "sends update PR request with updated parameter values" do + it "successfully updates the PR" do + stub_request(:patch, update_pull_request_url). + with(basic_auth: [username, password]). + to_return(status: 200) + + update_pull_request + + expect(WebMock). + to( + have_requested(:patch, update_pull_request_url). + with do |req| + pr_update_details = JSON.parse(req.body) + expect(pr_update_details). + to eq( + { + "title" => pr_name, + "autoCompleteSetBy" => { "id" => auto_complete_setby_user_id } + } + ) + end + ) + end + + it "raises helpful error when response is 404" do + stub_request(:patch, update_pull_request_url). + with(basic_auth: [username, password]). + to_return(status: 404) + + expect { update_pull_request }.to raise_error(Dependabot::Clients::Azure::NotFound) + end + end + end + describe "#get" do context "Using auth headers" do token = ":test_token" diff --git a/common/spec/dependabot/pull_request_updater/azure_spec.rb b/common/spec/dependabot/pull_request_updater/azure_spec.rb index 7e3fbf83712..741eb7b5fff 100644 --- a/common/spec/dependabot/pull_request_updater/azure_spec.rb +++ b/common/spec/dependabot/pull_request_updater/azure_spec.rb @@ -166,4 +166,39 @@ ) end end + + describe "#update_pr_elements" do + let(:update_pull_request_url) { repo_url + "/pullrequests/" + pull_request_number.to_s + "?api-version=5.0" } + + context "returns nil" do + it "when elements hash is empty" do + expect(updater.update_pr_elements).to be_nil + end + + it "when elements hash does not contain expected parameters for update" do + elements = { test_parameter: "test" } + + expect(updater.update_pr_elements(elements)).to be_nil + end + end + + it "updates the given pr elements" do + elements = { pr_name: "Test PR", auto_complete_setby_user_id: "id" } + + stub_request(:patch, update_pull_request_url). + to_return(status: 200) + + updater.update_pr_elements(elements) + + expect(WebMock). + to( + have_requested(:patch, update_pull_request_url). + with(body: + { + title: elements[:pr_name], + autoCompleteSetBy: { id: elements[:auto_complete_setby_user_id] } + }) + ) + end + end end