提交 5eeb3910 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 b7e512c8
......@@ -159,6 +159,7 @@ karma:
- tmp/tests/frontend/
reports:
junit: junit_karma.xml
cobertura: coverage-javascript/cobertura-coverage.xml
karma-as-if-foss:
extends:
......@@ -226,6 +227,8 @@ coverage-frontend:
expire_in: 31d
paths:
- coverage-frontend/
reports:
cobertura: coverage-frontend/cobertura-coverage.xml
.qa-frontend-node:
extends:
......
......@@ -256,6 +256,8 @@ rspec:coverage:
- coverage/index.html
- coverage/assets/
- tmp/memory_test/
reports:
cobertura: coverage/coverage.xml
# EE/FOSS: default refs (MRs, master, schedules) jobs #
#######################################################
......
......@@ -371,6 +371,7 @@ group :development, :test do
gem 'scss_lint', '~> 0.56.0', require: false
gem 'haml_lint', '~> 0.34.0', require: false
gem 'simplecov', '~> 0.18.5', require: false
gem 'simplecov-cobertura', '~> 1.3.1', require: false
gem 'bundler-audit', '~> 0.6.1', require: false
gem 'benchmark-ips', '~> 2.3.0', require: false
......
......@@ -1021,6 +1021,8 @@ GEM
simplecov (0.18.5)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov-cobertura (1.3.1)
simplecov (~> 0.8)
simplecov-html (0.12.2)
sixarm_ruby_unaccent (1.2.0)
slack-messenger (2.3.3)
......@@ -1382,6 +1384,7 @@ DEPENDENCIES
sidekiq-cron (~> 1.0)
simple_po_parser (~> 1.1.2)
simplecov (~> 0.18.5)
simplecov-cobertura (~> 1.3.1)
slack-messenger (~> 2.3.3)
snowplow-tracker (~> 0.6.1)
spring (~> 2.0.0)
......
......@@ -25,6 +25,9 @@ export default {
UPDATE_ALERT_ASSIGNEES_ERROR: s__(
'AlertManagement|There was an error while updating the assignee(s) of the alert. Please try again.',
),
UPDATE_ALERT_ASSIGNEES_GRAPHQL_ERROR: s__(
'AlertManagement|This assignee cannot be assigned to this alert.',
),
components: {
GlIcon,
GlDropdown,
......@@ -156,9 +159,17 @@ export default {
projectPath: this.projectPath,
},
})
.then(() => {
.then(({ data: { alertSetAssignees: { errors } = [] } = {} } = {}) => {
this.hideDropdown();
this.$emit('alert-refresh');
if (errors[0]) {
return this.$emit(
'alert-sidebar-error',
`${this.$options.UPDATE_ALERT_ASSIGNEES_GRAPHQL_ERROR} ${errors[0]}.`,
);
}
return this.$emit('alert-refresh');
})
.catch(() => {
this.$emit('alert-sidebar-error', this.$options.UPDATE_ALERT_ASSIGNEES_ERROR);
......
......@@ -171,7 +171,7 @@
@include btn-green;
}
&.btn-inverted {
&.btn-inverted:not(.disabled):not(:disabled) {
&.btn-success {
@include btn-outline($white, $green-600, $green-500, $green-100, $green-700, $green-500, $green-200, $green-600, $green-800);
}
......@@ -501,18 +501,19 @@
// All disabled buttons, regardless of color, type, etc
%disabled {
background-color: $gray-light !important;
border-color: $gray-200 !important;
color: $gl-text-color-disabled !important;
opacity: 1 !important;
cursor: default !important;
background-color: $gray-light;
border-color: $gray-200;
color: $gl-text-color-disabled;
opacity: 1;
text-decoration: none;
cursor: default;
&.cursor-not-allowed {
cursor: not-allowed !important;
cursor: not-allowed;
}
i {
color: $gl-text-color-disabled !important;
color: $gl-text-color-disabled;
}
}
......@@ -526,6 +527,10 @@ fieldset[disabled] .btn,
&:hover {
@extend %disabled;
}
&.btn-link {
background-color: transparent;
}
}
[readonly] {
......
......@@ -421,7 +421,6 @@ img.emoji {
.append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; }
.prepend-bottom-32 { margin-bottom: 32px; }
.ml-10 { margin-left: 4.5rem; }
.inline { display: inline-block; }
.center { text-align: center; }
......
......@@ -19,11 +19,6 @@
$ui-light-bg: #dfdfdf;
$ui-dark-mode-bg: #1f1f1f;
label {
margin: 0 $gl-padding-32 $gl-padding 0;
text-align: center;
}
.preview {
font-size: 0;
height: 48px;
......
......@@ -31,7 +31,10 @@ module MembershipActions
def destroy
member = membershipable.members_and_requesters.find(params[:id])
Members::DestroyService.new(current_user).execute(member)
# !! is used in case unassign_issuables contains empty string which would result in nil
unassign_issuables = !!ActiveRecord::Type::Boolean.new.cast(params.delete(:unassign_issuables))
Members::DestroyService.new(current_user).execute(member, unassign_issuables: unassign_issuables)
respond_to do |format|
format.html do
......
# frozen_string_literal: true
class ResourceMilestoneEventFinder
include FinderMethods
MAX_PER_PAGE = 100
attr_reader :params, :current_user, :eventable
def initialize(current_user, eventable, params = {})
def initialize(current_user, eventable)
@current_user = current_user
@eventable = eventable
@params = params
end
# Returns the ResourceMilestoneEvents of the eventable
# visible to the user.
#
# @return ResourceMilestoneEvent::ActiveRecord_AssociationRelation
def execute
Kaminari.paginate_array(visible_events)
eventable.resource_milestone_events.include_relations
.where(milestone_id: readable_milestone_ids) # rubocop: disable CodeReuse/ActiveRecord
end
private
def visible_events
@visible_events ||= visible_to_user(events)
end
attr_reader :current_user, :eventable
def events
@events ||= eventable.resource_milestone_events.include_relations.page(page).per(per_page)
end
def readable_milestone_ids
readable_milestones = events_milestones.select do |milestone|
parent_availabilities[key_for_parent(milestone.parent)]
end
def visible_to_user(events)
events.select { |event| visible_for_user?(event) }
readable_milestones.map(&:id).uniq
end
def visible_for_user?(event)
milestone = event_milestones[event.milestone_id]
return if milestone.blank?
# rubocop: disable CodeReuse/ActiveRecord
def events_milestones
@events_milestones ||= Milestone.where(id: unique_milestone_ids_from_events)
.includes(:project, :group)
end
# rubocop: enable CodeReuse/ActiveRecord
parent = milestone.parent
parent_availabilities[key_for_parent(parent)]
def relevant_milestone_parents
events_milestones.map(&:parent).uniq
end
def parent_availabilities
@parent_availabilities ||= relevant_parents.to_h do |parent|
@parent_availabilities ||= relevant_milestone_parents.to_h do |parent|
[key_for_parent(parent), Ability.allowed?(current_user, :read_milestone, parent)]
end
end
def key_for_parent(parent)
"#{parent.class.name}_#{parent.id}"
end
def event_milestones
@milestones ||= events.map(&:milestone).uniq.to_h do |milestone|
[milestone.id, milestone]
end
end
def relevant_parents
@relevant_parents ||= event_milestones.map { |_id, milestone| milestone.parent }
# rubocop: disable CodeReuse/ActiveRecord
def unique_milestone_ids_from_events
eventable.resource_milestone_events.select(:milestone_id).distinct
end
# rubocop: enable CodeReuse/ActiveRecord
def per_page
[params[:per_page], MAX_PER_PAGE].compact.min
end
def page
params[:page] || 1
def key_for_parent(parent)
"#{parent.class.name}_#{parent.id}"
end
end
......@@ -48,6 +48,14 @@ module MembersHelper
"#{request.path}?#{options.to_param}"
end
def member_path(member, unassign_issuables: false)
if member.is_a?(GroupMember)
group_group_member_path(member.source, member, { unassign_issuables: unassign_issuables })
else
project_project_member_path(member.source, member, { unassign_issuables: unassign_issuables })
end
end
private
def source_text(member)
......
......@@ -2,9 +2,12 @@
class IssueAssignee < ApplicationRecord
belongs_to :issue
belongs_to :assignee, class_name: "User", foreign_key: :user_id
belongs_to :assignee, class_name: "User", foreign_key: :user_id, inverse_of: :issue_assignees
validates :assignee, uniqueness: { scope: :issue_id }
scope :in_projects, ->(project_ids) { joins(:issue).where("issues.project_id in (?)", project_ids) }
scope :on_issues, ->(issue_ids) { where(issue_id: issue_ids) }
end
IssueAssignee.prepend_if_ee('EE::IssueAssignee')
......@@ -2,7 +2,9 @@
class MergeRequestAssignee < ApplicationRecord
belongs_to :merge_request
belongs_to :assignee, class_name: "User", foreign_key: :user_id
belongs_to :assignee, class_name: "User", foreign_key: :user_id, inverse_of: :merge_request_assignees
validates :assignee, uniqueness: { scope: :merge_request_id }
scope :in_projects, ->(project_ids) { joins(:merge_request).where("merge_requests.target_project_id in (?)", project_ids) }
end
......@@ -69,6 +69,7 @@ class Snippet < ApplicationRecord
validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values }
after_save :store_mentions!, if: :any_mentionable_attributes_changed?
after_create :create_statistics
# Scopes
scope :are_internal, -> { where(visibility_level: Snippet::INTERNAL) }
......
......@@ -4,4 +4,32 @@ class SnippetStatistics < ApplicationRecord
belongs_to :snippet
validates :snippet, presence: true
delegate :repository, to: :snippet
def update_commit_count
self.commit_count = repository.commit_count
end
def update_repository_size
self.repository_size = repository.size.megabytes
end
def update_file_count
count = if snippet.repository_exists?
repository.ls_files(repository.root_ref).size
else
0
end
self.file_count = count
end
def refresh!
update_commit_count
update_repository_size
update_file_count
save!
end
end
......@@ -163,9 +163,10 @@ class User < ApplicationRecord
has_many :award_emoji, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
has_many :issue_assignees, inverse_of: :assignee
has_many :merge_request_assignees, inverse_of: :assignee
has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
has_many :assigned_merge_requests, class_name: "MergeRequest", through: :merge_request_assignees, source: :merge_request
has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'UserCallout'
......
......@@ -19,6 +19,8 @@ module AlertManagement
return error_no_updates if params.empty?
filter_assignees
return error_no_assignee_permissions if unauthorized_assignees?
old_assignees = alert.assignees.to_a
if alert.update(params)
......@@ -38,10 +40,6 @@ module AlertManagement
current_user&.can?(:update_alert_management_alert, alert)
end
def assignee_todo_allowed?
assignee&.can?(:read_alert_management_alert, alert)
end
def todo_service
strong_memoize(:todo_service) do
TodoService.new
......@@ -64,18 +62,20 @@ module AlertManagement
error(_('Please provide attributes to update'))
end
def error_no_assignee_permissions
error(_('Assignee has no permissions'))
end
# ----- Assignee-related behavior ------
def unauthorized_assignees?
params[:assignees]&.any? { |user| !user.can?(:read_alert_management_alert, alert) }
end
def filter_assignees
return if params[:assignees].nil?
params[:assignees] = Array(assignee)
end
def assignee
strong_memoize(:assignee) do
# Take first assignee while multiple are not currently supported
params[:assignees]&.first
end
# Always take first assignee while multiple are not currently supported
params[:assignees] = Array(params[:assignees].first)
end
def process_assignement(old_assignees)
......@@ -84,8 +84,7 @@ module AlertManagement
end
def assign_todo
# Remove check in follow-up issue https://gitlab.com/gitlab-org/gitlab/-/issues/222672
return unless assignee_todo_allowed?
return if alert.assignees.empty?
todo_service.assign_alert(alert, current_user)
end
......
......@@ -2,7 +2,9 @@
module Members
class DestroyService < Members::BaseService
def execute(member, skip_authorization: false, skip_subresources: false)
WAIT_FOR_DELETE = 1.hour
def execute(member, skip_authorization: false, skip_subresources: false, unassign_issuables: false)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member)
@skip_auth = skip_authorization
......@@ -19,6 +21,7 @@ module Members
delete_subresources(member) unless skip_subresources
enqueue_delete_todos(member)
enqueue_unassign_issuables(member) if unassign_issuables
after_execute(member: member)
......@@ -64,6 +67,14 @@ module Members
raise "Unknown member type: #{member}!"
end
end
def enqueue_unassign_issuables(member)
source_type = member.is_a?(GroupMember) ? 'Group' : 'Project'
member.run_after_commit do
MembersDestroyer::UnassignIssuablesWorker.perform_in(WAIT_FOR_DELETE, member.user_id, member.source_id, source_type)
end
end
end
end
......
# frozen_string_literal: true
module Members
class UnassignIssuablesService
attr_reader :user, :entity
def initialize(user, entity)
@user = user
@entity = entity
end
def execute
return unless entity && user
project_ids = entity.is_a?(Group) ? entity.all_projects.select(:id) : [entity.id]
user.issue_assignees.on_issues(Issue.in_projects(project_ids).select(:id)).delete_all
user.merge_request_assignees.in_projects(project_ids).delete_all
user.invalidate_cache_counts
end
end
end
......@@ -8,11 +8,12 @@
%p
= s_('Preferences|Customize the appearance of the application header and navigation sidebar.')
.col-lg-8.application-theme
- Gitlab::Themes.each do |theme|
= label_tag do
.preview{ class: theme.css_class }
= f.radio_button :theme_id, theme.id, checked: Gitlab::Themes.for_user(@user).id == theme.id
= theme.name
.row
- Gitlab::Themes.each do |theme|
%label.col-6.col-sm-4.col-md-3.gl-mb-5.gl-text-center
.preview{ class: theme.css_class }
= f.radio_button :theme_id, theme.id, checked: Gitlab::Themes.for_user(@user).id == theme.id
= theme.name
.col-sm-12
%hr
......
......@@ -118,7 +118,7 @@
data: { confirm: leave_confirmation_message(member.source) },
class: "btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}"
- elsif !user&.project_bot?
= link_to member,
= link_to member_path(member.member),
method: :delete,
data: { confirm: remove_member_message(member), qa_selector: 'delete_member_button' },
class: "btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}",
......
......@@ -1115,6 +1115,14 @@
:weight: 1
:idempotent:
:tags: []
- :name: unassign_issuables:members_destroyer_unassign_issuables
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: update_namespace_statistics:namespaces_root_statistics
:feature_category: :source_code_management
:has_external_dependencies:
......
# frozen_string_literal: true
module MembersDestroyer
class UnassignIssuablesWorker
include ApplicationWorker
ENTITY_TYPES = %w(Group Project).freeze
queue_namespace :unassign_issuables
feature_category :authentication_and_authorization
idempotent!
def perform(user_id, entity_id, entity_type)
unless ENTITY_TYPES.include?(entity_type)
logger.error(
message: "#{entity_type} is not a supported entity.",
entity_type: entity_type,
entity_id: entity_id,
user_id: user_id
)
return
end
user = User.find(user_id)
entity = entity_type.constantize.find(entity_id)
::Members::UnassignIssuablesService.new(user, entity).execute
end
end
end
---
title: Suppress progress on pulling on Performance Test
merge_request: 34368
author: Takuya Noguchi
type: other
---
title: Added CI template for Dart
merge_request: 32942
author: agilob
type: added
---
title: Extend members REST API with the option to unassign Issues and Merge Requests when member leaves team
merge_request: 34388
author:
type: changed
---
title: Copy project snippet routes under - scope
merge_request: 35022
author:
type: other
---
title: Fix pagination for resource milestone events api
merge_request: 33845
author:
type: fixed
---
title: Add snippet statistics logic
merge_request: 35118
author:
type: added
---
title: Limit alert assignment to only users who can read alerts
merge_request: 34681
author:
type: fixed
---
title: Fix alignment of navigation theme options
merge_request: 35041
author:
type: fixed
......@@ -181,7 +181,7 @@ module.exports = function(config) {
if (process.env.BABEL_ENV === 'coverage' || process.env.NODE_ENV === 'coverage') {
karmaConfig.reporters.push('coverage-istanbul');
karmaConfig.coverageIstanbulReporter = {
reports: ['html', 'text-summary'],
reports: ['html', 'text-summary', 'cobertura'],
dir: 'coverage-javascript/',
subdir: '.',
fixWebpackSourcePaths: true,
......
......@@ -369,6 +369,19 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
# Serve snippet routes under /-/snippets.
# To ensure an old unscoped routing is used for the UI we need to
# add prefix 'as' to the scope routing and place it below original routing.
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/29572
scope '-', as: :scoped do
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do # rubocop: disable Cop/PutProjectRoutesUnderScope
member do
get :raw
post :mark_as_spam
end
end
end
namespace :prometheus do
resources :alerts, constraints: { id: /\d+/ }, only: [:index, :create, :show, :update, :destroy] do # rubocop: disable Cop/PutProjectRoutesUnderScope
post :notify, on: :collection
......
......@@ -262,6 +262,8 @@
- 1
- - todos_destroyer
- 1
- - unassign_issuables
- 1
- - update_external_pull_requests
- 3
- - update_highest_role
......
......@@ -373,6 +373,7 @@ DELETE /projects/:id/members/:user_id
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `user_id` | integer | yes | The user ID of the member |
| `unassign_issuables` | boolean | false | Flag indicating if the removed member should be unassigned from any issues or merge requests within given group or project |
```shell
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/members/:user_id"
......
---
stage: Plan
group: Project Management
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Resource weight events API
Resource weight events keep track of what happens to GitLab [issues](../user/project/issues/).
Use them to track which weight was set, who did it, and when it happened.
## Issues
### List project issue weight events
Gets a list of all weight events for a single issue.
```plaintext
GET /projects/:id/issues/:issue_iid/resource_weight_events
```
| Attribute | Type | Required | Description |
| ----------- | -------------- | -------- | ------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `issue_iid` | integer | yes | The IID of an issue |
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/resource_weight_events"
```
Example response:
```json
[
{
"id": 142,
"user": {
"id": 1,
"name": "Administrator",
"username": "root",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/root"
},
"created_at": "2018-08-20T13:38:20.077Z",
"issue_id": 253,
"weight": 3
},
{
"id": 143,
"user": {
"id": 1,
"name": "Administrator",
"username": "root",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/root"
},
"created_at": "2018-08-21T14:38:20.077Z",
"issue_id": 253,
"weight": 2
}
]
```
### Get single issue weight event
Returns a single weight event for a specific project issue
```plaintext
GET /projects/:id/issues/:issue_iid/resource_weight_events/:resource_weight_event_id
```
Parameters:
| Attribute | Type | Required | Description |
| ----------------------------- | -------------- | -------- | ------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path](README.md#namespaced-path-encoding) of the project |
| `issue_iid` | integer | yes | The IID of an issue |
| `resource_weight_event_id` | integer | yes | The ID of a weight event |
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/resource_weight_events/143"
```
Example response:
```json
{
"id": 143,
"user": {
"id": 1,
"name": "Administrator",
"username": "root",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/root"
},
"created_at": "2018-08-21T14:38:20.077Z",
"issue_id": 253,
"weight": 2
}
```
# Database Reviewer Guidelines
This page includes introductory material for new database reviewers.
If you are interested in getting an application update reviewed,
check the [database review guidelines](../database_review.md).
## Scope of work done by a database reviewer
Database reviewers are domain experts who have substantial experience with databases,
`SQL`, and query performance optimization.
A database review is required whenever an application update [touches the database](../database_review.md#general-process).
The database reviewer is tasked with reviewing the database specific updates and
making sure that any queries or modifications will perform without issues
at the scale of GitLab.com.
For more information on the database review process, check the [database review guidelines](../database_review.md).
## How to apply for becoming a database reviewer
Team members are encouraged to self-identify as database domain experts and add it to their [team profile](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/data/team.yml)
```yaml
projects:
gitlab:
- reviewer database
```
Assign the MR which adds your expertise to the `team.yml` file to a database maintainer
or the [Database Team's Engineering Manager](https://about.gitlab.com/handbook/engineering/development/enablement/database/).
Once the `team.yml` update is merged, the [Reviewer roulette](../code_review.md#reviewer-roulette)
may recommend you as a database reviewer.
## Resources for database reviewers
As a database reviewer, join the internal `#database` Slack channel and ask questions or discuss
database related issues with other database reviewers and maintainers.
There is also an optional database office hours call held bi-weekly, alternating between
European/US and APAC friendly hours. You can join the office hours call and bring topics
that require a more in-depth discussion between the database reviewers and maintainers:
- [Database Office Hours Agenda](https://docs.google.com/document/d/1wgfmVL30F8SdMg-9yY6Y8djPSxWNvKmhR5XmsvYX1EI/edit).
- [Youtube playlist with past recordings](https://www.youtube.com/playlist?list=PL05JrBw4t0Kp-kqXeiF7fF7cFYaKtdqXM).
You should also join the [#database-labs](../understanding_explain_plans.md#database-lab)
Slack channel and get familiar with how to use Joe, the slackbot that provides developers
with their own clone of the production database.
Understanding and efficiently using `EXPLAIN` plans is at the core of the database review process.
The following guides provide a quick introduction and links to follow on more advanced topics:
- Guide on [understanding EXPLAIN plans](../understanding_explain_plans.md).
- [Explaining the unexplainable series in depesz](https://www.depesz.com/tag/unexplainable/).
Finally, you can find various guides in the [Database guides](index.md) page that cover more specific
topics and use cases. The most frequently required during database reviewing are the following:
- [Migrations style guide](../migration_style_guide.md) for creating safe SQL migrations.
- [What requires downtime?](../what_requires_downtime.md).
- [SQL guidelines](../sql.md) for working with SQL queries.
## How to apply for becoming a database maintainer
Once a database reviewer feels confident on switching to a database maintainer,
they can update their [team profile](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/data/team.yml)
to a `trainee_maintainer database`:
```yaml
projects:
gitlab:
- trainee_maintainer database
```
The first step is to a create a [Trainee Database Maintainer Issue](https://gitlab.com/gitlab-com/www-gitlab-com/issues/new?issuable_template=trainee-database-maintainer).
Use and follow the process described in the 'Trainee database maintainer' template.
Note that [trainee maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer)
are three times as likely to be picked by the [Danger bot](../dangerbot.md) as other reviewers.
## What to do if you feel overwhelmed
Similar to all types of reviews, [unblocking others is always a top priority](https://about.gitlab.com/handbook/values/#global-optimization).
Database reviewers are expected to [review assigned merge requests in a timely manner](../code_review.md#review-turnaround-time)
or let the author know as soon as possible and help them find another reviewer or maintainer.
We are doing reviews to help the rest of the GitLab team and, at the same time, get exposed
to more use cases, get a lot of insights and hone our database and data management skills.
If you are feeling overwhelmed, think you are at capacity, and are unable to accept any more
reviews until some have been completed, communicate this through your GitLab status by setting
the `:red_circle:` emoji and mentioning that you are at capacity in the status text.
# Database guides
## Database Reviews
- If you're creating a database MR for review, check out our [Database review guidelines](../database_review.md).
It provides an introduction on database-related changes, migrations, and complex SQL queries.
- If you're a database reviewer or want to become one, check out our [introduction to reviewing database changes](database_reviewer_guidelines.md).
## Tooling
- [Understanding EXPLAIN plans](../understanding_explain_plans.md)
......
......@@ -8,7 +8,7 @@ This page offers a walkthrough of a common configuration
for GitLab on AWS. You should customize it to accommodate your needs.
NOTE: **Note**
For organizations with 300 users or less, the recommended AWS installation method is to launch an EC2 single box [Omnibus Installation](https://about.gitlab.com/install/) and implement a snapshot strategy for backing up the data.
For organizations with 1,000 users or less, the recommended AWS installation method is to launch an EC2 single box [Omnibus Installation](https://about.gitlab.com/install/) and implement a snapshot strategy for backing up the data. See the [1,000 user reference architecture](../../administration/reference_architectures/1k_users.md) for more.
## Introduction
......
......@@ -6,6 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Infrastructure as code with Terraform and GitLab
## Motivation
The Terraform integration features within GitLab enable your GitOps / Infrastructure-as-Code (IaC)
workflows to tie into GitLab's authentication and authorization. These features focus on
lowering the barrier to entry for teams to adopt Terraform, collaborate effectively within
GitLab, and support Terraform best practices.
## GitLab managed Terraform State
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2673) in GitLab 13.0.
......
......@@ -835,7 +835,7 @@ The instructions below relate to installing and running Certbot on a Linux serve
## Using an older version of `gitlabktl`
There may be situations where you want to run an older version of `gitlabktl`. This
requires setting an older version of the `gitlabktl` image in the `.gitlab-ci.yml file.`
requires setting an older version of the `gitlabktl` image in the `.gitlab-ci.yml` file.
To set an older version, add `image:` to the `functions:deploy` block. For example:
......
......@@ -145,6 +145,8 @@ module API
desc 'Removes a user from a group or project.'
params do
requires :user_id, type: Integer, desc: 'The user ID of the member'
optional :unassign_issuables, type: Boolean, default: false,
desc: 'Flag indicating if the removed member should be unassigned from any issues or merge requests within given group or project'
end
# rubocop: disable CodeReuse/ActiveRecord
delete ":id/members/:user_id" do
......@@ -152,7 +154,7 @@ module API
member = source.members.find_by!(user_id: params[:user_id])
destroy_conditionally!(member) do
::Members::DestroyService.new(current_user).execute(member)
::Members::DestroyService.new(current_user).execute(member, unassign_issuables: params[:unassign_issuables])
end
end
# rubocop: enable CodeReuse/ActiveRecord
......
......@@ -26,8 +26,7 @@ module API
get ":id/#{eventables_str}/:eventable_id/resource_milestone_events" do
eventable = find_noteable(eventable_type, params[:eventable_id])
opts = { page: params[:page], per_page: params[:per_page] }
events = ResourceMilestoneEventFinder.new(current_user, eventable, opts).execute
events = ResourceMilestoneEventFinder.new(current_user, eventable).execute
present paginate(events), with: Entities::ResourceMilestoneEvent
end
......
# https://hub.docker.com/r/google/dart
image: google/dart:2.8.4
variables:
# Use to learn more:
# pub run test --help
PUB_VARS: "--platform vm --timeout 30s --concurrency=6 --test-randomize-ordering-seed=random --reporter=expanded"
# Cache downloaded dependencies and plugins between builds.
# To keep cache across branches add 'key: "$CI_JOB_NAME"'
cache:
paths:
- .pub-cache/global_packages
before_script:
- export PATH="$PATH":"~/.pub-cache/bin"
- pub get --no-precompile
test:
stage: test
script:
- pub run test $PUB_VARS
......@@ -4,6 +4,7 @@ performance:
allow_failure: true
variables:
DOCKER_TLS_CERTDIR: ""
SITESPEED_IMAGE: "sitespeedio/sitespeed.io:11.2.0"
services:
- docker:19.03.11-dind
script:
......@@ -17,13 +18,14 @@ performance:
- mkdir gitlab-exporter
- wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.0.0/index.js
- mkdir sitespeed-results
- docker pull --quiet ${SITESPEED_IMAGE}
- |
if [ -f .gitlab-urls.txt ]
then
sed -i -e 's@^@'"$CI_ENVIRONMENT_URL"'@' .gitlab-urls.txt
docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:11.2.0 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt
docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io ${SITESPEED_IMAGE} --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt
else
docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:11.2.0 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL"
docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io ${SITESPEED_IMAGE} --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL"
fi
- mv sitespeed-results/data/performance.json performance.json
artifacts:
......
......@@ -2011,6 +2011,9 @@ msgstr ""
msgid "AlertManagement|There was an error while updating the status of the alert. Please try again."
msgstr ""
msgid "AlertManagement|This assignee cannot be assigned to this alert."
msgstr ""
msgid "AlertManagement|Tool"
msgstr ""
......@@ -3020,6 +3023,9 @@ msgid_plural "%d Assignees"
msgstr[0] ""
msgstr[1] ""
msgid "Assignee has no permissions"
msgstr ""
msgid "Assignee lists not available with your current license"
msgstr ""
......
......@@ -26,6 +26,6 @@ reportFiles
const context = createContext({ coverageMap: coverageMap, dir: 'coverage-frontend' });
['json', 'lcov', 'text-summary', 'clover'].forEach(reporter => {
['json', 'lcov', 'text-summary', 'clover', 'cobertura'].forEach(reporter => {
create(reporter, {}).execute(context);
});
......@@ -2,6 +2,7 @@
require_relative '../spec/simplecov_env'
SimpleCovEnv.configure_profile
SimpleCovEnv.configure_formatter
module SimpleCov
module ResultMerger
......
......@@ -42,18 +42,6 @@ RSpec.describe ResourceMilestoneEventFinder do
expect(subject).to be_empty
end
it 'paginates results' do
milestone = create(:milestone, project: issue_project)
create_event(milestone)
create_event(milestone)
issue_project.add_guest(user)
paginated = described_class.new(user, issue, per_page: 1).execute
expect(subject.count).to eq 2
expect(paginated.count).to eq 1
end
context 'when multiple events share the same milestone' do
it 'avoids N+1 queries' do
issue_project.add_developer(user)
......@@ -71,8 +59,8 @@ RSpec.describe ResourceMilestoneEventFinder do
create_event(milestone2, :add)
create_event(milestone2, :remove)
# 1 events + 1 milestones + 1 project + 1 user + 4 ability
expect { described_class.new(user, issue).execute }.not_to exceed_query_limit(control_count + 7)
# 1 milestones + 1 project + 1 user + 4 ability
expect { described_class.new(user, issue).execute }.not_to exceed_query_limit(control_count + 6)
end
end
......
......@@ -59,7 +59,7 @@ describe('Alert Details Sidebar Assignees', () => {
describe('updating the alert status', () => {
const mockUpdatedMutationResult = {
data: {
updateAlertStatus: {
alertSetAssignees: {
errors: [],
alert: {
assigneeUsernames: ['root'],
......@@ -125,6 +125,26 @@ describe('Alert Details Sidebar Assignees', () => {
});
});
it('shows an error when request contains error messages', () => {
wrapper.setData({ isDropdownSearching: false });
const errorMutationResult = {
data: {
alertSetAssignees: {
errors: ['There was a problem for sure.'],
alert: {},
},
},
};
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(errorMutationResult);
return wrapper.vm.$nextTick().then(() => {
const SideBarAssigneeItem = wrapper.findAll(SidebarAssignee).at(0);
SideBarAssigneeItem.vm.$emit('click');
expect(wrapper.emitted('alert-refresh')).toBeUndefined();
});
});
it('stops updating and cancels loading when the request fails', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
wrapper.vm.updateAlertAssignees('root');
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe IssueAssignee do
RSpec.describe IssueAssignee do
let(:issue) { create(:issue) }
subject { issue.issue_assignees.build(assignee: create(:user)) }
......@@ -15,4 +15,37 @@ describe IssueAssignee do
describe 'validations' do
it { is_expected.to validate_uniqueness_of(:assignee).scoped_to(:issue_id) }
end
describe 'scopes' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:project_issue) { create(:issue, project: project, assignee_ids: [user.id]) }
before do
issue.update!(assignee_ids: [user.id])
end
context 'in_projects' do
it 'returns issue assignees for given project' do
expect(IssueAssignee.count).to eq 2
assignees = IssueAssignee.in_projects([project])
expect(assignees.count).to eq 1
expect(assignees.first.user_id).to eq project_issue.issue_assignees.first.user_id
expect(assignees.first.issue_id).to eq project_issue.issue_assignees.first.issue_id
end
end
context 'on_issues' do
it 'returns issue assignees for given issues' do
expect(IssueAssignee.count).to eq 2
assignees = IssueAssignee.on_issues([project_issue])
expect(assignees.count).to eq 1
expect(assignees.first.issue_id).to eq project_issue.issue_assignees.first.issue_id
end
end
end
end
......@@ -15,4 +15,26 @@ describe MergeRequestAssignee do
describe 'validations' do
it { is_expected.to validate_uniqueness_of(:assignee).scoped_to(:merge_request_id) }
end
describe 'scopes' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:project_merge_request) { create(:merge_request, target_project: project, source_project: project, assignee_ids: [user.id]) }
before do
merge_request.update!(assignee_ids: [user.id])
end
context 'in_projects' do
it 'returns issue assignees for given project' do
expect(MergeRequestAssignee.count).to eq 2
assignees = MergeRequestAssignee.in_projects([project])
expect(assignees.count).to eq 1
expect(assignees.first.user_id).to eq project_merge_request.merge_request_assignees.first.user_id
expect(assignees.first.merge_request_id).to eq project_merge_request.merge_request_assignees.first.merge_request_id
end
end
end
end
......@@ -92,6 +92,17 @@ describe Snippet do
end
end
describe 'callbacks' do
it 'creates snippet statistics when the snippet is created' do
snippet = build(:snippet)
expect(snippet.statistics).to be_nil
snippet.save
expect(snippet.statistics).to be_persisted
end
end
describe '#to_reference' do
context 'when snippet belongs to a project' do
let(:project) { build(:project, name: 'sample-project') }
......
......@@ -3,6 +3,87 @@
require 'spec_helper'
describe SnippetStatistics do
let_it_be(:snippet_without_repo) { create(:snippet) }
let_it_be(:snippet_with_repo) { create(:snippet, :repository) }
let(:statistics) { snippet_with_repo.statistics }
it { is_expected.to belong_to(:snippet) }
it { is_expected.to validate_presence_of(:snippet) }
describe '#update_commit_count' do
subject { statistics.update_commit_count }
it 'updates the count of commits' do
commit_count = snippet_with_repo.repository.commit_count
subject
expect(statistics.commit_count).to eq commit_count
end
context 'when the snippet does not have a repository' do
let(:statistics) { snippet_without_repo.statistics }
it 'returns 0' do
expect(subject).to eq 0
expect(statistics.commit_count).to eq 0
end
end
end
describe '#update_file_count' do
subject { statistics.update_file_count }
it 'updates the count of files' do
file_count = snippet_with_repo.repository.ls_files(nil).count
subject
expect(statistics.file_count).to eq file_count
end
context 'when the snippet does not have a repository' do
let(:statistics) { snippet_without_repo.statistics }
it 'returns 0' do
expect(subject).to eq 0
expect(statistics.file_count).to eq 0
end
end
end
describe '#update_repository_size' do
subject { statistics.update_repository_size }
it 'updates the repository_size' do
repository_size = snippet_with_repo.repository.size.megabytes.to_i
subject
expect(statistics.repository_size).to eq repository_size
end
context 'when the snippet does not have a repository' do
let(:statistics) { snippet_without_repo.statistics }
it 'returns 0' do
expect(subject).to eq 0
expect(statistics.repository_size).to eq 0
end
end
end
describe '#refresh!' do
subject { statistics.refresh! }
it 'retrieves and saves statistic data from repository' do
expect(statistics).to receive(:update_commit_count)
expect(statistics).to receive(:update_file_count)
expect(statistics).to receive(:update_repository_size)
expect(statistics).to receive(:save!)
subject
end
end
end
......@@ -407,6 +407,10 @@ describe 'project routing' do
it 'to #destroy' do
expect(delete('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
end
it 'to #show from scope routing' do
expect(get('/gitlab/gitlabhq/-/snippets/1')).to route_to('projects/snippets#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
end
end
# test_project_hook POST /:project_id/hooks/:id/test(.:format) hooks#test
......
......@@ -4,6 +4,7 @@ require 'spec_helper'
describe AlertManagement::Alerts::UpdateService do
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:other_user_with_permissions) { create(:user) }
let_it_be(:user_without_permissions) { create(:user) }
let_it_be(:alert, reload: true) { create(:alert_management_alert) }
let_it_be(:project) { alert.project }
......@@ -15,119 +16,131 @@ describe AlertManagement::Alerts::UpdateService do
before_all do
project.add_developer(user_with_permissions)
project.add_developer(other_user_with_permissions)
end
describe '#execute' do
shared_examples 'does not add a todo' do
specify { expect { response }.not_to change(Todo, :count) }
end
shared_examples 'does not add a system note' do
specify { expect { response }.not_to change(Note, :count) }
end
shared_examples 'error response' do |message|
it_behaves_like 'does not add a todo'
it_behaves_like 'does not add a system note'
it 'has an informative message' do
expect(response).to be_error
expect(response.message).to eq(message)
end
end
subject(:response) { service.execute }
context 'when the current_user is nil' do
let(:current_user) { nil }
it 'results in an error' do
expect(response).to be_error
expect(response.message).to eq('You have no permissions')
end
it_behaves_like 'error response', 'You have no permissions'
end
context 'when user does not have permission to update alerts' do
context 'when current_user does not have permission to update alerts' do
let(:current_user) { user_without_permissions }
it 'results in an error' do
expect(response).to be_error
expect(response.message).to eq('You have no permissions')
end
it_behaves_like 'error response', 'You have no permissions'
end
context 'when no parameters are included' do
it 'results in an error' do
expect(response).to be_error
expect(response.message).to eq('Please provide attributes to update')
end
it_behaves_like 'error response', 'Please provide attributes to update'
end
context 'when an error occures during update' do
context 'when an error occurs during update' do
let(:params) { { title: nil } }
it 'results in an error' do
expect { response }.not_to change { alert.reload.notes.count }
expect(response).to be_error
expect(response.message).to eq("Title can't be blank")
end
it_behaves_like 'error response', "Title can't be blank"
end
context 'when a model attribute is included without assignees' do
let(:params) { { title: 'This is an updated alert.' } }
it_behaves_like 'does not add a todo'
it_behaves_like 'does not add a system note'
it 'updates the attribute' do
original_title = alert.title
expect { response }.to change { alert.title }.from(original_title).to(params[:title])
expect(response).to be_success
end
it 'skips adding a todo' do
expect { response }.not_to change(Todo, :count)
end
end
context 'when assignees are included' do
let(:params) { { assignees: [user_with_permissions] } }
shared_examples 'adds a todo' do
let(:assignee) { expected_assignees.first }
after do
alert.assignees = []
specify do
expect { response }.to change { assignee.reload.todos.count }.by(1)
expect(assignee.todos.last.author).to eq(current_user)
end
end
it 'assigns the user' do
expect { response }.to change { alert.reload.assignees }.from([]).to(params[:assignees])
expect(response).to be_success
shared_examples 'adds a system note' do
specify { expect { response }.to change { alert.reload.notes.count }.by(1) }
end
it 'creates a system note for the assignment' do
expect { response }.to change { alert.reload.notes.count }.by(1)
end
shared_examples 'successful assignment' do
it_behaves_like 'adds a system note'
it_behaves_like 'adds a todo'
it 'adds a todo' do
expect { response }.to change { Todo.where(user: user_with_permissions).count }.by(1)
after do
alert.assignees = []
end
specify do
expect { response }.to change { alert.reload.assignees }.from([]).to(expected_assignees)
expect(response).to be_success
end
end
context 'when current user is not the assignee' do
let(:assignee_user) { create(:user) }
let(:params) { { assignees: [assignee_user] } }
let(:expected_assignees) { params[:assignees] }
it 'skips adding todo for assignee without permission to read alert' do
expect { response }.not_to change(Todo, :count)
end
context 'when the assignee is the current user' do
let(:params) { { assignees: [current_user] } }
context 'when assignee has read permission' do
before do
project.add_developer(assignee_user)
end
it_behaves_like 'successful assignment'
end
it 'adds a todo' do
response
context 'when the assignee has read permissions' do
let(:params) { { assignees: [other_user_with_permissions] } }
expect(Todo.first.author).to eq(current_user)
end
end
it_behaves_like 'successful assignment'
end
context 'when current_user is nil' do
let(:current_user) { nil }
context 'when the assignee does not have read permissions' do
let(:params) { { assignees: [user_without_permissions] } }
it 'skips adding todo if current_user is nil' do
project.add_developer(assignee_user)
it_behaves_like 'error response', 'Assignee has no permissions'
end
expect { response }.not_to change(Todo, :count)
end
context 'when user is already assigned' do
let(:params) { { assignees: [user_with_permissions] } }
before do
alert.assignees << user_with_permissions
end
it_behaves_like 'does not add a system note'
# TODO: We should not add another todo in this scenario
it_behaves_like 'adds a todo'
end
context 'with multiple users included' do
let(:params) { { assignees: [user_with_permissions, user_without_permissions] } }
let(:expected_assignees) { [user_with_permissions] }
it 'assigns the first permissioned user' do
expect { response }.to change { alert.reload.assignees }.from([]).to([user_with_permissions])
expect(response).to be_success
end
it_behaves_like 'successful assignment'
end
end
end
......
......@@ -56,12 +56,23 @@ describe Members::DestroyService do
expect(member_user.todos_pending_count).to be(1)
expect(member_user.todos_done_count).to be(1)
described_class.new(current_user).execute(member, opts)
service = described_class.new(current_user)
if opts[:unassign_issuables]
expect(service).to receive(:enqueue_unassign_issuables).with(member)
end
service.execute(member, opts)
expect(member_user.assigned_open_merge_requests_count).to be(0)
expect(member_user.assigned_open_issues_count).to be(0)
expect(member_user.todos_pending_count).to be(0)
expect(member_user.todos_done_count).to be(0)
unless opts[:unassign_issuables]
expect(member_user.assigned_merge_requests.opened.count).to be(1)
expect(member_user.assigned_issues.opened.count).to be(1)
end
end
end
......@@ -100,7 +111,7 @@ describe Members::DestroyService do
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError'
it_behaves_like 'a service destroying a member with access' do
let(:opts) { { skip_authorization: true } }
let(:opts) { { skip_authorization: true, unassign_issuables: true } }
end
end
......@@ -114,7 +125,7 @@ describe Members::DestroyService do
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError'
it_behaves_like 'a service destroying a member with access' do
let(:opts) { { skip_authorization: true } }
let(:opts) { { skip_authorization: true, unassign_issuables: true } }
end
end
end
......@@ -133,6 +144,12 @@ describe Members::DestroyService do
end
it_behaves_like 'a service destroying a member with access'
context 'unassign issuables' do
it_behaves_like 'a service destroying a member with access' do
let(:opts) { { unassign_issuables: true } }
end
end
end
context 'with a group member' do
......@@ -143,6 +160,12 @@ describe Members::DestroyService do
end
it_behaves_like 'a service destroying a member with access'
context 'unassign issuables' do
it_behaves_like 'a service destroying a member with access' do
let(:opts) { { unassign_issuables: true } }
end
end
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Members::UnassignIssuablesService do
let_it_be(:group) { create(:group, :private) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user, reload: true) { create(:user) }
let_it_be(:assigned_issue1, reload: true) { create(:issue, project: project, assignees: [user]) }
let_it_be(:assigned_issue2, reload: true) { create(:issue, project: project, assignees: [user]) }
let!(:assigned_merge_request1) { create(:merge_request, :simple, :closed, target_project: project, source_project: project, assignees: [user], title: 'Test1') }
let!(:assigned_merge_request2) { create(:merge_request, :simple, :opened, target_project: project, source_project: project, assignees: [user], title: 'Test2') }
describe '#execute' do
RSpec.shared_examples 'un-assigning issuables' do |issue_count, mr_count, open_issue_count, open_mr_count|
it 'removes issuable assignments', :aggregate_failures do
expect(user.assigned_issues.count).to eq(issue_count)
expect(user.assigned_merge_requests.count).to eq(mr_count)
subject
expect(user.assigned_issues.count).to eq(0)
expect(user.assigned_merge_requests.count).to eq(0)
end
it 'invalidates user cache', :aggregate_failures, :clean_gitlab_redis_cache do
expect(user.assigned_open_merge_requests_count).to eq(open_mr_count)
expect(user.assigned_open_issues_count).to eq(open_issue_count)
subject
expect(user.assigned_open_merge_requests_count).to eq(0)
expect(user.assigned_open_issues_count).to eq(0)
end
end
context 'when a user leaves a project' do
before do
project.add_maintainer(user)
end
subject { described_class.new(user, project).execute }
it_behaves_like 'un-assigning issuables', 2, 2, 2, 1
end
context 'when a user leaves a group' do
let_it_be(:project2) { create(:project, group: group) }
let_it_be(:assigned_issue3, reload: true) { create(:issue, project: project2, assignees: [user]) }
let_it_be(:assigned_issue4, reload: true) { create(:issue, project: project2, assignees: [user]) }
let!(:assigned_merge_request3) { create(:merge_request, :simple, :closed, target_project: project2, source_project: project2, assignees: [user], title: 'Test1') }
let!(:assigned_merge_request4) { create(:merge_request, :simple, :opened, target_project: project2, source_project: project2, assignees: [user], title: 'Test2') }
before do
group.add_maintainer(user)
end
subject { described_class.new(user, group).execute }
it_behaves_like 'un-assigning issuables', 4, 4, 4, 2
end
end
end
# frozen_string_literal: true
require 'simplecov'
require 'simplecov-cobertura'
require 'active_support/core_ext/numeric/time'
require_relative '../lib/gitlab/utils'
......@@ -12,10 +13,19 @@ module SimpleCovEnv
configure_profile
configure_job
configure_formatter
SimpleCov.start
end
def configure_formatter
SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([
SimpleCov::Formatter::SimpleFormatter,
SimpleCov::Formatter::HTMLFormatter,
SimpleCov::Formatter::CoberturaFormatter
])
end
def configure_job
SimpleCov.configure do
if ENV['CI_JOB_NAME']
......
......@@ -16,6 +16,29 @@ RSpec.shared_examples 'resource_milestone_events API' do |parent_type, eventable
expect(json_response.first['action']).to eq(event.action)
end
context 'when there is an event with a milestone which is not visible for requesting user' do
let!(:private_project) { create(:project, :private) }
let!(:private_milestone) { create(:milestone, project: private_project) }
let!(:other_user) { create(:user) }
it 'returns the expected events' do
create_event(private_milestone)
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events", other_user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(response.headers['X-Total']).to eq('1')
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.first['id']).to eq(event.id)
expect(json_response.first['milestone']['id']).to eq(event.milestone.id)
expect(json_response.first['action']).to eq(event.action)
end
end
it "returns a 404 error when eventable id not found" do
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_milestone_events", user)
......@@ -60,6 +83,20 @@ RSpec.shared_examples 'resource_milestone_events API' do |parent_type, eventable
end
end
describe 'pagination' do
let!(:event1) { create_event(milestone) }
let!(:event2) { create_event(milestone) }
# https://gitlab.com/gitlab-org/gitlab/-/issues/220192
it 'returns the second page' do
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events?page=2&per_page=1", user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to eq(1)
expect(json_response.first['id']).to eq(event2.id)
end
end
def create_event(milestone, action: :add)
create(:resource_milestone_event, eventable.class.name.underscore => eventable, milestone: milestone, action: action)
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe MembersDestroyer::UnassignIssuablesWorker do
let_it_be(:group) { create(:group, :private) }
let_it_be(:user, reload: true) { create(:user) }
context 'when unsupported membership source entity' do
it 'exits early and logs error' do
params = { message: "SomeEntity is not a supported entity.", entity_type: 'SomeEntity', entity_id: group.id, user_id: user.id }
expect(Sidekiq.logger).to receive(:error).with(params)
described_class.new.perform(user.id, group.id, 'SomeEntity')
end
end
it "calls the Members::UnassignIssuablesService with the params it was given" do
service = double
expect(Members::UnassignIssuablesService).to receive(:new).with(user, group).and_return(service)
expect(service).to receive(:execute)
described_class.new.perform(user.id, group.id, 'Group')
end
end
......@@ -843,10 +843,10 @@
eslint-plugin-vue "^6.2.1"
vue-eslint-parser "^7.0.0"
"@gitlab/svgs@1.140.0":
version "1.140.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.140.0.tgz#593f1f65b0df57c3399fcfb9f472f59aa64da074"
integrity sha512-6gANJGi2QkpvOgFTMcY3SIwEqhO69i6R3jU4BSskkVziwDdAWxGonln22a4Iu//Iv0NrsFDpAA0jIVfnJzw0iA==
"@gitlab/svgs@1.141.0":
version "1.141.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.141.0.tgz#0d6c03511180669538be5c63a96b2ae28840bbf4"
integrity sha512-6k4HA0jVGMb/47bqcflSdpLGpo0rN2yd5K2X39LVQxukrg56PdZQvFPxT2UDOgChLstEtmN/iJTZuXqpeVOg+g==
"@gitlab/ui@17.2.0":
version "17.2.0"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册