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 {
merge_requests: 'merge-request-recent-searches',
group_members: 'group-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';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { __ } from '~/locale';
import { deprecatedCreateFlash as flash } from '~/flash';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
......@@ -32,3 +34,65 @@ document.addEventListener('DOMContentLoaded', () => {
new Members(); // 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
# Authorize
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
def index
......
......@@ -20,7 +20,6 @@ module Ci
belongs_to :runner
belongs_to :trigger_request
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
RUNNER_FEATURES = {
......@@ -38,7 +37,6 @@ module Ci
DEGRADATION_THRESHOLD_VARIABLE_NAME = 'DEGRADATION_THRESHOLD'
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_many :trace_sections, class_name: 'Ci::BuildTraceSection'
has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id, inverse_of: :build
......@@ -236,21 +234,14 @@ module Ci
state_machine :status 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?
end
event :enqueue_scheduled do
transition scheduled: :waiting_for_resource, if: :requires_resource?
transition scheduled: :preparing, if: :any_unmet_prerequisites?
transition scheduled: :pending
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
transition preparing: :pending
end
......@@ -279,23 +270,6 @@ module Ci
build.scheduled_at = build.options_scheduled_at
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|
!build.any_unmet_prerequisites? # If false is returned, it stops the transition
end
......@@ -328,16 +302,6 @@ module Ci
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|
build.run_after_commit do
build.run_status_commit_hooks!
......@@ -467,6 +431,11 @@ module Ci
pipeline.builds.retried.where(name: self.name).count
end
override :all_met_to_become_pending?
def all_met_to_become_pending?
super && !any_unmet_prerequisites?
end
def any_unmet_prerequisites?
prerequisites.present?
end
......@@ -501,10 +470,6 @@ module Ci
end
end
def requires_resource?
self.resource_group_id.present?
end
def has_environment?
environment.present?
end
......
......@@ -3,6 +3,11 @@
module Ci
class Processable < ::CommitStatus
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
......@@ -20,6 +25,48 @@ module Ci
where('NOT EXISTS (?)', needs)
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)
aggregated_needs_names = Ci::BuildNeed
.scoped_build
......@@ -77,6 +124,15 @@ module Ci
raise NotImplementedError
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
def scheduling_type_dag?
scheduling_type.nil? ? find_legacy_scheduling_type == :dag : super
......
......@@ -5,9 +5,9 @@ module Ci
extend Gitlab::Ci::Model
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 :retained_by, -> (build) { where(build: build) }
scope :free, -> { where(processable: nil) }
scope :retained_by, -> (processable) { where(processable: processable) }
end
end
......@@ -7,7 +7,7 @@ module Ci
belongs_to :project, inverse_of: :resource_groups
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,
length: { maximum: 255 },
......@@ -19,12 +19,12 @@ module Ci
##
# NOTE: This is concurrency-safe method that the subquery in the `UPDATE`
# works as explicit locking.
def assign_resource_to(build)
resources.free.limit(1).update_all(build_id: build.id) > 0
def assign_resource_to(processable)
resources.free.limit(1).update_all(build_id: processable.id) > 0
end
def release_resource_from(build)
resources.retained_by(build).update_all(build_id: nil) > 0
def release_resource_from(processable)
resources.retained_by(processable).update_all(build_id: nil) > 0
end
private
......
......@@ -255,15 +255,7 @@ class CommitStatus < ApplicationRecord
end
def all_met_to_become_pending?
!any_unmet_prerequisites? && !requires_resource?
end
def any_unmet_prerequisites?
false
end
def requires_resource?
false
true
end
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
#
# For example, for entry `:commit_count` there's a method called `commit_count` which
# 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
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? root_ref merged_branch_names
......@@ -53,7 +53,7 @@ class Repository
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
# the corresponding methods to call for refreshing caches.
METHOD_CACHES_FOR_FILE_TYPES = {
readme: %i(rendered_readme readme_path),
readme: %i(readme_path),
changelog: :changelog,
license: %i(license_blob license_key license),
contributing: :contribution_guide,
......@@ -498,23 +498,7 @@ class Repository
end
def blob_at(sha, path)
blob = 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
Blob.decorate(raw_repository.blob_at(sha, path), container)
rescue Gitlab::Git::Repository::NoRepository
nil
end
......@@ -612,15 +596,6 @@ class Repository
end
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
file_on_head(:contributing)
end
......
......@@ -7,8 +7,8 @@ module Ci
def execute(resource_group)
free_resources = resource_group.resources.free.count
resource_group.builds.waiting_for_resource.take(free_resources).each do |build|
build.enqueue_waiting_for_resource
resource_group.processables.waiting_for_resource.take(free_resources).each do |processable|
processable.enqueue_waiting_for_resource
end
end
# rubocop: enable CodeReuse/ActiveRecord
......
- page_title _("Members")
- group = @project.group
- vue_project_members_list_enabled = Feature.enabled?(:vue_project_members_list, @project)
.js-remove-member-modal
.row.gl-mt-3
......@@ -74,24 +75,44 @@
%span.badge.badge-pill= @requesters.count
.tab-content
#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 }
- if show_groups?(@group_links)
#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)
#tab-invited-members.tab-pane
.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 vue_project_members_list_enabled
.js-project-invited-members-list{ data: project_members_list_data_attributes(@project, @invited_members) }
.loading
.spinner.spinner-md
- else
.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)
#tab-access-requests.tab-pane
.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 }
- if vue_project_members_list_enabled
.js-project-access-requests-list{ data: project_members_list_data_attributes(@project, @requesters) }
.loading
.spinner.spinner-md
- else
.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:
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
"last_activity_on": "2021-01-27"
},
{
"id": 2,
......@@ -292,7 +293,8 @@ Example response:
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"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,
......@@ -300,7 +302,8 @@ Example response:
"name": "Foo bar",
"state": "active",
"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
module Entities
class BillableMember < ::API::Entities::UserBasic
expose :public_email, as: :email
expose :last_activity_on
end
end
end
......
......@@ -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) }
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!
allow(Gitlab.config.ldap).to receive_messages(enabled: true)
......
......@@ -8,6 +8,8 @@ RSpec.describe 'Projects > Audit Events', :js do
let(:project) { create(:project, :repository, namespace: user.namespace) }
before do
stub_feature_flags(vue_project_members_list: false)
project.add_maintainer(user)
sign_in(user)
end
......
......@@ -10,6 +10,7 @@ RSpec.describe 'Project > Members > Invite group and members', :js do
before do
stub_feature_flags(invite_members_group_modal: false)
stub_feature_flags(vue_project_members_list: false)
end
describe 'Share group lock' do
......
......@@ -8,6 +8,8 @@ RSpec.describe 'Projects > Members > Member is removed from project' do
let(:other_user) { create(:user) }
before do
stub_feature_flags(vue_project_members_list: false)
project.add_maintainer(user)
project.add_maintainer(other_user)
sign_in(user)
......
......@@ -3,10 +3,16 @@
require 'spec_helper'
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 }
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
let(:public_email) { 'public@email.com' }
......
......@@ -14,7 +14,7 @@ module Gitlab
ALLOWED_KEYS = %i[tags script type image services start_in artifacts
cache dependencies before_script after_script
environment coverage retry parallel interruptible timeout
resource_group release secrets].freeze
release secrets].freeze
REQUIRED_BY_NEEDS = %i[stage].freeze
......@@ -30,7 +30,6 @@ module Gitlab
}
validates :dependencies, array_of_strings: true
validates :resource_group, type: String
validates :allow_failure, hash_or_boolean: true
end
......@@ -124,7 +123,7 @@ module Gitlab
attributes :script, :tags, :when, :dependencies,
:needs, :retry, :parallel, :start_in,
:interruptible, :timeout, :resource_group,
:interruptible, :timeout,
:release, :allow_failure
def self.matching?(name, config)
......@@ -174,7 +173,6 @@ module Gitlab
ignore: ignored?,
allow_failure_criteria: allow_failure_criteria,
needs: needs_defined? ? needs_value : nil,
resource_group: resource_group,
scheduling_type: needs_defined? ? :dag : :stage
).compact
end
......
......@@ -15,7 +15,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Inheritable
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
validations do
......@@ -32,6 +32,7 @@ module Gitlab
with_options allow_nil: true do
validates :extends, array_of_strings_or_string: true
validates :rules, array_of_hashes: true
validates :resource_group, type: String
end
end
......@@ -64,7 +65,7 @@ module Gitlab
inherit: false,
default: {}
attributes :extends, :rules
attributes :extends, :rules, :resource_group
end
def compose!(deps = nil)
......@@ -125,7 +126,8 @@ module Gitlab
rules: rules_value,
variables: root_and_job_variables_value,
only: only_value,
except: except_value }.compact
except: except_value,
resource_group: resource_group }.compact
end
def root_and_job_variables_value
......
......@@ -8,17 +8,17 @@ module Gitlab
class ResourceGroup < Seed::Base
include Gitlab::Utils::StrongMemoize
attr_reader :build, :resource_group_key
attr_reader :processable, :resource_group_key
def initialize(build, resource_group_key)
@build = build
def initialize(processable, resource_group_key)
@processable = processable
@resource_group_key = resource_group_key
end
def to_resource
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)
resource_group if resource_group.persisted?
......@@ -28,7 +28,7 @@ module Gitlab
def expanded_resource_group_key
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
......
......@@ -5,6 +5,9 @@ module Gitlab
class Query < SimpleDelegator
include EncodingHelper
QUOTES_REGEXP = %r{\A"|"\Z}.freeze
TOKEN_WITH_QUOTES_REGEXP = %r{\s(?=(?:[^"]|"[^"]*")*$)}.freeze
def initialize(query, filter_opts = {}, &block)
@raw_query = query.dup
@filters = []
......@@ -35,22 +38,24 @@ module Gitlab
def extract_filters
fragments = []
query_tokens = parse_raw_query
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
input = match.split(':')[1..-1].join
next if input.empty?
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('\*', '.*?')
fragments << match
parsed_filters << filter
end
query = (@raw_query.split - fragments).join(' ')
query = (query_tokens - fragments).join(' ')
query = '*' if query.empty?
[query, filters]
......@@ -61,6 +66,13 @@ module Gitlab
@filter_options[:encode_binary] ? encode_binary(result) : result
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
......@@ -3281,6 +3281,9 @@ msgstr ""
msgid "An error occurred while loading the file. Please try again later."
msgstr ""
msgid "An error occurred while loading the members, please try again."
msgstr ""
msgid "An error occurred while loading the merge request changes."
msgstr ""
......@@ -18101,6 +18104,9 @@ msgstr ""
msgid "Members|Role updated successfully."
msgstr ""
msgid "Members|Search groups"
msgstr ""
msgid "Members|Search invited"
msgstr ""
......
......@@ -5,7 +5,7 @@ FactoryBot.define do
resource_group factory: :ci_resource_group
trait(:retained) do
build factory: :ci_build
processable factory: :ci_build
end
end
end
......@@ -10,6 +10,8 @@ RSpec.describe "Admin::Projects" do
let(:current_user) { create(:admin) }
before do
stub_feature_flags(vue_project_members_list: false)
sign_in(current_user)
gitlab_enable_admin_mode_sign_in(current_user)
end
......
......@@ -8,6 +8,8 @@ RSpec.describe 'Projects > Members > Anonymous user sees members' do
let(:project) { create(:project, :public) }
before do
stub_feature_flags(vue_project_members_list: false)
project.add_maintainer(user)
create(:project_group_link, project: project, group: group)
end
......
......@@ -13,6 +13,8 @@ RSpec.describe 'Projects members', :js do
let(:group_requester) { create(:user) }
before do
stub_feature_flags(vue_project_members_list: false)
project.add_developer(developer)
group.add_owner(user)
sign_in(user)
......
......@@ -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) }
before do
stub_feature_flags(vue_project_members_list: false)
travel_to Time.now.utc.beginning_of_day
project.add_maintainer(user)
......
......@@ -10,6 +10,7 @@ RSpec.describe 'Project > Members > Invite group', :js do
before do
stub_feature_flags(invite_members_group_modal: false)
stub_feature_flags(vue_project_members_list: false)
end
describe 'Share with group lock' do
......
......@@ -13,10 +13,18 @@ RSpec.describe 'Project members list' do
before do
stub_feature_flags(invite_members_group_modal: false)
stub_feature_flags(vue_project_members_list: false)
sign_in(user1)
group.add_owner(user1)
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
project.add_developer(user2)
......
......@@ -11,6 +11,8 @@ RSpec.describe 'Projects > Members > Maintainer adds member with expiration date
let(:new_member) { create(:user) }
before do
stub_feature_flags(vue_project_members_list: false)
travel_to Time.now.utc.beginning_of_day
project.add_maintainer(maintainer)
......
......@@ -8,6 +8,8 @@ RSpec.describe 'Projects > Members > Sorting' do
let(:project) { create(:project, namespace: maintainer.namespace, creator: maintainer) }
before do
stub_feature_flags(vue_project_members_list: false)
create(:project_member, :developer, user: developer, project: project, created_at: 3.days.ago)
sign_in(maintainer)
......
......@@ -20,6 +20,7 @@ RSpec.describe 'Projects > Members > Tabs' do
end
before do
stub_feature_flags(vue_project_members_list: false)
allow(Kaminari.config).to receive(:default_per_page).and_return(1)
sign_in(user)
......
......@@ -11,6 +11,8 @@ RSpec.describe 'Projects > Settings > User manages project members' do
let(:user_mike) { create(:user, name: 'Mike') }
before do
stub_feature_flags(vue_project_members_list: false)
project.add_maintainer(user)
project.add_developer(user_dmitriy)
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
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
let(:config) do
{
......@@ -340,6 +349,26 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
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 'of variables' do
let(:config) do
......
......@@ -53,6 +53,14 @@ RSpec.describe Gitlab::FileFinder do
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
expect(Gitlab::GitalyClient).to receive(:call).at_most(10).times.and_call_original
......
......@@ -292,12 +292,11 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do
describe '#expire_method_caches' 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(redis_set_cache).to receive(:expire).with(:rendered_readme, :branch_names)
expect(redis_hash_cache).to receive(:delete).with(:rendered_readme, :branch_names)
expect(redis_set_cache).to receive(:expire).with(: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
it 'does not expire caches for non-existent methods' do
......
......@@ -46,4 +46,22 @@ RSpec.describe Gitlab::Search::Query do
expect(subject.filters).to all(include(negated: true))
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
......@@ -1185,60 +1185,6 @@ RSpec.describe Ci::Build do
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
subject { build.on_stop }
......
......@@ -2321,7 +2321,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
context 'on waiting for resource' 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)
build.enqueue
......
......@@ -122,4 +122,58 @@ RSpec.describe Ci::Processable do
it { is_expected.to be_empty }
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
......@@ -32,12 +32,12 @@ RSpec.describe Ci::ResourceGroup do
let(:build) { create(:ci_build) }
let(:resource_group) { create(:ci_resource_group) }
it 'retains resource for the build' do
expect(resource_group.resources.first.build).to be_nil
it 'retains resource for the processable' do
expect(resource_group.resources.first.processable).to be_nil
is_expected.to eq(true)
expect(resource_group.resources.first.build).to eq(build)
expect(resource_group.resources.first.processable).to eq(build)
end
context 'when there are no free resources' do
......@@ -51,7 +51,7 @@ RSpec.describe Ci::ResourceGroup do
end
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
expect { subject }.to raise_error(ActiveRecord::RecordNotUnique)
......@@ -71,11 +71,11 @@ RSpec.describe Ci::ResourceGroup do
end
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)
expect(resource_group.resources.first.build).to be_nil
expect(resource_group.resources.first.processable).to be_nil
end
end
......
......@@ -19,7 +19,7 @@ RSpec.describe Ci::Resource do
subject { described_class.retained_by(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
is_expected.to eq([resource])
......
......@@ -725,22 +725,6 @@ RSpec.describe CommitStatus do
let(:commit_status) { create(:commit_status) }
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
describe '#enqueue' do
......@@ -748,7 +732,6 @@ RSpec.describe CommitStatus do
before do
allow(Time).to receive(:now).and_return(current_time)
expect(commit_status.any_unmet_prerequisites?).to eq false
end
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
it { is_expected.to be_an_instance_of(::Blob) }
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
subject { repository.blob_at(repository.find_branch('feature').target, 'README.md') }
......@@ -1938,7 +1932,6 @@ RSpec.describe Repository do
expect(repository).to receive(:expire_method_caches).with([
:size,
:commit_count,
:rendered_readme,
:readme_path,
:contribution_guide,
:changelog,
......@@ -2314,14 +2307,6 @@ RSpec.describe Repository do
expect(repository.readme).to be_nil
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
......@@ -2527,9 +2512,8 @@ RSpec.describe Repository do
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
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(:license_blob)
expect(repository).to receive(:license_key)
......
......@@ -76,6 +76,31 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
}
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
describe 'child pipeline triggers' do
......
......@@ -952,9 +952,9 @@ RSpec.describe Ci::CreatePipelineService do
expect(result).to be_persisted
expect(deploy_job.resource_group.key).to eq(resource_group_key)
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.first.build).to eq(nil)
expect(resource_group.resources.first.processable).to eq(nil)
end
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