Commit 38bab6e1 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 47b8f79a
...@@ -118,13 +118,17 @@ export default { ...@@ -118,13 +118,17 @@ export default {
<div v-if="loading" class="py-3"> <div v-if="loading" class="py-3">
<gl-loading-icon :size="3" /> <gl-loading-icon :size="3" />
</div> </div>
<div v-else-if="showDetails" class="error-details"> <div v-else-if="showDetails" class="error-details">
<div class="top-area align-items-center justify-content-between py-3"> <div class="top-area align-items-center justify-content-between py-3">
<span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span> <span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span>
<form ref="sentryIssueForm" :action="projectIssuesPath" method="POST"> <form ref="sentryIssueForm" :action="projectIssuesPath" method="POST">
<gl-form-input class="hidden" name="issue[title]" :value="issueTitle" /> <gl-form-input class="hidden" name="issue[title]" :value="issueTitle" />
<input name="issue[description]" :value="issueDescription" type="hidden" /> <input name="issue[description]" :value="issueDescription" type="hidden" />
<gl-form-input
:value="error.id"
class="hidden"
name="issue[sentry_issue_attributes][sentry_issue_identifier]"
/>
<gl-form-input :value="csrfToken" class="hidden" name="authenticity_token" /> <gl-form-input :value="csrfToken" class="hidden" name="authenticity_token" />
<loading-button <loading-button
v-if="!error.gitlab_issue" v-if="!error.gitlab_issue"
......
...@@ -237,7 +237,10 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -237,7 +237,10 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def issue_params def issue_params
params.require(:issue).permit(*issue_params_attributes) params.require(:issue).permit(
*issue_params_attributes,
sentry_issue_attributes: [:sentry_issue_identifier]
)
end end
def issue_params_attributes def issue_params_attributes
......
...@@ -45,6 +45,8 @@ class Issue < ApplicationRecord ...@@ -45,6 +45,8 @@ class Issue < ApplicationRecord
has_many :user_mentions, class_name: "IssueUserMention" has_many :user_mentions, class_name: "IssueUserMention"
has_one :sentry_issue has_one :sentry_issue
accepts_nested_attributes_for :sentry_issue
validates :project, presence: true validates :project, presence: true
alias_attribute :parent_ids, :project_id alias_attribute :parent_ids, :project_id
......
...@@ -4,5 +4,7 @@ class SentryIssue < ApplicationRecord ...@@ -4,5 +4,7 @@ class SentryIssue < ApplicationRecord
belongs_to :issue belongs_to :issue
validates :issue, uniqueness: true, presence: true validates :issue, uniqueness: true, presence: true
validates :sentry_issue_identifier, presence: true validates :sentry_issue_identifier,
uniqueness: true,
presence: true
end end
...@@ -95,6 +95,12 @@ through in CI. However, you can speed these cycles up somewhat by emptying the ...@@ -95,6 +95,12 @@ through in CI. However, you can speed these cycles up somewhat by emptying the
`.gitlab/ci/rails.gitlab-ci.yml` file in your merge request. Just don't forget `.gitlab/ci/rails.gitlab-ci.yml` file in your merge request. Just don't forget
to revert the change before merging! to revert the change before merging!
To enable the Dangerfile on another existing GitLab project, run the following extra steps, based on [this procedure](https://danger.systems/guides/getting_started.html#creating-a-bot-account-for-danger-to-use):
1. Add `@gitlab-bot` to the project as a `reporter`.
1. Add the `@gitlab-bot`'s `GITLAB_API_PRIVATE_TOKEN` value as a value for a new CI/CD
variable named `DANGER_GITLAB_API_TOKEN`.
You should add the `~Danger bot` label to the merge request before sending it You should add the `~Danger bot` label to the merge request before sending it
for review. for review.
......
...@@ -13,7 +13,7 @@ GitLab supports several ways deploy Serverless applications in both Kubernetes E ...@@ -13,7 +13,7 @@ GitLab supports several ways deploy Serverless applications in both Kubernetes E
Currently we support: Currently we support:
- [Knative](#knative): Build Knative applications with Knative and `gitlabktl` on GKE. - [Knative](#knative): Build Knative applications with Knative and `gitlabktl` on GKE and EKS.
- [AWS Lambda](aws.md): Create serverless applications via the Serverless Framework and GitLab CI. - [AWS Lambda](aws.md): Create serverless applications via the Serverless Framework and GitLab CI.
## Knative ## Knative
...@@ -89,7 +89,7 @@ The minimum recommended cluster size to run Knative is 3-nodes, 6 vCPUs, and 22. ...@@ -89,7 +89,7 @@ The minimum recommended cluster size to run Knative is 3-nodes, 6 vCPUs, and 22.
for other platforms [Install kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/). for other platforms [Install kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/).
1. The Ingress is now available at this address and will route incoming requests to the proper service based on the DNS 1. The Ingress is now available at this address and will route incoming requests to the proper service based on the DNS
name in the request. To support this, a wildcard DNS A record should be created for the desired domain name. For example, name in the request. To support this, a wildcard DNS record should be created for the desired domain name. For example,
if your Knative base domain is `knative.info` then you need to create an A record or CNAME record with domain `*.knative.info` if your Knative base domain is `knative.info` then you need to create an A record or CNAME record with domain `*.knative.info`
pointing the ip address or hostname of the Ingress. pointing the ip address or hostname of the Ingress.
......
...@@ -188,7 +188,7 @@ module API ...@@ -188,7 +188,7 @@ module API
end end
class BasicProjectDetails < ProjectIdentity class BasicProjectDetails < ProjectIdentity
include ::API::ProjectsBatchCounting include ::API::ProjectsRelationBuilder
expose :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } expose :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
# Avoids an N+1 query: https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770 # Avoids an N+1 query: https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770
...@@ -430,7 +430,7 @@ module API ...@@ -430,7 +430,7 @@ module API
options: { only_owned: true, limit: projects_limit } options: { only_owned: true, limit: projects_limit }
).execute ).execute
Entities::Project.preload_and_batch_count!(projects) Entities::Project.prepare_relation(projects)
end end
expose :shared_projects, using: Entities::Project do |group, options| expose :shared_projects, using: Entities::Project do |group, options|
...@@ -440,7 +440,7 @@ module API ...@@ -440,7 +440,7 @@ module API
options: { only_shared: true, limit: projects_limit } options: { only_shared: true, limit: projects_limit }
).execute ).execute
Entities::Project.preload_and_batch_count!(projects) Entities::Project.prepare_relation(projects)
end end
def projects_limit def projects_limit
......
...@@ -231,7 +231,7 @@ module API ...@@ -231,7 +231,7 @@ module API
projects, options = with_custom_attributes(projects, options) projects, options = with_custom_attributes(projects, options)
present options[:with].preload_and_batch_count!(projects), options present options[:with].prepare_relation(projects), options
end end
desc 'Get a list of subgroups in this group.' do desc 'Get a list of subgroups in this group.' do
......
...@@ -75,17 +75,15 @@ module API ...@@ -75,17 +75,15 @@ module API
mutually_exclusive :import_url, :template_name, :template_project_id mutually_exclusive :import_url, :template_name, :template_project_id
end end
def find_projects def load_projects
ProjectsFinder.new(current_user: current_user, params: project_finder_params).execute ProjectsFinder.new(current_user: current_user, params: project_finder_params).execute
end end
# Prepare the full projects query def present_projects(projects, options = {})
# None of this is supposed to actually execute any database query
def prepare_query(projects)
projects = reorder_projects(projects) projects = reorder_projects(projects)
projects = apply_filters(projects) projects = apply_filters(projects)
projects = paginate(projects)
projects, options = with_custom_attributes(projects) projects, options = with_custom_attributes(projects, options)
options = options.reverse_merge( options = options.reverse_merge(
with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails, with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails,
...@@ -93,23 +91,9 @@ module API ...@@ -93,23 +91,9 @@ module API
current_user: current_user, current_user: current_user,
license: false license: false
) )
options[:with] = Entities::BasicProjectDetails if params[:simple] options[:with] = Entities::BasicProjectDetails if params[:simple]
projects = options[:with].preload_relation(projects, options) present options[:with].prepare_relation(projects, options), options
[projects, options]
end
def prepare_and_present(project_relation)
projects, options = prepare_query(project_relation)
projects = paginate_and_retrieve!(projects)
# Refresh count caches
options[:with].execute_batch_counting(projects)
present projects, options
end end
def translate_params_for_compatibility(params) def translate_params_for_compatibility(params)
...@@ -134,7 +118,7 @@ module API ...@@ -134,7 +118,7 @@ module API
params[:user] = user params[:user] = user
prepare_and_present find_projects present_projects load_projects
end end
desc 'Get projects starred by a user' do desc 'Get projects starred by a user' do
...@@ -150,7 +134,7 @@ module API ...@@ -150,7 +134,7 @@ module API
not_found!('User') unless user not_found!('User') unless user
starred_projects = StarredProjectsFinder.new(user, params: project_finder_params, current_user: current_user).execute starred_projects = StarredProjectsFinder.new(user, params: project_finder_params, current_user: current_user).execute
prepare_and_present starred_projects present_projects starred_projects
end end
end end
...@@ -166,7 +150,7 @@ module API ...@@ -166,7 +150,7 @@ module API
use :with_custom_attributes use :with_custom_attributes
end end
get do get do
prepare_and_present find_projects present_projects load_projects
end end
desc 'Create new project' do desc 'Create new project' do
...@@ -303,7 +287,7 @@ module API ...@@ -303,7 +287,7 @@ module API
get ':id/forks' do get ':id/forks' do
forks = ForkProjectsFinder.new(user_project, params: project_finder_params, current_user: current_user).execute forks = ForkProjectsFinder.new(user_project, params: project_finder_params, current_user: current_user).execute
prepare_and_present forks present_projects forks
end end
desc 'Check pages access of this project' desc 'Check pages access of this project'
......
# frozen_string_literal: true
module API
module ProjectsBatchCounting
extend ActiveSupport::Concern
class_methods do
# This adds preloading to the query and executes batch counting
# Side-effect: The query will be executed during batch counting
def preload_and_batch_count!(projects_relation)
preload_relation(projects_relation).tap do |projects|
execute_batch_counting(projects)
end
end
def execute_batch_counting(projects)
::Projects::BatchForksCountService.new(forks_counting_projects(projects)).refresh_cache
::Projects::BatchOpenIssuesCountService.new(projects).refresh_cache
end
def forks_counting_projects(projects)
projects
end
end
end
end
# frozen_string_literal: true
module API
module ProjectsRelationBuilder
extend ActiveSupport::Concern
class_methods do
def prepare_relation(projects_relation, options = {})
projects_relation = preload_relation(projects_relation, options)
execute_batch_counting(projects_relation)
projects_relation
end
def preload_relation(projects_relation, options = {})
projects_relation
end
def forks_counting_projects(projects_relation)
projects_relation
end
def batch_forks_counting(projects_relation)
::Projects::BatchForksCountService.new(forks_counting_projects(projects_relation)).refresh_cache
end
def batch_open_issues_counting(projects_relation)
::Projects::BatchOpenIssuesCountService.new(projects_relation).refresh_cache
end
def execute_batch_counting(projects_relation)
batch_forks_counting(projects_relation)
batch_open_issues_counting(projects_relation)
end
end
end
end
...@@ -1073,6 +1073,24 @@ describe Projects::IssuesController do ...@@ -1073,6 +1073,24 @@ describe Projects::IssuesController do
expect(issue.time_estimate).to eq(7200) expect(issue.time_estimate).to eq(7200)
end end
end end
context 'when created from sentry error' do
subject { post_new_issue(sentry_issue_attributes: { sentry_issue_identifier: 1234567 }) }
it 'creates an issue' do
expect { subject }.to change(Issue, :count)
end
it 'creates a sentry issue' do
expect { subject }.to change(SentryIssue, :count)
end
it 'with existing issue it will not create an issue' do
post_new_issue(sentry_issue_attributes: { sentry_issue_identifier: 1234567 })
expect { subject }.not_to change(Issue, :count)
end
end
end end
describe 'POST #mark_as_spam' do describe 'POST #mark_as_spam' do
......
...@@ -110,7 +110,7 @@ describe('ErrorDetails', () => { ...@@ -110,7 +110,7 @@ describe('ErrorDetails', () => {
beforeEach(() => { beforeEach(() => {
store.state.details.loading = false; store.state.details.loading = false;
store.state.details.error = { store.state.details.error = {
id: 1, id: 129381,
title: 'Issue title', title: 'Issue title',
external_url: 'http://sentry.gitlab.net/gitlab', external_url: 'http://sentry.gitlab.net/gitlab',
first_seen: '2017-05-26T13:32:48Z', first_seen: '2017-05-26T13:32:48Z',
...@@ -121,6 +121,13 @@ describe('ErrorDetails', () => { ...@@ -121,6 +121,13 @@ describe('ErrorDetails', () => {
mountComponent(); mountComponent();
}); });
it('should send sentry_issue_identifier', () => {
const sentryErrorIdInput = wrapper.find(
'glforminput-stub[name="issue[sentry_issue_attributes][sentry_issue_identifier]"',
);
expect(sentryErrorIdInput.attributes('value')).toBe('129381');
});
it('should set the form values with title and description', () => { it('should set the form values with title and description', () => {
const csrfTokenInput = wrapper.find('glforminput-stub[name="authenticity_token"]'); const csrfTokenInput = wrapper.find('glforminput-stub[name="authenticity_token"]');
const issueTitleInput = wrapper.find('glforminput-stub[name="issue[title]"]'); const issueTitleInput = wrapper.find('glforminput-stub[name="issue[title]"]');
......
# frozen_string_literal: true
require 'spec_helper'
describe API::ProjectsBatchCounting do
subject do
Class.new do
include ::API::ProjectsBatchCounting
end
end
describe '.preload_and_batch_count!' do
let(:projects) { double }
let(:preloaded_projects) { double }
it 'preloads the relation' do
allow(subject).to receive(:execute_batch_counting).with(preloaded_projects)
expect(subject).to receive(:preload_relation).with(projects).and_return(preloaded_projects)
expect(subject.preload_and_batch_count!(projects)).to eq(preloaded_projects)
end
it 'executes batch counting' do
allow(subject).to receive(:preload_relation).with(projects).and_return(preloaded_projects)
expect(subject).to receive(:execute_batch_counting).with(preloaded_projects)
subject.preload_and_batch_count!(projects)
end
end
describe '.execute_batch_counting' do
let(:projects) { create_list(:project, 2) }
let(:count_service) { double }
it 'counts forks' do
allow(::Projects::BatchForksCountService).to receive(:new).with(projects).and_return(count_service)
expect(count_service).to receive(:refresh_cache)
subject.execute_batch_counting(projects)
end
it 'counts open issues' do
allow(::Projects::BatchOpenIssuesCountService).to receive(:new).with(projects).and_return(count_service)
expect(count_service).to receive(:refresh_cache)
subject.execute_batch_counting(projects)
end
context 'custom fork counting' do
subject do
Class.new do
include ::API::ProjectsBatchCounting
def self.forks_counting_projects(projects)
[projects.first]
end
end
end
it 'counts forks for other projects' do
allow(::Projects::BatchForksCountService).to receive(:new).with([projects.first]).and_return(count_service)
expect(count_service).to receive(:refresh_cache)
subject.execute_batch_counting(projects)
end
end
end
end
...@@ -13,5 +13,6 @@ describe SentryIssue do ...@@ -13,5 +13,6 @@ describe SentryIssue do
it { is_expected.to validate_presence_of(:issue) } it { is_expected.to validate_presence_of(:issue) }
it { is_expected.to validate_uniqueness_of(:issue) } it { is_expected.to validate_uniqueness_of(:issue) }
it { is_expected.to validate_presence_of(:sentry_issue_identifier) } it { is_expected.to validate_presence_of(:sentry_issue_identifier) }
it { is_expected.to validate_uniqueness_of(:sentry_issue_identifier).with_message("has already been taken") }
end end
end end
...@@ -155,35 +155,6 @@ describe API::Projects do ...@@ -155,35 +155,6 @@ describe API::Projects do
project4 project4
end end
# This is a regression spec for https://gitlab.com/gitlab-org/gitlab/issues/37919
context 'batch counting forks and open issues and refreshing count caches' do
# We expect to count these projects (only the ones on the first page, not all matching ones)
let(:projects) { Project.public_to_user(nil).order(id: :desc).first(per_page) }
let(:per_page) { 2 }
let(:count_service) { double }
before do
# Create more projects, so we have more than one page
create_list(:project, 5, :public)
end
it 'batch counts project forks' do
expect(::Projects::BatchForksCountService).to receive(:new).with(projects).and_return(count_service)
expect(count_service).to receive(:refresh_cache)
get api("/projects?per_page=#{per_page}")
expect(response.status).to eq 200
end
it 'batch counts open issues' do
expect(::Projects::BatchOpenIssuesCountService).to receive(:new).with(projects).and_return(count_service)
expect(count_service).to receive(:refresh_cache)
get api("/projects?per_page=#{per_page}")
expect(response.status).to eq 200
end
end
context 'when unauthenticated' do context 'when unauthenticated' do
it_behaves_like 'projects response' do it_behaves_like 'projects response' do
let(:filter) { { search: project.name } } let(:filter) { { search: project.name } }
...@@ -599,87 +570,6 @@ describe API::Projects do ...@@ -599,87 +570,6 @@ describe API::Projects do
let(:projects) { Project.all } let(:projects) { Project.all }
end end
end end
context 'with keyset pagination' do
let(:current_user) { user }
let(:projects) { [public_project, project, project2, project3] }
context 'headers and records' do
let(:params) { { pagination: 'keyset', order_by: :id, sort: :asc, per_page: 1 } }
it 'includes a pagination header with link to the next page' do
get api('/projects', current_user), params: params
expect(response.header).to include('Links')
expect(response.header['Links']).to include('pagination=keyset')
expect(response.header['Links']).to include("id_after=#{public_project.id}")
end
it 'contains only the first project with per_page = 1' do
get api('/projects', current_user), params: params
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).to contain_exactly(public_project.id)
end
it 'does not include a link if the end has reached and there is no more data' do
get api('/projects', current_user), params: params.merge(id_after: project2.id)
expect(response.header).not_to include('Links')
end
it 'responds with 501 if order_by is different from id' do
get api('/projects', current_user), params: params.merge(order_by: :created_at)
expect(response).to have_gitlab_http_status(405)
end
end
context 'with descending sorting' do
let(:params) { { pagination: 'keyset', order_by: :id, sort: :desc, per_page: 1 } }
it 'includes a pagination header with link to the next page' do
get api('/projects', current_user), params: params
expect(response.header).to include('Links')
expect(response.header['Links']).to include('pagination=keyset')
expect(response.header['Links']).to include("id_before=#{project3.id}")
end
it 'contains only the last project with per_page = 1' do
get api('/projects', current_user), params: params
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).to contain_exactly(project3.id)
end
end
context 'retrieving the full relation' do
let(:params) { { pagination: 'keyset', order_by: :id, sort: :desc, per_page: 2 } }
it 'returns all projects' do
url = '/projects'
requests = 0
ids = []
while url && requests <= 5 # circuit breaker
requests += 1
get api(url, current_user), params: params
links = response.header['Links']
url = links&.match(/<[^>]+(\/projects\?[^>]+)>; rel="next"/) do |match|
match[1]
end
ids += JSON.parse(response.body).map { |p| p['id'] }
end
expect(ids).to contain_exactly(*projects.map(&:id))
end
end
end
end end
describe 'POST /projects' do describe 'POST /projects' 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