Commit 5fe2ffc8 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents a0f57c39 4d8eae7f
...@@ -3,4 +3,6 @@ export default { ...@@ -3,4 +3,6 @@ export default {
merge_requests: 'merge-request-recent-searches', merge_requests: 'merge-request-recent-searches',
group_members: 'group-members-recent-searches', group_members: 'group-members-recent-searches',
group_invited_members: 'group-invited-members-recent-searches', group_invited_members: 'group-invited-members-recent-searches',
project_members: 'project-members-recent-searches',
project_group_links: 'project-group-links-recent-searches',
}; };
...@@ -6,6 +6,8 @@ import groupsSelect from '~/groups_select'; ...@@ -6,6 +6,8 @@ import groupsSelect from '~/groups_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { __ } from '~/locale';
import { deprecatedCreateFlash as flash } from '~/flash';
function mountRemoveMemberModal() { function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal'); const el = document.querySelector('.js-remove-member-modal');
...@@ -32,3 +34,65 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -32,3 +34,65 @@ document.addEventListener('DOMContentLoaded', () => {
new Members(); // eslint-disable-line no-new new Members(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new
}); });
if (window.gon.features.vueProjectMembersList) {
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
Promise.all([
import('~/members/index'),
import('~/members/utils'),
import('~/projects/members/utils'),
import('~/locale'),
])
.then(
([
{ initMembersApp },
{ groupLinkRequestFormatter },
{ projectMemberRequestFormatter },
{ s__ },
]) => {
initMembersApp(document.querySelector('.js-project-members-list'), {
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
requestFormatter: projectMemberRequestFormatter,
filteredSearchBar: {
show: true,
tokens: ['with_inherited_permissions'],
searchParam: 'search',
placeholder: s__('Members|Filter members'),
recentSearchesStorageKey: 'project_members',
},
});
initMembersApp(document.querySelector('.js-project-group-links-list'), {
tableFields: SHARED_FIELDS.concat('granted'),
tableAttrs: {
table: { 'data-qa-selector': 'groups_list' },
tr: { 'data-qa-selector': 'group_row' },
},
requestFormatter: groupLinkRequestFormatter,
filteredSearchBar: {
show: true,
tokens: [],
searchParam: 'search_groups',
placeholder: s__('Members|Search groups'),
recentSearchesStorageKey: 'project_group_links',
},
});
initMembersApp(document.querySelector('.js-project-invited-members-list'), {
tableFields: SHARED_FIELDS.concat('invited'),
requestFormatter: projectMemberRequestFormatter,
});
initMembersApp(document.querySelector('.js-project-access-requests-list'), {
tableFields: SHARED_FIELDS.concat('requested'),
requestFormatter: projectMemberRequestFormatter,
});
},
)
.catch(() => {
flash(__('An error occurred while loading the members, please try again.'));
});
}
export const PROJECT_MEMBER_BASE_PROPERTY_NAME = 'project_member';
import { baseRequestFormatter } from '~/members/utils';
import { PROJECT_MEMBER_BASE_PROPERTY_NAME } from './constants';
import { MEMBER_ACCESS_LEVEL_PROPERTY_NAME } from '~/members/constants';
export const projectMemberRequestFormatter = baseRequestFormatter(
PROJECT_MEMBER_BASE_PROPERTY_NAME,
MEMBER_ACCESS_LEVEL_PROPERTY_NAME,
);
...@@ -8,6 +8,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -8,6 +8,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController
# Authorize # Authorize
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access] before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
before_action do
push_frontend_feature_flag(:vue_project_members_list, @project)
end
feature_category :authentication_and_authorization feature_category :authentication_and_authorization
def index def index
......
...@@ -20,7 +20,6 @@ module Ci ...@@ -20,7 +20,6 @@ module Ci
belongs_to :runner belongs_to :runner
belongs_to :trigger_request belongs_to :trigger_request
belongs_to :erased_by, class_name: 'User' belongs_to :erased_by, class_name: 'User'
belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :builds
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
RUNNER_FEATURES = { RUNNER_FEATURES = {
...@@ -38,7 +37,6 @@ module Ci ...@@ -38,7 +37,6 @@ module Ci
DEGRADATION_THRESHOLD_VARIABLE_NAME = 'DEGRADATION_THRESHOLD' DEGRADATION_THRESHOLD_VARIABLE_NAME = 'DEGRADATION_THRESHOLD'
has_one :deployment, as: :deployable, class_name: 'Deployment' has_one :deployment, as: :deployable, class_name: 'Deployment'
has_one :resource, class_name: 'Ci::Resource', inverse_of: :build
has_one :pending_state, class_name: 'Ci::BuildPendingState', inverse_of: :build has_one :pending_state, class_name: 'Ci::BuildPendingState', inverse_of: :build
has_many :trace_sections, class_name: 'Ci::BuildTraceSection' has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id, inverse_of: :build has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id, inverse_of: :build
...@@ -236,21 +234,14 @@ module Ci ...@@ -236,21 +234,14 @@ module Ci
state_machine :status do state_machine :status do
event :enqueue do event :enqueue do
transition [:created, :skipped, :manual, :scheduled] => :waiting_for_resource, if: :requires_resource?
transition [:created, :skipped, :manual, :scheduled] => :preparing, if: :any_unmet_prerequisites? transition [:created, :skipped, :manual, :scheduled] => :preparing, if: :any_unmet_prerequisites?
end end
event :enqueue_scheduled do event :enqueue_scheduled do
transition scheduled: :waiting_for_resource, if: :requires_resource?
transition scheduled: :preparing, if: :any_unmet_prerequisites? transition scheduled: :preparing, if: :any_unmet_prerequisites?
transition scheduled: :pending transition scheduled: :pending
end end
event :enqueue_waiting_for_resource do
transition waiting_for_resource: :preparing, if: :any_unmet_prerequisites?
transition waiting_for_resource: :pending
end
event :enqueue_preparing do event :enqueue_preparing do
transition preparing: :pending transition preparing: :pending
end end
...@@ -279,23 +270,6 @@ module Ci ...@@ -279,23 +270,6 @@ module Ci
build.scheduled_at = build.options_scheduled_at build.scheduled_at = build.options_scheduled_at
end end
before_transition any => :waiting_for_resource do |build|
build.waiting_for_resource_at = Time.current
end
before_transition on: :enqueue_waiting_for_resource do |build|
next unless build.requires_resource?
build.resource_group.assign_resource_to(build) # If false is returned, it stops the transition
end
after_transition any => :waiting_for_resource do |build|
build.run_after_commit do
Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
.perform_async(build.resource_group_id)
end
end
before_transition on: :enqueue_preparing do |build| before_transition on: :enqueue_preparing do |build|
!build.any_unmet_prerequisites? # If false is returned, it stops the transition !build.any_unmet_prerequisites? # If false is returned, it stops the transition
end end
...@@ -328,16 +302,6 @@ module Ci ...@@ -328,16 +302,6 @@ module Ci
end end
end end
after_transition any => ::Ci::Build.completed_statuses do |build|
next unless build.resource_group_id.present?
next unless build.resource_group.release_resource_from(build)
build.run_after_commit do
Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
.perform_async(build.resource_group_id)
end
end
after_transition any => [:success, :failed, :canceled] do |build| after_transition any => [:success, :failed, :canceled] do |build|
build.run_after_commit do build.run_after_commit do
build.run_status_commit_hooks! build.run_status_commit_hooks!
...@@ -467,6 +431,11 @@ module Ci ...@@ -467,6 +431,11 @@ module Ci
pipeline.builds.retried.where(name: self.name).count pipeline.builds.retried.where(name: self.name).count
end end
override :all_met_to_become_pending?
def all_met_to_become_pending?
super && !any_unmet_prerequisites?
end
def any_unmet_prerequisites? def any_unmet_prerequisites?
prerequisites.present? prerequisites.present?
end end
...@@ -501,10 +470,6 @@ module Ci ...@@ -501,10 +470,6 @@ module Ci
end end
end end
def requires_resource?
self.resource_group_id.present?
end
def has_environment? def has_environment?
environment.present? environment.present?
end end
......
...@@ -3,6 +3,11 @@ ...@@ -3,6 +3,11 @@
module Ci module Ci
class Processable < ::CommitStatus class Processable < ::CommitStatus
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
extend ::Gitlab::Utils::Override
has_one :resource, class_name: 'Ci::Resource', foreign_key: 'build_id', inverse_of: :processable
belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :processables
accepts_nested_attributes_for :needs accepts_nested_attributes_for :needs
...@@ -20,6 +25,48 @@ module Ci ...@@ -20,6 +25,48 @@ module Ci
where('NOT EXISTS (?)', needs) where('NOT EXISTS (?)', needs)
end end
state_machine :status do
event :enqueue do
transition [:created, :skipped, :manual, :scheduled] => :waiting_for_resource, if: :with_resource_group?
end
event :enqueue_scheduled do
transition scheduled: :waiting_for_resource, if: :with_resource_group?
end
event :enqueue_waiting_for_resource do
transition waiting_for_resource: :preparing, if: :any_unmet_prerequisites?
transition waiting_for_resource: :pending
end
before_transition any => :waiting_for_resource do |processable|
processable.waiting_for_resource_at = Time.current
end
before_transition on: :enqueue_waiting_for_resource do |processable|
next unless processable.with_resource_group?
processable.resource_group.assign_resource_to(processable)
end
after_transition any => :waiting_for_resource do |processable|
processable.run_after_commit do
Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
.perform_async(processable.resource_group_id)
end
end
after_transition any => ::Ci::Processable.completed_statuses do |processable|
next unless processable.with_resource_group?
next unless processable.resource_group.release_resource_from(processable)
processable.run_after_commit do
Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
.perform_async(processable.resource_group_id)
end
end
end
def self.select_with_aggregated_needs(project) def self.select_with_aggregated_needs(project)
aggregated_needs_names = Ci::BuildNeed aggregated_needs_names = Ci::BuildNeed
.scoped_build .scoped_build
...@@ -77,6 +124,15 @@ module Ci ...@@ -77,6 +124,15 @@ module Ci
raise NotImplementedError raise NotImplementedError
end end
override :all_met_to_become_pending?
def all_met_to_become_pending?
super && !with_resource_group?
end
def with_resource_group?
self.resource_group_id.present?
end
# Overriding scheduling_type enum's method for nil `scheduling_type`s # Overriding scheduling_type enum's method for nil `scheduling_type`s
def scheduling_type_dag? def scheduling_type_dag?
scheduling_type.nil? ? find_legacy_scheduling_type == :dag : super scheduling_type.nil? ? find_legacy_scheduling_type == :dag : super
......
...@@ -5,9 +5,9 @@ module Ci ...@@ -5,9 +5,9 @@ module Ci
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :resources belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :resources
belongs_to :build, class_name: 'Ci::Build', inverse_of: :resource belongs_to :processable, class_name: 'Ci::Processable', foreign_key: 'build_id', inverse_of: :resource
scope :free, -> { where(build: nil) } scope :free, -> { where(processable: nil) }
scope :retained_by, -> (build) { where(build: build) } scope :retained_by, -> (processable) { where(processable: processable) }
end end
end end
...@@ -7,7 +7,7 @@ module Ci ...@@ -7,7 +7,7 @@ module Ci
belongs_to :project, inverse_of: :resource_groups belongs_to :project, inverse_of: :resource_groups
has_many :resources, class_name: 'Ci::Resource', inverse_of: :resource_group has_many :resources, class_name: 'Ci::Resource', inverse_of: :resource_group
has_many :builds, class_name: 'Ci::Build', inverse_of: :resource_group has_many :processables, class_name: 'Ci::Processable', inverse_of: :resource_group
validates :key, validates :key,
length: { maximum: 255 }, length: { maximum: 255 },
...@@ -19,12 +19,12 @@ module Ci ...@@ -19,12 +19,12 @@ module Ci
## ##
# NOTE: This is concurrency-safe method that the subquery in the `UPDATE` # NOTE: This is concurrency-safe method that the subquery in the `UPDATE`
# works as explicit locking. # works as explicit locking.
def assign_resource_to(build) def assign_resource_to(processable)
resources.free.limit(1).update_all(build_id: build.id) > 0 resources.free.limit(1).update_all(build_id: processable.id) > 0
end end
def release_resource_from(build) def release_resource_from(processable)
resources.retained_by(build).update_all(build_id: nil) > 0 resources.retained_by(processable).update_all(build_id: nil) > 0
end end
private private
......
...@@ -255,15 +255,7 @@ class CommitStatus < ApplicationRecord ...@@ -255,15 +255,7 @@ class CommitStatus < ApplicationRecord
end end
def all_met_to_become_pending? def all_met_to_become_pending?
!any_unmet_prerequisites? && !requires_resource? true
end
def any_unmet_prerequisites?
false
end
def requires_resource?
false
end end
def auto_canceled? def auto_canceled?
......
# frozen_string_literal: true
class ReadmeBlob < SimpleDelegator
include BlobActiveModel
attr_reader :repository
def initialize(blob, repository)
@repository = repository
super(blob)
end
def rendered_markup
repository.rendered_readme
end
end
...@@ -39,7 +39,7 @@ class Repository ...@@ -39,7 +39,7 @@ class Repository
# #
# For example, for entry `:commit_count` there's a method called `commit_count` which # For example, for entry `:commit_count` there's a method called `commit_count` which
# stores its data in the `commit_count` cache key. # stores its data in the `commit_count` cache key.
CACHED_METHODS = %i(size commit_count rendered_readme readme_path contribution_guide CACHED_METHODS = %i(size commit_count readme_path contribution_guide
changelog license_blob license_key gitignore changelog license_blob license_key gitignore
gitlab_ci_yml branch_names tag_names branch_count gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? root_ref merged_branch_names tag_count avatar exists? root_ref merged_branch_names
...@@ -53,7 +53,7 @@ class Repository ...@@ -53,7 +53,7 @@ class Repository
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
# the corresponding methods to call for refreshing caches. # the corresponding methods to call for refreshing caches.
METHOD_CACHES_FOR_FILE_TYPES = { METHOD_CACHES_FOR_FILE_TYPES = {
readme: %i(rendered_readme readme_path), readme: %i(readme_path),
changelog: :changelog, changelog: :changelog,
license: %i(license_blob license_key license), license: %i(license_blob license_key license),
contributing: :contribution_guide, contributing: :contribution_guide,
...@@ -498,23 +498,7 @@ class Repository ...@@ -498,23 +498,7 @@ class Repository
end end
def blob_at(sha, path) def blob_at(sha, path)
blob = Blob.decorate(raw_repository.blob_at(sha, path), container) Blob.decorate(raw_repository.blob_at(sha, path), container)
# Don't attempt to return a special result if there is no blob at all
return unless blob
# Don't attempt to return a special result if this can't be a README
return blob unless Gitlab::FileDetector.type_of(blob.name) == :readme
# Don't attempt to return a special result unless we're looking at HEAD
return blob unless head_commit&.sha == sha
case path
when head_tree&.readme_path
ReadmeBlob.new(blob, self)
else
blob
end
rescue Gitlab::Git::Repository::NoRepository rescue Gitlab::Git::Repository::NoRepository
nil nil
end end
...@@ -612,15 +596,6 @@ class Repository ...@@ -612,15 +596,6 @@ class Repository
end end
cache_method :readme_path cache_method :readme_path
def rendered_readme
return unless readme
context = { project: project }
MarkupHelper.markup_unsafe(readme.name, readme.data, context)
end
cache_method :rendered_readme
def contribution_guide def contribution_guide
file_on_head(:contributing) file_on_head(:contributing)
end end
......
...@@ -7,8 +7,8 @@ module Ci ...@@ -7,8 +7,8 @@ module Ci
def execute(resource_group) def execute(resource_group)
free_resources = resource_group.resources.free.count free_resources = resource_group.resources.free.count
resource_group.builds.waiting_for_resource.take(free_resources).each do |build| resource_group.processables.waiting_for_resource.take(free_resources).each do |processable|
build.enqueue_waiting_for_resource processable.enqueue_waiting_for_resource
end end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
......
- page_title _("Members") - page_title _("Members")
- group = @project.group - group = @project.group
- vue_project_members_list_enabled = Feature.enabled?(:vue_project_members_list, @project)
.js-remove-member-modal .js-remove-member-modal
.row.gl-mt-3 .row.gl-mt-3
...@@ -74,24 +75,44 @@ ...@@ -74,24 +75,44 @@
%span.badge.badge-pill= @requesters.count %span.badge.badge-pill= @requesters.count
.tab-content .tab-content
#tab-members.tab-pane{ class: ('active' unless groups_tab_active?) } #tab-members.tab-pane{ class: ('active' unless groups_tab_active?) }
= render 'projects/project_members/team', project: @project, group: group, members: @project_members, current_user_is_group_owner: current_user_is_group_owner?(@project) - if vue_project_members_list_enabled
.js-project-members-list{ data: project_members_list_data_attributes(@project, @project_members) }
.loading
.spinner.spinner-md
- else
= render 'projects/project_members/team', project: @project, group: group, members: @project_members, current_user_is_group_owner: current_user_is_group_owner?(@project)
= paginate @project_members, theme: "gitlab", params: { search_groups: nil } = paginate @project_members, theme: "gitlab", params: { search_groups: nil }
- if show_groups?(@group_links) - if show_groups?(@group_links)
#tab-groups.tab-pane{ class: ('active' if groups_tab_active?) } #tab-groups.tab-pane{ class: ('active' if groups_tab_active?) }
= render 'projects/project_members/groups', group_links: @group_links - if vue_project_members_list_enabled
.js-project-group-links-list{ data: project_group_links_list_data_attributes(@project, @group_links) }
.loading
.spinner.spinner-md
- else
= render 'projects/project_members/groups', group_links: @group_links
- if show_invited_members?(@project, @invited_members) - if show_invited_members?(@project, @invited_members)
#tab-invited-members.tab-pane #tab-invited-members.tab-pane
.card.card-without-border - if vue_project_members_list_enabled
= render 'shared/members/tab_pane/header' do .js-project-invited-members-list{ data: project_members_list_data_attributes(@project, @invited_members) }
= render 'shared/members/tab_pane/title' do .loading
= html_escape(_('Members invited to %{strong_start}%{project_name}%{strong_end}')) % { project_name: @project.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } .spinner.spinner-md
%ul.content-list.members-list - else
= render partial: 'shared/members/member', collection: @invited_members, as: :member, locals: { membership_source: @project, group: group, current_user_is_group_owner: current_user_is_group_owner?(@project) } .card.card-without-border
= render 'shared/members/tab_pane/header' do
= render 'shared/members/tab_pane/title' do
= html_escape(_('Members invited to %{strong_start}%{project_name}%{strong_end}')) % { project_name: @project.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: @invited_members, as: :member, locals: { membership_source: @project, group: group, current_user_is_group_owner: current_user_is_group_owner?(@project) }
- if show_access_requests?(@project, @requesters) - if show_access_requests?(@project, @requesters)
#tab-access-requests.tab-pane #tab-access-requests.tab-pane
.card.card-without-border - if vue_project_members_list_enabled
= render 'shared/members/tab_pane/header' do .js-project-access-requests-list{ data: project_members_list_data_attributes(@project, @requesters) }
= render 'shared/members/tab_pane/title' do .loading
= html_escape(_('Users requesting access to %{strong_start}%{project_name}%{strong_end}')) % { project_name: @project.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } .spinner.spinner-md
%ul.content-list.members-list - else
= render partial: 'shared/members/member', collection: @requesters, as: :member, locals: { membership_source: @project, group: group } .card.card-without-border
= render 'shared/members/tab_pane/header' do
= render 'shared/members/tab_pane/title' do
= html_escape(_('Users requesting access to %{strong_start}%{project_name}%{strong_end}')) % { project_name: @project.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: @requesters, as: :member, locals: { membership_source: @project, group: group }
---
title: Improve search filter by taking space in file path into account
merge_request: 52392
author:
type: fixed
---
name: vue_project_members_list
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52148
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299954
milestone: '13.9'
type: development
group: group::access
default_enabled: false
...@@ -284,6 +284,7 @@ Example response: ...@@ -284,6 +284,7 @@ Example response:
"state": "active", "state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon", "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root", "web_url": "http://192.168.1.8:3000/root",
"last_activity_on": "2021-01-27"
}, },
{ {
"id": 2, "id": 2,
...@@ -292,7 +293,8 @@ Example response: ...@@ -292,7 +293,8 @@ Example response:
"state": "active", "state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon", "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root", "web_url": "http://192.168.1.8:3000/root",
"email": "john@example.com" "email": "john@example.com",
"last_activity_on": "2021-01-25"
}, },
{ {
"id": 3, "id": 3,
...@@ -300,7 +302,8 @@ Example response: ...@@ -300,7 +302,8 @@ Example response:
"name": "Foo bar", "name": "Foo bar",
"state": "active", "state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon", "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root" "web_url": "http://192.168.1.8:3000/root",
"last_activity_on": "2021-01-20"
} }
] ]
``` ```
......
---
title: Add last_activity_on attribute to Billable Members API
merge_request: 52666
author:
type: changed
...@@ -5,6 +5,7 @@ module EE ...@@ -5,6 +5,7 @@ module EE
module Entities module Entities
class BillableMember < ::API::Entities::UserBasic class BillableMember < ::API::Entities::UserBasic
expose :public_email, as: :email expose :public_email, as: :email
expose :last_activity_on
end end
end end
end end
......
...@@ -18,6 +18,7 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev ...@@ -18,6 +18,7 @@ RSpec.describe 'Groups > Members > Maintainer/Owner can override LDAP access lev
let!(:regular_member) { create(:group_member, :guest, group: group, user: maryjane, ldap: false) } let!(:regular_member) { create(:group_member, :guest, group: group, user: maryjane, ldap: false) }
before do before do
stub_feature_flags(vue_project_members_list: false)
# We need to actually activate the LDAP config otherwise `Group#ldap_synced?` will always be false! # We need to actually activate the LDAP config otherwise `Group#ldap_synced?` will always be false!
allow(Gitlab.config.ldap).to receive_messages(enabled: true) allow(Gitlab.config.ldap).to receive_messages(enabled: true)
......
...@@ -8,6 +8,8 @@ RSpec.describe 'Projects > Audit Events', :js do ...@@ -8,6 +8,8 @@ RSpec.describe 'Projects > Audit Events', :js do
let(:project) { create(:project, :repository, namespace: user.namespace) } let(:project) { create(:project, :repository, namespace: user.namespace) }
before do before do
stub_feature_flags(vue_project_members_list: false)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)
end end
......
...@@ -10,6 +10,7 @@ RSpec.describe 'Project > Members > Invite group and members', :js do ...@@ -10,6 +10,7 @@ RSpec.describe 'Project > Members > Invite group and members', :js do
before do before do
stub_feature_flags(invite_members_group_modal: false) stub_feature_flags(invite_members_group_modal: false)
stub_feature_flags(vue_project_members_list: false)
end end
describe 'Share group lock' do describe 'Share group lock' do
......
...@@ -8,6 +8,8 @@ RSpec.describe 'Projects > Members > Member is removed from project' do ...@@ -8,6 +8,8 @@ RSpec.describe 'Projects > Members > Member is removed from project' do
let(:other_user) { create(:user) } let(:other_user) { create(:user) }
before do before do
stub_feature_flags(vue_project_members_list: false)
project.add_maintainer(user) project.add_maintainer(user)
project.add_maintainer(other_user) project.add_maintainer(other_user)
sign_in(user) sign_in(user)
......
...@@ -3,10 +3,16 @@ ...@@ -3,10 +3,16 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ::EE::API::Entities::BillableMember do RSpec.describe ::EE::API::Entities::BillableMember do
let(:member) { build(:user, public_email: public_email, email: 'private@email.com') } let(:last_activity_on) { Date.today }
let(:public_email) { nil }
let(:member) { build(:user, public_email: public_email, email: 'private@email.com', last_activity_on: last_activity_on) }
subject(:entity_representation) { described_class.new(member).as_json } subject(:entity_representation) { described_class.new(member).as_json }
it 'returns the last_activity_on attribute' do
expect(entity_representation[:last_activity_on]).to eq last_activity_on
end
context 'when the user has a public_email assigned' do context 'when the user has a public_email assigned' do
let(:public_email) { 'public@email.com' } let(:public_email) { 'public@email.com' }
......
...@@ -14,7 +14,7 @@ module Gitlab ...@@ -14,7 +14,7 @@ module Gitlab
ALLOWED_KEYS = %i[tags script type image services start_in artifacts ALLOWED_KEYS = %i[tags script type image services start_in artifacts
cache dependencies before_script after_script cache dependencies before_script after_script
environment coverage retry parallel interruptible timeout environment coverage retry parallel interruptible timeout
resource_group release secrets].freeze release secrets].freeze
REQUIRED_BY_NEEDS = %i[stage].freeze REQUIRED_BY_NEEDS = %i[stage].freeze
...@@ -30,7 +30,6 @@ module Gitlab ...@@ -30,7 +30,6 @@ module Gitlab
} }
validates :dependencies, array_of_strings: true validates :dependencies, array_of_strings: true
validates :resource_group, type: String
validates :allow_failure, hash_or_boolean: true validates :allow_failure, hash_or_boolean: true
end end
...@@ -124,7 +123,7 @@ module Gitlab ...@@ -124,7 +123,7 @@ module Gitlab
attributes :script, :tags, :when, :dependencies, attributes :script, :tags, :when, :dependencies,
:needs, :retry, :parallel, :start_in, :needs, :retry, :parallel, :start_in,
:interruptible, :timeout, :resource_group, :interruptible, :timeout,
:release, :allow_failure :release, :allow_failure
def self.matching?(name, config) def self.matching?(name, config)
...@@ -174,7 +173,6 @@ module Gitlab ...@@ -174,7 +173,6 @@ module Gitlab
ignore: ignored?, ignore: ignored?,
allow_failure_criteria: allow_failure_criteria, allow_failure_criteria: allow_failure_criteria,
needs: needs_defined? ? needs_value : nil, needs: needs_defined? ? needs_value : nil,
resource_group: resource_group,
scheduling_type: needs_defined? ? :dag : :stage scheduling_type: needs_defined? ? :dag : :stage
).compact ).compact
end end
......
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Inheritable include ::Gitlab::Config::Entry::Inheritable
PROCESSABLE_ALLOWED_KEYS = %i[extends stage only except rules variables PROCESSABLE_ALLOWED_KEYS = %i[extends stage only except rules variables
inherit allow_failure when needs].freeze inherit allow_failure when needs resource_group].freeze
included do included do
validations do validations do
...@@ -32,6 +32,7 @@ module Gitlab ...@@ -32,6 +32,7 @@ module Gitlab
with_options allow_nil: true do with_options allow_nil: true do
validates :extends, array_of_strings_or_string: true validates :extends, array_of_strings_or_string: true
validates :rules, array_of_hashes: true validates :rules, array_of_hashes: true
validates :resource_group, type: String
end end
end end
...@@ -64,7 +65,7 @@ module Gitlab ...@@ -64,7 +65,7 @@ module Gitlab
inherit: false, inherit: false,
default: {} default: {}
attributes :extends, :rules attributes :extends, :rules, :resource_group
end end
def compose!(deps = nil) def compose!(deps = nil)
...@@ -125,7 +126,8 @@ module Gitlab ...@@ -125,7 +126,8 @@ module Gitlab
rules: rules_value, rules: rules_value,
variables: root_and_job_variables_value, variables: root_and_job_variables_value,
only: only_value, only: only_value,
except: except_value }.compact except: except_value,
resource_group: resource_group }.compact
end end
def root_and_job_variables_value def root_and_job_variables_value
......
...@@ -8,17 +8,17 @@ module Gitlab ...@@ -8,17 +8,17 @@ module Gitlab
class ResourceGroup < Seed::Base class ResourceGroup < Seed::Base
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
attr_reader :build, :resource_group_key attr_reader :processable, :resource_group_key
def initialize(build, resource_group_key) def initialize(processable, resource_group_key)
@build = build @processable = processable
@resource_group_key = resource_group_key @resource_group_key = resource_group_key
end end
def to_resource def to_resource
return unless resource_group_key.present? return unless resource_group_key.present?
resource_group = build.project.resource_groups resource_group = processable.project.resource_groups
.safe_find_or_create_by(key: expanded_resource_group_key) .safe_find_or_create_by(key: expanded_resource_group_key)
resource_group if resource_group.persisted? resource_group if resource_group.persisted?
...@@ -28,7 +28,7 @@ module Gitlab ...@@ -28,7 +28,7 @@ module Gitlab
def expanded_resource_group_key def expanded_resource_group_key
strong_memoize(:expanded_resource_group_key) do strong_memoize(:expanded_resource_group_key) do
ExpandVariables.expand(resource_group_key, -> { build.simple_variables }) ExpandVariables.expand(resource_group_key, -> { processable.simple_variables })
end end
end end
end end
......
...@@ -5,6 +5,9 @@ module Gitlab ...@@ -5,6 +5,9 @@ module Gitlab
class Query < SimpleDelegator class Query < SimpleDelegator
include EncodingHelper include EncodingHelper
QUOTES_REGEXP = %r{\A"|"\Z}.freeze
TOKEN_WITH_QUOTES_REGEXP = %r{\s(?=(?:[^"]|"[^"]*")*$)}.freeze
def initialize(query, filter_opts = {}, &block) def initialize(query, filter_opts = {}, &block)
@raw_query = query.dup @raw_query = query.dup
@filters = [] @filters = []
...@@ -35,22 +38,24 @@ module Gitlab ...@@ -35,22 +38,24 @@ module Gitlab
def extract_filters def extract_filters
fragments = [] fragments = []
query_tokens = parse_raw_query
filters = @filters.each_with_object([]) do |filter, parsed_filters| filters = @filters.each_with_object([]) do |filter, parsed_filters|
match = @raw_query.split.find { |part| part =~ /\A-?#{filter[:name]}:/ } match = query_tokens.find { |part| part =~ /\A-?#{filter[:name]}:/ }
next unless match next unless match
input = match.split(':')[1..-1].join input = match.split(':')[1..-1].join
next if input.empty? next if input.empty?
filter[:negated] = match.start_with?("-") filter[:negated] = match.start_with?("-")
filter[:value] = parse_filter(filter, input) filter[:value] = parse_filter(filter, input.gsub(QUOTES_REGEXP, ''))
filter[:regex_value] = Regexp.escape(filter[:value]).gsub('\*', '.*?') filter[:regex_value] = Regexp.escape(filter[:value]).gsub('\*', '.*?')
fragments << match fragments << match
parsed_filters << filter parsed_filters << filter
end end
query = (@raw_query.split - fragments).join(' ') query = (query_tokens - fragments).join(' ')
query = '*' if query.empty? query = '*' if query.empty?
[query, filters] [query, filters]
...@@ -61,6 +66,13 @@ module Gitlab ...@@ -61,6 +66,13 @@ module Gitlab
@filter_options[:encode_binary] ? encode_binary(result) : result @filter_options[:encode_binary] ? encode_binary(result) : result
end end
def parse_raw_query
# Positive lookahead for any non-quote char or even number of quotes
# for example '"search term" path:"foo bar.txt"' would break into
# ["search term", "path:\"foo bar.txt\""]
@raw_query.split(TOKEN_WITH_QUOTES_REGEXP).reject(&:empty?)
end
end end
end end
end end
...@@ -3281,6 +3281,9 @@ msgstr "" ...@@ -3281,6 +3281,9 @@ msgstr ""
msgid "An error occurred while loading the file. Please try again later." msgid "An error occurred while loading the file. Please try again later."
msgstr "" msgstr ""
msgid "An error occurred while loading the members, please try again."
msgstr ""
msgid "An error occurred while loading the merge request changes." msgid "An error occurred while loading the merge request changes."
msgstr "" msgstr ""
...@@ -18101,6 +18104,9 @@ msgstr "" ...@@ -18101,6 +18104,9 @@ msgstr ""
msgid "Members|Role updated successfully." msgid "Members|Role updated successfully."
msgstr "" msgstr ""
msgid "Members|Search groups"
msgstr ""
msgid "Members|Search invited" msgid "Members|Search invited"
msgstr "" msgstr ""
......
...@@ -5,7 +5,7 @@ FactoryBot.define do ...@@ -5,7 +5,7 @@ FactoryBot.define do
resource_group factory: :ci_resource_group resource_group factory: :ci_resource_group
trait(:retained) do trait(:retained) do
build factory: :ci_build processable factory: :ci_build
end end
end end
end end
...@@ -10,6 +10,8 @@ RSpec.describe "Admin::Projects" do ...@@ -10,6 +10,8 @@ RSpec.describe "Admin::Projects" do
let(:current_user) { create(:admin) } let(:current_user) { create(:admin) }
before do before do
stub_feature_flags(vue_project_members_list: false)
sign_in(current_user) sign_in(current_user)
gitlab_enable_admin_mode_sign_in(current_user) gitlab_enable_admin_mode_sign_in(current_user)
end end
......
...@@ -8,6 +8,8 @@ RSpec.describe 'Projects > Members > Anonymous user sees members' do ...@@ -8,6 +8,8 @@ RSpec.describe 'Projects > Members > Anonymous user sees members' do
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
before do before do
stub_feature_flags(vue_project_members_list: false)
project.add_maintainer(user) project.add_maintainer(user)
create(:project_group_link, project: project, group: group) create(:project_group_link, project: project, group: group)
end end
......
...@@ -13,6 +13,8 @@ RSpec.describe 'Projects members', :js do ...@@ -13,6 +13,8 @@ RSpec.describe 'Projects members', :js do
let(:group_requester) { create(:user) } let(:group_requester) { create(:user) }
before do before do
stub_feature_flags(vue_project_members_list: false)
project.add_developer(developer) project.add_developer(developer)
group.add_owner(user) group.add_owner(user)
sign_in(user) sign_in(user)
......
...@@ -11,6 +11,8 @@ RSpec.describe 'Projects > Members > Groups with access list', :js do ...@@ -11,6 +11,8 @@ RSpec.describe 'Projects > Members > Groups with access list', :js do
let!(:group_link) { create(:project_group_link, project: project, group: group, **additional_link_attrs) } let!(:group_link) { create(:project_group_link, project: project, group: group, **additional_link_attrs) }
before do before do
stub_feature_flags(vue_project_members_list: false)
travel_to Time.now.utc.beginning_of_day travel_to Time.now.utc.beginning_of_day
project.add_maintainer(user) project.add_maintainer(user)
......
...@@ -10,6 +10,7 @@ RSpec.describe 'Project > Members > Invite group', :js do ...@@ -10,6 +10,7 @@ RSpec.describe 'Project > Members > Invite group', :js do
before do before do
stub_feature_flags(invite_members_group_modal: false) stub_feature_flags(invite_members_group_modal: false)
stub_feature_flags(vue_project_members_list: false)
end end
describe 'Share with group lock' do describe 'Share with group lock' do
......
...@@ -13,10 +13,18 @@ RSpec.describe 'Project members list' do ...@@ -13,10 +13,18 @@ RSpec.describe 'Project members list' do
before do before do
stub_feature_flags(invite_members_group_modal: false) stub_feature_flags(invite_members_group_modal: false)
stub_feature_flags(vue_project_members_list: false)
sign_in(user1) sign_in(user1)
group.add_owner(user1) group.add_owner(user1)
end end
it 'pushes `vue_project_members_list` feature flag to the frontend' do
visit_members_page
expect(page).to have_pushed_frontend_feature_flags(vueProjectMembersList: false)
end
it 'show members from project and group' do it 'show members from project and group' do
project.add_developer(user2) project.add_developer(user2)
......
...@@ -11,6 +11,8 @@ RSpec.describe 'Projects > Members > Maintainer adds member with expiration date ...@@ -11,6 +11,8 @@ RSpec.describe 'Projects > Members > Maintainer adds member with expiration date
let(:new_member) { create(:user) } let(:new_member) { create(:user) }
before do before do
stub_feature_flags(vue_project_members_list: false)
travel_to Time.now.utc.beginning_of_day travel_to Time.now.utc.beginning_of_day
project.add_maintainer(maintainer) project.add_maintainer(maintainer)
......
...@@ -8,6 +8,8 @@ RSpec.describe 'Projects > Members > Sorting' do ...@@ -8,6 +8,8 @@ RSpec.describe 'Projects > Members > Sorting' do
let(:project) { create(:project, namespace: maintainer.namespace, creator: maintainer) } let(:project) { create(:project, namespace: maintainer.namespace, creator: maintainer) }
before do before do
stub_feature_flags(vue_project_members_list: false)
create(:project_member, :developer, user: developer, project: project, created_at: 3.days.ago) create(:project_member, :developer, user: developer, project: project, created_at: 3.days.ago)
sign_in(maintainer) sign_in(maintainer)
......
...@@ -20,6 +20,7 @@ RSpec.describe 'Projects > Members > Tabs' do ...@@ -20,6 +20,7 @@ RSpec.describe 'Projects > Members > Tabs' do
end end
before do before do
stub_feature_flags(vue_project_members_list: false)
allow(Kaminari.config).to receive(:default_per_page).and_return(1) allow(Kaminari.config).to receive(:default_per_page).and_return(1)
sign_in(user) sign_in(user)
......
...@@ -11,6 +11,8 @@ RSpec.describe 'Projects > Settings > User manages project members' do ...@@ -11,6 +11,8 @@ RSpec.describe 'Projects > Settings > User manages project members' do
let(:user_mike) { create(:user, name: 'Mike') } let(:user_mike) { create(:user, name: 'Mike') }
before do before do
stub_feature_flags(vue_project_members_list: false)
project.add_maintainer(user) project.add_maintainer(user)
project.add_developer(user_dmitriy) project.add_developer(user_dmitriy)
sign_in(user) sign_in(user)
......
import { projectMemberRequestFormatter } from '~/projects/members/utils';
describe('project member utils', () => {
describe('projectMemberRequestFormatter', () => {
it('returns expected format', () => {
expect(
projectMemberRequestFormatter({
accessLevel: 50,
expires_at: '2020-10-16',
}),
).toEqual({ project_member: { access_level: 50, expires_at: '2020-10-16' } });
});
});
});
...@@ -73,6 +73,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do ...@@ -73,6 +73,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
end end
end end
context 'when resource_group key is not a string' do
let(:config) { { resource_group: 123 } }
it 'returns error about wrong value type' do
expect(entry).not_to be_valid
expect(entry.errors).to include "job resource group should be a string"
end
end
context 'when it uses both "when:" and "rules:"' do context 'when it uses both "when:" and "rules:"' do
let(:config) do let(:config) do
{ {
...@@ -340,6 +349,26 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do ...@@ -340,6 +349,26 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
end end
end end
context 'with resource group' do
using RSpec::Parameterized::TableSyntax
where(:resource_group, :result) do
'iOS' | 'iOS'
'review/$CI_COMMIT_REF_NAME' | 'review/$CI_COMMIT_REF_NAME'
nil | nil
end
with_them do
let(:config) { { script: 'ls', resource_group: resource_group }.compact }
it do
entry.compose!(deps)
expect(entry.resource_group).to eq(result)
end
end
end
context 'with inheritance' do context 'with inheritance' do
context 'of variables' do context 'of variables' do
let(:config) do let(:config) do
......
...@@ -53,6 +53,14 @@ RSpec.describe Gitlab::FileFinder do ...@@ -53,6 +53,14 @@ RSpec.describe Gitlab::FileFinder do
end end
end end
context 'with white space in the path' do
it 'filters by path correctly' do
results = subject.find('directory path:"with space/README.md"')
expect(results.count).to eq(1)
end
end
it 'does not cause N+1 query' do it 'does not cause N+1 query' do
expect(Gitlab::GitalyClient).to receive(:call).at_most(10).times.and_call_original expect(Gitlab::GitalyClient).to receive(:call).at_most(10).times.and_call_original
......
...@@ -292,12 +292,11 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do ...@@ -292,12 +292,11 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do
describe '#expire_method_caches' do describe '#expire_method_caches' do
it 'expires the caches of the given methods' do it 'expires the caches of the given methods' do
expect(cache).to receive(:expire).with(:rendered_readme)
expect(cache).to receive(:expire).with(:branch_names) expect(cache).to receive(:expire).with(:branch_names)
expect(redis_set_cache).to receive(:expire).with(:rendered_readme, :branch_names) expect(redis_set_cache).to receive(:expire).with(:branch_names)
expect(redis_hash_cache).to receive(:delete).with(:rendered_readme, :branch_names) expect(redis_hash_cache).to receive(:delete).with(:branch_names)
repository.expire_method_caches(%i(rendered_readme branch_names)) repository.expire_method_caches(%i(branch_names))
end end
it 'does not expire caches for non-existent methods' do it 'does not expire caches for non-existent methods' do
......
...@@ -46,4 +46,22 @@ RSpec.describe Gitlab::Search::Query do ...@@ -46,4 +46,22 @@ RSpec.describe Gitlab::Search::Query do
expect(subject.filters).to all(include(negated: true)) expect(subject.filters).to all(include(negated: true))
end end
end end
context 'with filter value in quotes' do
let(:query) { '"foo bar" name:"my test script.txt"' }
it 'does not break the filter value in quotes' do
expect(subject.term).to eq('"foo bar"')
expect(subject.filters[0]).to include(name: :name, negated: false, value: "MY TEST SCRIPT.TXT")
end
end
context 'with extra white spaces between the query words' do
let(:query) { ' foo = bar name:"my test.txt"' }
it 'removes the extra whitespace between tokens' do
expect(subject.term).to eq('foo = bar')
expect(subject.filters[0]).to include(name: :name, negated: false, value: "MY TEST.TXT")
end
end
end end
...@@ -1185,60 +1185,6 @@ RSpec.describe Ci::Build do ...@@ -1185,60 +1185,6 @@ RSpec.describe Ci::Build do
end end
end end
describe 'state transition with resource group' do
let(:resource_group) { create(:ci_resource_group, project: project) }
context 'when build status is created' do
let(:build) { create(:ci_build, :created, project: project, resource_group: resource_group) }
it 'is waiting for resource when build is enqueued' do
expect(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async).with(resource_group.id)
expect { build.enqueue! }.to change { build.status }.from('created').to('waiting_for_resource')
expect(build.waiting_for_resource_at).not_to be_nil
end
context 'when build is waiting for resource' do
before do
build.update_column(:status, 'waiting_for_resource')
end
it 'is enqueued when build requests resource' do
expect { build.enqueue_waiting_for_resource! }.to change { build.status }.from('waiting_for_resource').to('pending')
end
it 'releases a resource when build finished' do
expect(build.resource_group).to receive(:release_resource_from).with(build).and_call_original
expect(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async).with(build.resource_group_id)
build.enqueue_waiting_for_resource!
build.success!
end
context 'when build has prerequisites' do
before do
allow(build).to receive(:any_unmet_prerequisites?) { true }
end
it 'is preparing when build is enqueued' do
expect { build.enqueue_waiting_for_resource! }.to change { build.status }.from('waiting_for_resource').to('preparing')
end
end
context 'when there are no available resources' do
before do
resource_group.assign_resource_to(create(:ci_build))
end
it 'stays as waiting for resource when build requests resource' do
expect { build.enqueue_waiting_for_resource }.not_to change { build.status }
end
end
end
end
end
describe '#on_stop' do describe '#on_stop' do
subject { build.on_stop } subject { build.on_stop }
......
...@@ -2321,7 +2321,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do ...@@ -2321,7 +2321,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
context 'on waiting for resource' do context 'on waiting for resource' do
before do before do
allow(build).to receive(:requires_resource?) { true } allow(build).to receive(:with_resource_group?) { true }
allow(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async) allow(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async)
build.enqueue build.enqueue
......
...@@ -122,4 +122,58 @@ RSpec.describe Ci::Processable do ...@@ -122,4 +122,58 @@ RSpec.describe Ci::Processable do
it { is_expected.to be_empty } it { is_expected.to be_empty }
end end
end end
describe 'state transition with resource group' do
let(:resource_group) { create(:ci_resource_group, project: project) }
context 'when build status is created' do
let(:build) { create(:ci_build, :created, project: project, resource_group: resource_group) }
it 'is waiting for resource when build is enqueued' do
expect(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async).with(resource_group.id)
expect { build.enqueue! }.to change { build.status }.from('created').to('waiting_for_resource')
expect(build.waiting_for_resource_at).not_to be_nil
end
context 'when build is waiting for resource' do
before do
build.update_column(:status, 'waiting_for_resource')
end
it 'is enqueued when build requests resource' do
expect { build.enqueue_waiting_for_resource! }.to change { build.status }.from('waiting_for_resource').to('pending')
end
it 'releases a resource when build finished' do
expect(build.resource_group).to receive(:release_resource_from).with(build).and_call_original
expect(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async).with(build.resource_group_id)
build.enqueue_waiting_for_resource!
build.success!
end
context 'when build has prerequisites' do
before do
allow(build).to receive(:any_unmet_prerequisites?) { true }
end
it 'is preparing when build is enqueued' do
expect { build.enqueue_waiting_for_resource! }.to change { build.status }.from('waiting_for_resource').to('preparing')
end
end
context 'when there are no available resources' do
before do
resource_group.assign_resource_to(create(:ci_build))
end
it 'stays as waiting for resource when build requests resource' do
expect { build.enqueue_waiting_for_resource }.not_to change { build.status }
end
end
end
end
end
end end
...@@ -32,12 +32,12 @@ RSpec.describe Ci::ResourceGroup do ...@@ -32,12 +32,12 @@ RSpec.describe Ci::ResourceGroup do
let(:build) { create(:ci_build) } let(:build) { create(:ci_build) }
let(:resource_group) { create(:ci_resource_group) } let(:resource_group) { create(:ci_resource_group) }
it 'retains resource for the build' do it 'retains resource for the processable' do
expect(resource_group.resources.first.build).to be_nil expect(resource_group.resources.first.processable).to be_nil
is_expected.to eq(true) is_expected.to eq(true)
expect(resource_group.resources.first.build).to eq(build) expect(resource_group.resources.first.processable).to eq(build)
end end
context 'when there are no free resources' do context 'when there are no free resources' do
...@@ -51,7 +51,7 @@ RSpec.describe Ci::ResourceGroup do ...@@ -51,7 +51,7 @@ RSpec.describe Ci::ResourceGroup do
end end
context 'when the build has already retained a resource' do context 'when the build has already retained a resource' do
let!(:another_resource) { create(:ci_resource, resource_group: resource_group, build: build) } let!(:another_resource) { create(:ci_resource, resource_group: resource_group, processable: build) }
it 'fails to retain resource' do it 'fails to retain resource' do
expect { subject }.to raise_error(ActiveRecord::RecordNotUnique) expect { subject }.to raise_error(ActiveRecord::RecordNotUnique)
...@@ -71,11 +71,11 @@ RSpec.describe Ci::ResourceGroup do ...@@ -71,11 +71,11 @@ RSpec.describe Ci::ResourceGroup do
end end
it 'releases resource from the build' do it 'releases resource from the build' do
expect(resource_group.resources.first.build).to eq(build) expect(resource_group.resources.first.processable).to eq(build)
is_expected.to eq(true) is_expected.to eq(true)
expect(resource_group.resources.first.build).to be_nil expect(resource_group.resources.first.processable).to be_nil
end end
end end
......
...@@ -19,7 +19,7 @@ RSpec.describe Ci::Resource do ...@@ -19,7 +19,7 @@ RSpec.describe Ci::Resource do
subject { described_class.retained_by(build) } subject { described_class.retained_by(build) }
let(:build) { create(:ci_build) } let(:build) { create(:ci_build) }
let!(:resource) { create(:ci_resource, build: build) } let!(:resource) { create(:ci_resource, processable: build) }
it 'returns retained resources' do it 'returns retained resources' do
is_expected.to eq([resource]) is_expected.to eq([resource])
......
...@@ -725,22 +725,6 @@ RSpec.describe CommitStatus do ...@@ -725,22 +725,6 @@ RSpec.describe CommitStatus do
let(:commit_status) { create(:commit_status) } let(:commit_status) { create(:commit_status) }
it { is_expected.to eq(true) } it { is_expected.to eq(true) }
context 'when build requires a resource' do
before do
allow(commit_status).to receive(:requires_resource?) { true }
end
it { is_expected.to eq(false) }
end
context 'when build has a prerequisite' do
before do
allow(commit_status).to receive(:any_unmet_prerequisites?) { true }
end
it { is_expected.to eq(false) }
end
end end
describe '#enqueue' do describe '#enqueue' do
...@@ -748,7 +732,6 @@ RSpec.describe CommitStatus do ...@@ -748,7 +732,6 @@ RSpec.describe CommitStatus do
before do before do
allow(Time).to receive(:now).and_return(current_time) allow(Time).to receive(:now).and_return(current_time)
expect(commit_status.any_unmet_prerequisites?).to eq false
end end
shared_examples 'commit status enqueued' do shared_examples 'commit status enqueued' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ReadmeBlob do
include FakeBlobHelpers
describe 'policy' do
let(:project) { build(:project, :repository) }
subject { described_class.new(fake_blob(path: 'README.md'), project.repository) }
it 'works with policy' do
expect(Ability.allowed?(project.creator, :read_blob, subject)).to be_truthy
end
end
end
...@@ -483,12 +483,6 @@ RSpec.describe Repository do ...@@ -483,12 +483,6 @@ RSpec.describe Repository do
it { is_expected.to be_an_instance_of(::Blob) } it { is_expected.to be_an_instance_of(::Blob) }
end end
context 'readme blob on HEAD' do
subject { repository.blob_at(repository.head_commit.sha, 'README.md') }
it { is_expected.to be_an_instance_of(::ReadmeBlob) }
end
context 'readme blob not on HEAD' do context 'readme blob not on HEAD' do
subject { repository.blob_at(repository.find_branch('feature').target, 'README.md') } subject { repository.blob_at(repository.find_branch('feature').target, 'README.md') }
...@@ -1938,7 +1932,6 @@ RSpec.describe Repository do ...@@ -1938,7 +1932,6 @@ RSpec.describe Repository do
expect(repository).to receive(:expire_method_caches).with([ expect(repository).to receive(:expire_method_caches).with([
:size, :size,
:commit_count, :commit_count,
:rendered_readme,
:readme_path, :readme_path,
:contribution_guide, :contribution_guide,
:changelog, :changelog,
...@@ -2314,14 +2307,6 @@ RSpec.describe Repository do ...@@ -2314,14 +2307,6 @@ RSpec.describe Repository do
expect(repository.readme).to be_nil expect(repository.readme).to be_nil
end end
end end
context 'when a README exists' do
let(:project) { create(:project, :repository) }
it 'returns the README' do
expect(repository.readme).to be_an_instance_of(ReadmeBlob)
end
end
end end
end end
...@@ -2527,9 +2512,8 @@ RSpec.describe Repository do ...@@ -2527,9 +2512,8 @@ RSpec.describe Repository do
describe '#refresh_method_caches' do describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches) expect(repository).to receive(:expire_method_caches)
.with(%i(rendered_readme readme_path license_blob license_key license)) .with(%i(readme_path license_blob license_key license))
expect(repository).to receive(:rendered_readme)
expect(repository).to receive(:readme_path) expect(repository).to receive(:readme_path)
expect(repository).to receive(:license_blob) expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_key) expect(repository).to receive(:license_key)
......
...@@ -76,6 +76,31 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do ...@@ -76,6 +76,31 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
} }
end end
end end
context 'with resource group' do
let(:config) do
<<~YAML
instrumentation_test:
stage: test
resource_group: iOS
trigger:
include:
- local: path/to/child.yml
YAML
end
# TODO: This test will be properly implemented in the next MR
# for https://gitlab.com/gitlab-org/gitlab/-/issues/39057.
it 'creates bridge job but still resource group is no-op', :aggregate_failures do
pipeline = create_pipeline!
test = pipeline.statuses.find_by(name: 'instrumentation_test')
expect(pipeline).to be_persisted
expect(test).to be_a Ci::Bridge
expect(project.resource_groups.count).to eq(0)
end
end
end end
describe 'child pipeline triggers' do describe 'child pipeline triggers' do
......
...@@ -952,9 +952,9 @@ RSpec.describe Ci::CreatePipelineService do ...@@ -952,9 +952,9 @@ RSpec.describe Ci::CreatePipelineService do
expect(result).to be_persisted expect(result).to be_persisted
expect(deploy_job.resource_group.key).to eq(resource_group_key) expect(deploy_job.resource_group.key).to eq(resource_group_key)
expect(project.resource_groups.count).to eq(1) expect(project.resource_groups.count).to eq(1)
expect(resource_group.builds.count).to eq(1) expect(resource_group.processables.count).to eq(1)
expect(resource_group.resources.count).to eq(1) expect(resource_group.resources.count).to eq(1)
expect(resource_group.resources.first.build).to eq(nil) expect(resource_group.resources.first.processable).to eq(nil)
end end
context 'when resource group key includes predefined variables' do context 'when resource group key includes predefined variables' 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