Commit e980d766 authored by Ash McKenzie's avatar Ash McKenzie

Merge branch 'rs-change-service-failure-reasons' into 'master'

Provide context about why a cherry pick or revert failed

Closes #33178

See merge request gitlab-org/gitlab!19518
parents 6b527478 a0669342
...@@ -449,7 +449,7 @@ group :ed25519 do ...@@ -449,7 +449,7 @@ group :ed25519 do
end end
# Gitaly GRPC protocol definitions # Gitaly GRPC protocol definitions
gem 'gitaly', '~> 1.65.0' gem 'gitaly', '~> 1.70.0'
gem 'grpc', '~> 1.24.0' gem 'grpc', '~> 1.24.0'
......
...@@ -358,7 +358,7 @@ GEM ...@@ -358,7 +358,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
git (1.5.0) git (1.5.0)
gitaly (1.65.0) gitaly (1.70.0)
grpc (~> 1.0) grpc (~> 1.0)
github-markup (1.7.0) github-markup (1.7.0)
gitlab-labkit (0.7.0) gitlab-labkit (0.7.0)
...@@ -1167,7 +1167,7 @@ DEPENDENCIES ...@@ -1167,7 +1167,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly (~> 1.65.0) gitaly (~> 1.70.0)
github-markup (~> 1.7.0) github-markup (~> 1.7.0)
gitlab-labkit (~> 0.5) gitlab-labkit (~> 0.5)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
......
...@@ -50,16 +50,24 @@ class BaseService ...@@ -50,16 +50,24 @@ class BaseService
private private
def error(message, http_status = nil) # Return a Hash with an `error` status
#
# message - Error message to include in the Hash
# http_status - Optional HTTP status code override (default: nil)
# pass_back - Additional attributes to be included in the resulting Hash
def error(message, http_status = nil, pass_back: {})
result = { result = {
message: message, message: message,
status: :error status: :error
} }.reverse_merge(pass_back)
result[:http_status] = http_status if http_status result[:http_status] = http_status if http_status
result result
end end
# Return a Hash with a `success` status
#
# pass_back - Additional attributes to be included in the resulting Hash
def success(pass_back = {}) def success(pass_back = {})
pass_back[:status] = :success pass_back[:status] = :success
pass_back pass_back
......
...@@ -23,14 +23,15 @@ module Commits ...@@ -23,14 +23,15 @@ module Commits
message, message,
start_project: @start_project, start_project: @start_project,
start_branch_name: @start_branch) start_branch_name: @start_branch)
rescue Gitlab::Git::Repository::CreateTreeError rescue Gitlab::Git::Repository::CreateTreeError => ex
act = action.to_s.dasherize act = action.to_s.dasherize
type = @commit.change_type_title(current_user) type = @commit.change_type_title(current_user)
error_msg = "Sorry, we cannot #{act} this #{type} automatically. " \ error_msg = "Sorry, we cannot #{act} this #{type} automatically. " \
"This #{type} may already have been #{act}ed, or a more recent " \ "This #{type} may already have been #{act}ed, or a more recent " \
"commit may have updated some of its content." "commit may have updated some of its content."
raise ChangeError, error_msg
raise ChangeError.new(error_msg, ex.error_code)
end end
end end
end end
...@@ -3,7 +3,15 @@ ...@@ -3,7 +3,15 @@
module Commits module Commits
class CreateService < ::BaseService class CreateService < ::BaseService
ValidationError = Class.new(StandardError) ValidationError = Class.new(StandardError)
ChangeError = Class.new(StandardError) class ChangeError < StandardError
attr_reader :error_code
def initialize(message, error_code = nil)
super(message)
@error_code = error_code
end
end
def initialize(*args) def initialize(*args)
super super
...@@ -21,8 +29,9 @@ module Commits ...@@ -21,8 +29,9 @@ module Commits
new_commit = create_commit! new_commit = create_commit!
success(result: new_commit) success(result: new_commit)
rescue ChangeError => ex
error(ex.message, pass_back: { error_code: ex.error_code })
rescue ValidationError, rescue ValidationError,
ChangeError,
Gitlab::Git::Index::IndexError, Gitlab::Git::Index::IndexError,
Gitlab::Git::CommitError, Gitlab::Git::CommitError,
Gitlab::Git::PreReceiveError, Gitlab::Git::PreReceiveError,
......
---
title: 'Add an `error_code` attribute to the API response when a cherry-pick or revert fails.'
merge_request: 19518
author:
type: added
...@@ -317,6 +317,21 @@ Example response: ...@@ -317,6 +317,21 @@ Example response:
} }
``` ```
In the event of a failed cherry-pick, the response will provide context about
why:
```json
{
"message": "Sorry, we cannot cherry-pick this commit automatically. This commit may already have been cherry-picked, or a more recent commit may have updated some of its content.",
"error_code": "empty"
}
```
In this case, the cherry-pick failed because the changeset was empty and likely
indicates that the commit already exists in the target branch. The other
possible error code is `conflict`, which indicates that there was a merge
conflict.
## Revert a commit ## Revert a commit
> [Introduced][ce-22919] in GitLab 11.5. > [Introduced][ce-22919] in GitLab 11.5.
...@@ -358,6 +373,19 @@ Example response: ...@@ -358,6 +373,19 @@ Example response:
} }
``` ```
In the event of a failed revert, the response will provide context about why:
```json
{
"message": "Sorry, we cannot revert this commit automatically. This commit may already have been reverted, or a more recent commit may have updated some of its content.",
"error_code": "conflict"
}
```
In this case, the revert failed because the attempted revert generated a merge
conflict. The other possible error code is `empty`, which indicates that the
changeset was empty, likely due to the change having already been reverted.
## Get the diff of a commit ## Get the diff of a commit
Get the diff of a commit in a project. Get the diff of a commit in a project.
......
...@@ -223,7 +223,7 @@ module API ...@@ -223,7 +223,7 @@ module API
present user_project.repository.commit(result[:result]), present user_project.repository.commit(result[:result]),
with: Entities::Commit with: Entities::Commit
else else
render_api_error!(result[:message], 400) error!(result.slice(:message, :error_code), 400, header)
end end
end end
...@@ -257,7 +257,7 @@ module API ...@@ -257,7 +257,7 @@ module API
present user_project.repository.commit(result[:result]), present user_project.repository.commit(result[:result]),
with: Entities::Commit with: Entities::Commit
else else
render_api_error!(result[:message], 400) error!(result.slice(:message, :error_code), 400, header)
end end
end end
......
...@@ -25,9 +25,18 @@ module Gitlab ...@@ -25,9 +25,18 @@ module Gitlab
InvalidRef = Class.new(StandardError) InvalidRef = Class.new(StandardError)
GitError = Class.new(StandardError) GitError = Class.new(StandardError)
DeleteBranchError = Class.new(StandardError) DeleteBranchError = Class.new(StandardError)
CreateTreeError = Class.new(StandardError)
TagExistsError = Class.new(StandardError) TagExistsError = Class.new(StandardError)
ChecksumError = Class.new(StandardError) ChecksumError = Class.new(StandardError)
class CreateTreeError < StandardError
attr_reader :error_code
def initialize(error_code)
super(self.class.name)
# The value coming from Gitaly is an uppercase String (e.g., "EMPTY")
@error_code = error_code.downcase.to_sym
end
end
# Directory name of repo # Directory name of repo
attr_reader :name attr_reader :name
......
...@@ -447,7 +447,7 @@ module Gitlab ...@@ -447,7 +447,7 @@ module Gitlab
elsif response.commit_error.presence elsif response.commit_error.presence
raise Gitlab::Git::CommitError, response.commit_error raise Gitlab::Git::CommitError, response.commit_error
elsif response.create_tree_error.presence elsif response.create_tree_error.presence
raise Gitlab::Git::Repository::CreateTreeError, response.create_tree_error raise Gitlab::Git::Repository::CreateTreeError, response.create_tree_error_code
end end
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update) Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
......
...@@ -211,10 +211,12 @@ describe Gitlab::GitalyClient::OperationService do ...@@ -211,10 +211,12 @@ describe Gitlab::GitalyClient::OperationService do
end end
context 'when a create_tree_error is present' do context 'when a create_tree_error is present' do
let(:response) { response_class.new(create_tree_error: "something failed") } let(:response) { response_class.new(create_tree_error: "something failed", create_tree_error_code: 'EMPTY') }
it 'raises a CreateTreeError' do it 'raises a CreateTreeError' do
expect { subject }.to raise_error(Gitlab::Git::Repository::CreateTreeError, "something failed") expect { subject }.to raise_error(Gitlab::Git::Repository::CreateTreeError) do |error|
expect(error.error_code).to eq(:empty)
end
end end
end end
......
...@@ -1376,6 +1376,12 @@ describe API::Commits do ...@@ -1376,6 +1376,12 @@ describe API::Commits do
it_behaves_like '400 response' do it_behaves_like '400 response' do
let(:request) { post api(route, current_user), params: { branch: 'markdown' } } let(:request) { post api(route, current_user), params: { branch: 'markdown' } }
end end
it 'includes an error_code in the response' do
post api(route, current_user), params: { branch: 'markdown' }
expect(json_response['error_code']).to eq 'empty'
end
end end
context 'when ref contains a dot' do context 'when ref contains a dot' do
...@@ -1535,6 +1541,19 @@ describe API::Commits do ...@@ -1535,6 +1541,19 @@ describe API::Commits do
let(:request) { post api(route, current_user) } let(:request) { post api(route, current_user) }
end end
end end
context 'when commit is already reverted in the target branch' do
it 'includes an error_code in the response' do
# First one actually reverts
post api(route, current_user), params: { branch: 'markdown' }
# Second one is redundant and should be empty
post api(route, current_user), params: { branch: 'markdown' }
expect(response).to have_gitlab_http_status(400)
expect(json_response['error_code']).to eq 'empty'
end
end
end end
context 'when authenticated', 'as a developer' do context 'when authenticated', 'as a developer' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment