summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-05-05 21:09:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-05 21:09:42 +0000
commit53288eeb6300a5c162f146b13d1710c71f0ee197 (patch)
tree790faa45cf2a56bb0022ef02f989ddbd8ab0c0d9
parent38ceebb9b3a541f8530b379d5b5ab5e13ffc58ed (diff)
downloadgitlab-ce-53288eeb6300a5c162f146b13d1710c71f0ee197.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml2
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue6
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/dag/index.js2
-rw-r--r--app/assets/stylesheets/framework/tables.scss14
-rw-r--r--app/controllers/projects/pipelines_controller.rb5
-rw-r--r--app/controllers/projects/settings/access_tokens_controller.rb71
-rw-r--r--app/helpers/projects_helper.rb6
-rw-r--r--app/models/concerns/timebox.rb66
-rw-r--r--app/models/milestone.rb59
-rw-r--r--app/models/namespace.rb2
-rw-r--r--app/models/personal_access_token.rb21
-rw-r--r--app/models/plan.rb4
-rw-r--r--app/models/project.rb4
-rw-r--r--app/services/resource_access_tokens/create_service.rb (renamed from app/services/resources/create_access_token_service.rb)18
-rw-r--r--app/services/resource_access_tokens/revoke_service.rb65
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml8
-rw-r--r--app/views/projects/settings/access_tokens/index.html.haml0
-rw-r--r--changelogs/unreleased/213324-fix-table-colors.yml5
-rw-r--r--changelogs/unreleased/38358-update-migration-helpers-to-use-check-constraints-instead-of-change.yml5
-rw-r--r--changelogs/unreleased/ph-33586-confirmCancelReply.yml5
-rw-r--r--config/routes/pipelines.rb1
-rw-r--r--config/routes/project.rb6
-rw-r--r--db/migrate/20200415203024_add_offset_pagination_plan_limit.rb9
-rw-r--r--db/structure.sql4
-rw-r--r--doc/administration/instance_limits.md23
-rw-r--r--doc/ci/junit_test_reports.md27
-rw-r--r--doc/development/documentation/styleguide.md38
-rw-r--r--doc/development/geo/framework.md6
-rw-r--r--doc/user/analytics/code_review_analytics.md5
-rw-r--r--doc/user/analytics/index.md7
-rw-r--r--doc/user/analytics/productivity_analytics.md7
-rw-r--r--doc/user/analytics/value_stream_analytics.md7
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance.pngbin5184 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_add_license_v12_3.pngbin28440 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_add_license_v13_0.pngbin0 -> 61862 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_decision.pngbin5975 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_decision_v13_0.pngbin0 -> 40646 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v13_0.pngbin95140 -> 51906 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_search_v12_3.pngbin26074 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_search_v13_0.pngbin0 -> 29857 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_settings_v12_3.pngbin14766 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_settings_v13_0.pngbin0 -> 17567 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_v13_0.pngbin0 -> 85525 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_list_v12_6.pngbin30154 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_list_v13_0.pngbin0 -> 89930 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/policies_maintainer_add_v12_9.pngbin6745 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/policies_maintainer_add_v13_0.pngbin0 -> 22079 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/policies_maintainer_edit_v12_9.pngbin10751 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/policies_maintainer_edit_v13_0.pngbin0 -> 40712 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/policies_v12_9.pngbin18966 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/policies_v13_0.pngbin0 -> 69562 bytes
-rw-r--r--doc/user/compliance/license_compliance/index.md27
-rw-r--r--doc/user/group/contribution_analytics/index.md5
-rw-r--r--doc/user/group/insights/index.md4
-rw-r--r--doc/user/group/issues_analytics/index.md4
-rw-r--r--lib/api/helpers/pagination_strategies.rb34
-rw-r--r--lib/api/projects.rb4
-rw-r--r--lib/banzai/pipeline.rb2
-rw-r--r--lib/banzai/renderer.rb7
-rw-r--r--lib/gitlab/auth.rb4
-rw-r--r--lib/gitlab/pagination/keyset.rb11
-rw-r--r--locale/gitlab.pot12
-rw-r--r--spec/controllers/projects/settings/access_tokens_controller_spec.rb190
-rw-r--r--spec/features/merge_request/user_posts_diff_notes_spec.rb4
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb5
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb51
-rw-r--r--spec/frontend/notes/components/note_form_spec.js8
-rw-r--r--spec/lib/api/helpers/pagination_strategies_spec.rb77
-rw-r--r--spec/lib/banzai/renderer_spec.rb57
-rw-r--r--spec/lib/gitlab/auth_spec.rb8
-rw-r--r--spec/lib/gitlab/pagination/keyset_spec.rb12
-rw-r--r--spec/models/milestone_spec.rb2
-rw-r--r--spec/models/personal_access_token_spec.rb23
-rw-r--r--spec/models/project_spec.rb17
-rw-r--r--spec/services/resource_access_tokens/create_service_spec.rb (renamed from spec/services/resources/create_access_token_service_spec.rb)22
-rw-r--r--spec/services/resource_access_tokens/revoke_service_spec.rb111
76 files changed, 1076 insertions, 133 deletions
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index c63397dceea..1b5280c315b 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -89,7 +89,7 @@ downtime_check:
- rspec_profiling/
- tmp/capybara/
- tmp/memory_test/
- - junit_rspec.xml
+ - log/*.log
reports:
junit: junit_rspec.xml
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index b024884bea0..21d0bffdf1c 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -328,7 +328,8 @@ export default {
<button
class="btn note-edit-cancel js-close-discussion-note-form"
type="button"
- @click="cancelHandler()"
+ data-testid="cancelBatchCommentsEnabled"
+ @click="cancelHandler(true)"
>
{{ __('Cancel') }}
</button>
@@ -353,7 +354,8 @@ export default {
<button
class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
type="button"
- @click="cancelHandler()"
+ data-testid="cancel"
+ @click="cancelHandler(true)"
>
{{ __('Cancel') }}
</button>
diff --git a/app/assets/javascripts/pages/projects/pipelines/dag/index.js b/app/assets/javascripts/pages/projects/pipelines/dag/index.js
new file mode 100644
index 00000000000..d19c22ba556
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pipelines/dag/index.js
@@ -0,0 +1,2 @@
+// /dag is an alias for show
+import '../show/index';
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 514bd090e28..ae5d452e57e 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -4,6 +4,17 @@
}
table {
+ /*
+ * TODO
+ * This is a temporary workaround until we fix the neutral
+ * color palette in https://gitlab.com/gitlab-org/gitlab/-/issues/213570
+ *
+ * Remove this code as soon as this happens
+ */
+ &.gl-table {
+ @include gl-text-gray-700;
+ }
+
&.table {
margin-bottom: $gl-padding;
@@ -32,8 +43,7 @@ table {
}
th {
- background-color: $gray-light;
- font-weight: $gl-font-weight-normal;
+ @include gl-bg-gray-100;
border-bottom: 0;
&.wide {
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 4a08ceb9860..c578925e04c 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -13,6 +13,7 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:junit_pipeline_view)
push_frontend_feature_flag(:filter_pipelines_search)
+ push_frontend_feature_flag(:dag_pipeline_tab)
end
before_action :ensure_pipeline, only: [:show]
@@ -94,6 +95,10 @@ class Projects::PipelinesController < Projects::ApplicationController
render_show
end
+ def dag
+ render_show
+ end
+
def failures
if @pipeline.failed_builds.present?
render_show
diff --git a/app/controllers/projects/settings/access_tokens_controller.rb b/app/controllers/projects/settings/access_tokens_controller.rb
new file mode 100644
index 00000000000..d6b4c4dd5dc
--- /dev/null
+++ b/app/controllers/projects/settings/access_tokens_controller.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Projects
+ module Settings
+ class AccessTokensController < Projects::ApplicationController
+ include ProjectsHelper
+
+ before_action :check_feature_availability
+
+ def index
+ @project_access_token = PersonalAccessToken.new
+ set_index_vars
+ end
+
+ def create
+ token_response = ResourceAccessTokens::CreateService.new(current_user, @project, create_params).execute
+
+ if token_response.success?
+ @project_access_token = token_response.payload[:access_token]
+ PersonalAccessToken.redis_store!(key_identity, @project_access_token.token)
+
+ redirect_to namespace_project_settings_access_tokens_path, notice: _("Your new project access token has been created.")
+ else
+ render :index
+ end
+ end
+
+ def revoke
+ @project_access_token = finder.find(params[:id])
+ revoked_response = ResourceAccessTokens::RevokeService.new(current_user, @project, @project_access_token).execute
+
+ if revoked_response.success?
+ flash[:notice] = _("Revoked project access token %{project_access_token_name}!") % { project_access_token_name: @project_access_token.name }
+ else
+ flash[:alert] = _("Could not revoke project access token %{project_access_token_name}.") % { project_access_token_name: @project_access_token.name }
+ end
+
+ redirect_to namespace_project_settings_access_tokens_path
+ end
+
+ private
+
+ def check_feature_availability
+ render_404 unless project_access_token_available?(@project)
+ end
+
+ def create_params
+ params.require(:project_access_token).permit(:name, :expires_at, scopes: [])
+ end
+
+ def set_index_vars
+ @scopes = Gitlab::Auth.resource_bot_scopes
+ @active_project_access_tokens = finder(state: 'active').execute
+ @inactive_project_access_tokens = finder(state: 'inactive', sort: 'expires_at_asc').execute
+ @new_project_access_token = PersonalAccessToken.redis_getdel(key_identity)
+ end
+
+ def finder(options = {})
+ PersonalAccessTokensFinder.new({ user: bot_users, impersonation: false }.merge(options))
+ end
+
+ def bot_users
+ @project.bots
+ end
+
+ def key_identity
+ "#{current_user.id}:#{@project.id}"
+ end
+ end
+ end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 0d05d60d9fc..3fb0e600465 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -740,6 +740,12 @@ module ProjectsHelper
Gitlab.config.registry.enabled &&
can?(current_user, :destroy_container_image, project)
end
+
+ def project_access_token_available?(project)
+ return false if ::Gitlab.com?
+
+ ::Feature.enabled?(:resource_access_token, project)
+ end
end
ProjectsHelper.prepend_if_ee('EE::ProjectsHelper')
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
index 7b518d3dda3..b460baff8bc 100644
--- a/app/models/concerns/timebox.rb
+++ b/app/models/concerns/timebox.rb
@@ -5,10 +5,31 @@ module Timebox
include AtomicInternalId
include CacheMarkdownField
+ include Gitlab::SQL::Pattern
include IidRoutes
include StripAttribute
+ TimeboxStruct = Struct.new(:title, :name, :id) do
+ # Ensure these models match the interface required for exporting
+ def serializable_hash(_opts = {})
+ { title: title, name: name, id: id }
+ end
+ end
+
+ # Represents a "No Timebox" state used for filtering Issues and Merge
+ # Requests that have no timeboxes assigned.
+ None = TimeboxStruct.new('No Timebox', 'No Timebox', 0)
+ Any = TimeboxStruct.new('Any Timebox', '', -1)
+ Upcoming = TimeboxStruct.new('Upcoming', '#upcoming', -2)
+ Started = TimeboxStruct.new('Started', '#started', -3)
+
included do
+ # Defines the same constants above, but inside the including class.
+ const_set :None, TimeboxStruct.new("No #{self.name}", "No #{self.name}", 0)
+ const_set :Any, TimeboxStruct.new("Any #{self.name}", '', -1)
+ const_set :Upcoming, TimeboxStruct.new('Upcoming', '#upcoming', -2)
+ const_set :Started, TimeboxStruct.new('Started', '#started', -3)
+
alias_method :timebox_id, :id
validates :group, presence: true, unless: :project
@@ -35,6 +56,7 @@ module Timebox
scope :active, -> { with_state(:active) }
scope :closed, -> { with_state(:closed) }
scope :for_projects, -> { where(group: nil).includes(:project) }
+ scope :with_title, -> (title) { where(title: title) }
scope :for_projects_and_groups, -> (projects, groups) do
projects = projects.compact if projects.is_a? Array
@@ -57,6 +79,50 @@ module Timebox
alias_attribute :name, :title
end
+ class_methods do
+ # Searches for timeboxes with a matching title or description.
+ #
+ # This method uses ILIKE on PostgreSQL
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
+ def search(query)
+ fuzzy_search(query, [:title, :description])
+ end
+
+ # Searches for timeboxes with a matching title.
+ #
+ # This method uses ILIKE on PostgreSQL
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
+ def search_title(query)
+ fuzzy_search(query, [:title])
+ end
+
+ def filter_by_state(timeboxes, state)
+ case state
+ when 'closed' then timeboxes.closed
+ when 'all' then timeboxes
+ else timeboxes.active
+ end
+ end
+
+ def count_by_state
+ reorder(nil).group(:state).count
+ end
+
+ def predefined_id?(id)
+ [Any.id, None.id, Upcoming.id, Started.id].include?(id)
+ end
+
+ def predefined?(timebox)
+ predefined_id?(timebox&.id)
+ end
+ end
+
def title=(value)
write_attribute(:title, sanitize_title(value)) if value.present?
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index d37e86cd3d9..b1cac9af30f 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -1,27 +1,12 @@
# frozen_string_literal: true
class Milestone < ApplicationRecord
- # Represents a "No Milestone" state used for filtering Issues and Merge
- # Requests that have no milestone assigned.
- MilestoneStruct = Struct.new(:title, :name, :id) do
- # Ensure these models match the interface required for exporting
- def serializable_hash(_opts = {})
- { title: title, name: name, id: id }
- end
- end
-
- None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
- Any = MilestoneStruct.new('Any Milestone', '', -1)
- Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
- Started = MilestoneStruct.new('Started', '#started', -3)
-
include Sortable
include Referable
include Timebox
include Milestoneish
include FromUnion
include Importable
- include Gitlab::SQL::Pattern
prepend_if_ee('::EE::Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
@@ -54,50 +39,6 @@ class Milestone < ApplicationRecord
state :active
end
- class << self
- # Searches for milestones with a matching title or description.
- #
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
- #
- # query - The search query as a String
- #
- # Returns an ActiveRecord::Relation.
- def search(query)
- fuzzy_search(query, [:title, :description])
- end
-
- # Searches for milestones with a matching title.
- #
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
- #
- # query - The search query as a String
- #
- # Returns an ActiveRecord::Relation.
- def search_title(query)
- fuzzy_search(query, [:title])
- end
-
- def filter_by_state(milestones, state)
- case state
- when 'closed' then milestones.closed
- when 'all' then milestones
- else milestones.active
- end
- end
-
- def count_by_state
- reorder(nil).group(:state).count
- end
-
- def predefined_id?(id)
- [Any.id, None.id, Upcoming.id, Started.id].include?(id)
- end
-
- def predefined?(milestone)
- predefined_id?(milestone&.id)
- end
- end
-
def self.reference_prefix
'%'
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index dfffe49fbbf..98715a8c67c 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -349,7 +349,7 @@ class Namespace < ApplicationRecord
# We default to PlanLimits.new otherwise a lot of specs would fail
# On production each plan should already have associated limits record
# https://gitlab.com/gitlab-org/gitlab/issues/36037
- actual_plan.limits || PlanLimits.new
+ actual_plan.actual_limits
end
def actual_plan_name
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index af079f7ebc4..7afee2a35cb 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -4,6 +4,7 @@ class PersonalAccessToken < ApplicationRecord
include Expirable
include TokenAuthenticatable
include Sortable
+ extend ::Gitlab::Utils::Override
add_authentication_token_field :token, digest: true
@@ -23,6 +24,8 @@ class PersonalAccessToken < ApplicationRecord
scope :without_impersonation, -> { where(impersonation: false) }
scope :for_user, -> (user) { where(user: user) }
scope :preload_users, -> { preload(:user) }
+ scope :order_expires_at_asc, -> { reorder(expires_at: :asc) }
+ scope :order_expires_at_desc, -> { reorder(expires_at: :desc) }
validates :scopes, presence: true
validate :validate_scopes
@@ -39,12 +42,14 @@ class PersonalAccessToken < ApplicationRecord
def self.redis_getdel(user_id)
Gitlab::Redis::SharedState.with do |redis|
- encrypted_token = redis.get(redis_shared_state_key(user_id))
- redis.del(redis_shared_state_key(user_id))
+ redis_key = redis_shared_state_key(user_id)
+ encrypted_token = redis.get(redis_key)
+ redis.del(redis_key)
+
begin
Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
rescue => ex
- logger.warn "Failed to decrypt PersonalAccessToken value stored in Redis for User ##{user_id}: #{ex.class}"
+ logger.warn "Failed to decrypt #{self.name} value stored in Redis for key ##{redis_key}: #{ex.class}"
encrypted_token
end
end
@@ -58,6 +63,16 @@ class PersonalAccessToken < ApplicationRecord
end
end
+ override :simple_sorts
+ def self.simple_sorts
+ super.merge(
+ {
+ 'expires_at_asc' => -> { order_expires_at_asc },
+ 'expires_at_desc' => -> { order_expires_at_desc }
+ }
+ )
+ end
+
protected
def validate_scopes
diff --git a/app/models/plan.rb b/app/models/plan.rb
index 5cfa1e258d6..acac5f9aeae 100644
--- a/app/models/plan.rb
+++ b/app/models/plan.rb
@@ -26,6 +26,10 @@ class Plan < ApplicationRecord
DEFAULT_PLANS
end
+ def actual_limits
+ self.limits || PlanLimits.new
+ end
+
def default?
self.class.default_plans.include?(name)
end
diff --git a/app/models/project.rb b/app/models/project.rb
index bd1785bc620..c3136cde4c5 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1519,6 +1519,10 @@ class Project < ApplicationRecord
end
end
+ def bots
+ users.project_bot
+ end
+
# Filters `users` to return only authorized users of the project
def members_among(users)
if users.is_a?(ActiveRecord::Relation) && !users.loaded?
diff --git a/app/services/resources/create_access_token_service.rb b/app/services/resource_access_tokens/create_service.rb
index fd3c8d78e58..c8e86e68383 100644
--- a/app/services/resources/create_access_token_service.rb
+++ b/app/services/resource_access_tokens/create_service.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
-module Resources
- class CreateAccessTokenService < BaseService
- attr_accessor :resource_type, :resource
-
- def initialize(resource_type, resource, user, params = {})
- @resource_type = resource_type
+module ResourceAccessTokens
+ class CreateService < BaseService
+ def initialize(current_user, resource, params = {})
+ @resource_type = resource.class.name.downcase
@resource = resource
- @current_user = user
+ @current_user = current_user
@params = params.dup
end
@@ -33,6 +31,8 @@ module Resources
private
+ attr_reader :resource_type, :resource
+
def feature_enabled?
::Feature.enabled?(:resource_access_token, resource)
end
@@ -85,7 +85,7 @@ module Resources
def personal_access_token_params
{
- name: "#{resource_type}_bot",
+ name: params[:name] || "#{resource_type}_bot",
impersonation: false,
scopes: params[:scopes] || default_scopes,
expires_at: params[:expires_at] || nil
@@ -93,7 +93,7 @@ module Resources
end
def default_scopes
- Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth.registry_scopes - [:read_user]
+ Gitlab::Auth.resource_bot_scopes
end
def provision_access(resource, user)
diff --git a/app/services/resource_access_tokens/revoke_service.rb b/app/services/resource_access_tokens/revoke_service.rb
new file mode 100644
index 00000000000..eea6bff572b
--- /dev/null
+++ b/app/services/resource_access_tokens/revoke_service.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module ResourceAccessTokens
+ class RevokeService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ RevokeAccessTokenError = Class.new(RuntimeError)
+
+ def initialize(current_user, resource, access_token)
+ @current_user = current_user
+ @access_token = access_token
+ @bot_user = access_token.user
+ @resource = resource
+ end
+
+ def execute
+ return error("Failed to find bot user") unless find_member
+
+ PersonalAccessToken.transaction do
+ access_token.revoke!
+
+ raise RevokeAccessTokenError, "Failed to remove #{bot_user.name} member from: #{resource.name}" unless remove_member
+
+ raise RevokeAccessTokenError, "Migration to ghost user failed" unless migrate_to_ghost_user
+ end
+
+ success("Revoked access token: #{access_token.name}")
+ rescue ActiveRecord::ActiveRecordError, RevokeAccessTokenError => error
+ log_error("Failed to revoke access token for #{bot_user.name}: #{error.message}")
+ error(error.message)
+ end
+
+ private
+
+ attr_reader :current_user, :access_token, :bot_user, :resource
+
+ def remove_member
+ ::Members::DestroyService.new(current_user).execute(find_member)
+ end
+
+ def migrate_to_ghost_user
+ ::Users::MigrateToGhostUserService.new(bot_user).execute
+ end
+
+ def find_member
+ strong_memoize(:member) do
+ if resource.is_a?(Project)
+ resource.project_member(bot_user)
+ elsif resource.is_a?(Group)
+ resource.group_member(bot_user)
+ else
+ false
+ end
+ end
+ end
+
+ def error(message)
+ ServiceResponse.error(message: message)
+ end
+
+ def success(message)
+ ServiceResponse.success(message: message)
+ end
+ end
+end
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 37ca020cfb6..e0090915570 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -1,4 +1,5 @@
- test_reports_enabled = Feature.enabled?(:junit_pipeline_view)
+- dag_pipeline_tab_enabled = Feature.enabled?(:dag_pipeline_tab)
.tabs-holder
%ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs
@@ -9,6 +10,10 @@
= link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
= _('Jobs')
%span.badge.badge-pill.js-builds-counter= pipeline.total_size
+ - if dag_pipeline_tab_enabled
+ %li.js-dag-tab-link
+ = link_to dag_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-dag', action: 'dag', toggle: 'tab' }, class: 'dag-tab' do
+ = _('DAG')
- if @pipeline.failed_builds.present?
%li.js-failures-tab-link
= link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
@@ -75,6 +80,9 @@
%code.bash.js-build-output
= build_summary(build)
+ - if dag_pipeline_tab_enabled
+ #js-tab-dag.tab-pane
+
#js-tab-tests.tab-pane
#js-pipeline-tests-detail
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project
diff --git a/app/views/projects/settings/access_tokens/index.html.haml b/app/views/projects/settings/access_tokens/index.html.haml
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/app/views/projects/settings/access_tokens/index.html.haml
diff --git a/changelogs/unreleased/213324-fix-table-colors.yml b/changelogs/unreleased/213324-fix-table-colors.yml
new file mode 100644
index 00000000000..55410d9d303
--- /dev/null
+++ b/changelogs/unreleased/213324-fix-table-colors.yml
@@ -0,0 +1,5 @@
+---
+title: Increase constrast ratio of text in some tables
+merge_request: 30903
+author:
+type: fixed
diff --git a/changelogs/unreleased/38358-update-migration-helpers-to-use-check-constraints-instead-of-change.yml b/changelogs/unreleased/38358-update-migration-helpers-to-use-check-constraints-instead-of-change.yml
new file mode 100644
index 00000000000..0a0163300e3
--- /dev/null
+++ b/changelogs/unreleased/38358-update-migration-helpers-to-use-check-constraints-instead-of-change.yml
@@ -0,0 +1,5 @@
+---
+title: Support limits for offset based pagination
+merge_request: 28460
+author:
+type: changed
diff --git a/changelogs/unreleased/ph-33586-confirmCancelReply.yml b/changelogs/unreleased/ph-33586-confirmCancelReply.yml
new file mode 100644
index 00000000000..ce0ffc38148
--- /dev/null
+++ b/changelogs/unreleased/ph-33586-confirmCancelReply.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed cancel reply button not alerting the user
+merge_request:
+author:
+type: fixed
diff --git a/config/routes/pipelines.rb b/config/routes/pipelines.rb
index 9b236a8ce17..cc3c3400526 100644
--- a/config/routes/pipelines.rb
+++ b/config/routes/pipelines.rb
@@ -15,6 +15,7 @@ resources :pipelines, only: [:index, :new, :create, :show, :destroy] do
post :cancel
post :retry
get :builds
+ get :dag
get :failures
get :status
get :test_report
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 864750d228f..ac8f621b2b6 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -90,6 +90,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :create_deploy_token, path: 'deploy_token/create'
post :cleanup
end
+
+ resources :access_tokens, only: [:index, :create] do
+ member do
+ put :revoke
+ end
+ end
end
resources :autocomplete_sources, only: [] do
diff --git a/db/migrate/20200415203024_add_offset_pagination_plan_limit.rb b/db/migrate/20200415203024_add_offset_pagination_plan_limit.rb
new file mode 100644
index 00000000000..b4d4be894f6
--- /dev/null
+++ b/db/migrate/20200415203024_add_offset_pagination_plan_limit.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddOffsetPaginationPlanLimit < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column :plan_limits, :offset_pagination_limit, :integer, default: 50000, null: false
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 5007801d85e..db3cc324d4e 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -4793,7 +4793,8 @@ CREATE TABLE public.plan_limits (
project_hooks integer DEFAULT 100 NOT NULL,
group_hooks integer DEFAULT 50 NOT NULL,
ci_project_subscriptions integer DEFAULT 2 NOT NULL,
- ci_pipeline_schedules integer DEFAULT 10 NOT NULL
+ ci_pipeline_schedules integer DEFAULT 10 NOT NULL,
+ offset_pagination_limit integer DEFAULT 50000 NOT NULL
);
CREATE SEQUENCE public.plan_limits_id_seq
@@ -13603,6 +13604,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200415161021
20200415161206
20200415192656
+20200415203024
20200416005331
20200416111111
20200416120128
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index e9a7f01c0c4..42d64713508 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -99,6 +99,29 @@ header. Such emails don't create comments on issues or merge requests.
Sentry payloads sent to GitLab have a 1 MB maximum limit, both for security reasons
and to limit memory consumption.
+## Max offset allowed via REST API for offset-based pagination
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34565) in GitLab 13.0.
+
+When using offset-based pagination in the REST API, there is a limit to the maximum
+requested offset into the set of results. This limit is only applied to endpoints that
+support keyset-based pagination. More information about pagination options can be
+found in the [API docs section on pagination](../api/README.md#pagination).
+
+To set this limit on a self-managed installation, run the following in the
+[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session):
+
+```ruby
+# If limits don't exist for the default plan, you can create one with:
+# Plan.default.create_limits!
+
+Plan.default.limits.update!(offset_pagination_limit: 10000)
+```
+
+- **Default offset pagination limit:** 50000
+
+NOTE: **Note:** Set the limit to `0` to disable it.
+
## CI/CD limits
### Number of jobs in active pipelines
diff --git a/doc/ci/junit_test_reports.md b/doc/ci/junit_test_reports.md
index 54d39c59248..4c95fbaebc2 100644
--- a/doc/ci/junit_test_reports.md
+++ b/doc/ci/junit_test_reports.md
@@ -249,3 +249,30 @@ following command:
```ruby
Feature.enable(:junit_pipeline_view)
```
+
+## Viewing JUnit screenshots on GitLab
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202114) in GitLab 13.0.
+
+If JUnit XML files contain an `attachment` tag, GitLab parses the attachment.
+
+Upload your screenshots as [artifacts](pipelines/job_artifacts.md#artifactsreportsjunit) to GitLab. The `attachment` tag **must** contain the absolute path to the screenshots you uploaded.
+
+```xml
+<testcase time="1.00" name="Test">
+ <system-out>[[ATTACHMENT|/absolute/path/to/some/file]]</system-out>
+</testcase>
+```
+
+When [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/6061) is complete, the attached file will be visible on the pipeline details page.
+
+### Enabling the feature
+
+This feature comes with the `:junit_pipeline_screenshots_view` feature flag disabled by default.
+
+To enable this feature, ask a GitLab administrator with [Rails console access](../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags) to run the
+following command:
+
+```ruby
+Feature.enable(:junit_pipeline_screenshots_view)
+```
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index d29ae81a3c8..e478234df1d 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -565,6 +565,44 @@ to mix types, that is also possible, as long as you don't mix items at the same
- Unordered list item three.
```
+## Tables
+
+Tables should be used to describe complex information in a straightforward
+manner. Note that in many cases, an unordered list is sufficient to describe a
+list of items with a single, simple description per item. But, if you have data
+that is best described by a matrix, tables are the best choice for use.
+
+### Creation guidelines
+
+Due to accessibility and scanability requirements, tables should not have any
+empty cells. If there is no otherwise meaningful value for a cell, consider entering
+*N/A* (for 'not applicable') or *none*.
+
+To help tables be easier to maintain, consider adding additional spaces to the
+column widths to make them consistent. For example:
+
+```markdown
+| App name | Description | Requirements |
+|:---------|:---------------------|:---------------|
+| App 1 | Description text 1. | Requirements 1 |
+| App 2 | Description text 2. | None |
+```
+
+Consider installing a plugin or extension in your editor for formatting tables:
+
+- [Markdown Table Prettifier](https://marketplace.visualstudio.com/items?itemName=darkriszty.markdown-table-prettify) for Visual Studio Code
+- [Markdown Table Formatter](https://packagecontrol.io/packages/Markdown%20Table%20Formatter) for Sublime Text
+- [Markdown Table Formatter](https://atom.io/packages/markdown-table-formatter) for Atom
+
+### Feature tables
+
+When creating tables of lists of features (such as whether or not features are
+available to certain roles on the [Permissions](../../user/permissions.md#project-members-permissions)
+page), use the following phrases (based on the SVG icons):
+
+- *No*: **{dotted-circle}** No
+- *Yes*: **{check-circle}** Yes
+
## Quotes
Valid for Markdown content only, not for frontmatter entries:
diff --git a/doc/development/geo/framework.md b/doc/development/geo/framework.md
index a2ee52cbc7c..ad772166ff1 100644
--- a/doc/development/geo/framework.md
+++ b/doc/development/geo/framework.md
@@ -379,7 +379,8 @@ Widgets should now be replicated by Geo!
1. Update `GET /geo_nodes/status` example response in `doc/api/geo_nodes.md` with new fields.
1. Update `ee/spec/models/geo_node_status_spec.rb` and `ee/spec/factories/geo_node_statuses.rb` with new fields.
-To do: Add verification on secondaries.
+To do: Add verification on secondaries. This should be done as part of
+[Geo: Self Service Framework - First Implementation for Package File verification](https://gitlab.com/groups/gitlab-org/-/epics/1817)
Widgets should now be verified by Geo!
@@ -505,7 +506,8 @@ via the GraphQL API!
#### Admin UI
-To do.
+To do: This should be done as part of
+[Geo: Implement frontend for Self-Service Framework replicables](https://gitlab.com/groups/gitlab-org/-/epics/2525)
Widget sync and verification data (aggregate and individual) should now be
available in the Admin UI!
diff --git a/doc/user/analytics/code_review_analytics.md b/doc/user/analytics/code_review_analytics.md
index bb74e673b56..e0aa01c29b2 100644
--- a/doc/user/analytics/code_review_analytics.md
+++ b/doc/user/analytics/code_review_analytics.md
@@ -1,7 +1,12 @@
---
description: "Learn how long your open merge requests have spent in code review, and what distinguishes the longest-running." # Up to ~200 chars long. They will be displayed in Google Search snippets. It may help to write the page intro first, and then reuse it here.
+stage: Manage
+group: Analytics
+To determine the technical writer assigned to the Stage/Group associated with this page, see:
+ https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
+
# Code Review Analytics **(STARTER)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/38062) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.7.
diff --git a/doc/user/analytics/index.md b/doc/user/analytics/index.md
index b2f7da234ad..e58dd771552 100644
--- a/doc/user/analytics/index.md
+++ b/doc/user/analytics/index.md
@@ -1,3 +1,10 @@
+---
+stage: Manage
+group: Analytics
+To determine the technical writer assigned to the Stage/Group associated with this page, see:
+ https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Analytics
## Analytics workspace
diff --git a/doc/user/analytics/productivity_analytics.md b/doc/user/analytics/productivity_analytics.md
index 0fa990150d7..d0fda61d6a5 100644
--- a/doc/user/analytics/productivity_analytics.md
+++ b/doc/user/analytics/productivity_analytics.md
@@ -1,3 +1,10 @@
+---
+stage: Manage
+group: Analytics
+To determine the technical writer assigned to the Stage/Group associated with this page, see:
+ https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Productivity Analytics **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12079) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
diff --git a/doc/user/analytics/value_stream_analytics.md b/doc/user/analytics/value_stream_analytics.md
index bab836770f6..1ded4a0cf0a 100644
--- a/doc/user/analytics/value_stream_analytics.md
+++ b/doc/user/analytics/value_stream_analytics.md
@@ -1,3 +1,10 @@
+---
+stage: Manage
+group: Analytics
+To determine the technical writer assigned to the Stage/Group associated with this page, see:
+ https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Value Stream Analytics
> - Introduced as Cycle Analytics prior to GitLab 12.3 at the project level.
diff --git a/doc/user/compliance/license_compliance/img/license_compliance.png b/doc/user/compliance/license_compliance/img/license_compliance.png
deleted file mode 100644
index cdce6b5fe38..00000000000
--- a/doc/user/compliance/license_compliance/img/license_compliance.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_add_license_v12_3.png b/doc/user/compliance/license_compliance/img/license_compliance_add_license_v12_3.png
deleted file mode 100644
index ea4db16284c..00000000000
--- a/doc/user/compliance/license_compliance/img/license_compliance_add_license_v12_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_add_license_v13_0.png b/doc/user/compliance/license_compliance/img/license_compliance_add_license_v13_0.png
new file mode 100644
index 00000000000..992c08edcd3
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/license_compliance_add_license_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_decision.png b/doc/user/compliance/license_compliance/img/license_compliance_decision.png
deleted file mode 100644
index fbf90bec7fd..00000000000
--- a/doc/user/compliance/license_compliance/img/license_compliance_decision.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_decision_v13_0.png b/doc/user/compliance/license_compliance/img/license_compliance_decision_v13_0.png
new file mode 100644
index 00000000000..d6c6142c0e7
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/license_compliance_decision_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v13_0.png b/doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v13_0.png
index 5dc46dbf979..9ae59e2b96b 100644
--- a/doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v13_0.png
+++ b/doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_search_v12_3.png b/doc/user/compliance/license_compliance/img/license_compliance_search_v12_3.png
deleted file mode 100644
index 4a7cff2e85c..00000000000
--- a/doc/user/compliance/license_compliance/img/license_compliance_search_v12_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_search_v13_0.png b/doc/user/compliance/license_compliance/img/license_compliance_search_v13_0.png
new file mode 100644
index 00000000000..8ee55003768
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/license_compliance_search_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_settings_v12_3.png b/doc/user/compliance/license_compliance/img/license_compliance_settings_v12_3.png
deleted file mode 100644
index 72d0888a9dc..00000000000
--- a/doc/user/compliance/license_compliance/img/license_compliance_settings_v12_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_settings_v13_0.png b/doc/user/compliance/license_compliance/img/license_compliance_settings_v13_0.png
new file mode 100644
index 00000000000..52b26abd9c5
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/license_compliance_settings_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_v13_0.png b/doc/user/compliance/license_compliance/img/license_compliance_v13_0.png
new file mode 100644
index 00000000000..dc227bf05ef
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/license_compliance_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_list_v12_6.png b/doc/user/compliance/license_compliance/img/license_list_v12_6.png
deleted file mode 100644
index 8f2b510be0d..00000000000
--- a/doc/user/compliance/license_compliance/img/license_list_v12_6.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_list_v13_0.png b/doc/user/compliance/license_compliance/img/license_list_v13_0.png
new file mode 100644
index 00000000000..3964c837c6a
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/license_list_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/policies_maintainer_add_v12_9.png b/doc/user/compliance/license_compliance/img/policies_maintainer_add_v12_9.png
deleted file mode 100644
index ad5a49eebe5..00000000000
--- a/doc/user/compliance/license_compliance/img/policies_maintainer_add_v12_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/policies_maintainer_add_v13_0.png b/doc/user/compliance/license_compliance/img/policies_maintainer_add_v13_0.png
new file mode 100644
index 00000000000..8070e2cb1a5
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/policies_maintainer_add_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/policies_maintainer_edit_v12_9.png b/doc/user/compliance/license_compliance/img/policies_maintainer_edit_v12_9.png
deleted file mode 100644
index 4f2380a0bf6..00000000000
--- a/doc/user/compliance/license_compliance/img/policies_maintainer_edit_v12_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/policies_maintainer_edit_v13_0.png b/doc/user/compliance/license_compliance/img/policies_maintainer_edit_v13_0.png
new file mode 100644
index 00000000000..741d1237751
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/policies_maintainer_edit_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/policies_v12_9.png b/doc/user/compliance/license_compliance/img/policies_v12_9.png
deleted file mode 100644
index b3bca716ae5..00000000000
--- a/doc/user/compliance/license_compliance/img/policies_v12_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/policies_v13_0.png b/doc/user/compliance/license_compliance/img/policies_v13_0.png
new file mode 100644
index 00000000000..4712d2b7aba
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/policies_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md
index 9002fdf8229..3e468cbb607 100644
--- a/doc/user/compliance/license_compliance/index.md
+++ b/doc/user/compliance/license_compliance/index.md
@@ -30,12 +30,20 @@ will be displayed in the merge request area. That is the case when you add the
Consecutive merge requests will have something to compare to and the license
compliance report will be shown properly.
-![License Compliance Widget](img/license_compliance.png)
+![License Compliance Widget](img/license_compliance_v13_0.png)
If you are a project or group Maintainer, you can click on a license to be given
the choice to allow it or deny it.
-![License approval decision](img/license_compliance_decision.png)
+![License approval decision](img/license_compliance_decision_v13_0.png)
+
+When GitLab detects a **Denied** license, you can view it in the [license list](#license-list).
+
+![License List](img/license_list_v13_0.png)
+
+You can view and modify existing policies from the [policies](#policies) tab.
+
+![Edit Policy](img/policies_maintainer_edit_v13_0.png)
## Use cases
@@ -402,7 +410,7 @@ To allow or deny a license:
**License Compliance** section.
1. Click the **Add a license** button.
- ![License Compliance Add License](img/license_compliance_add_license_v12_3.png)
+ ![License Compliance Add License](img/license_compliance_add_license_v13_0.png)
1. In the **License name** dropdown, either:
- Select one of the available licenses. You can search for licenses in the field
@@ -416,13 +424,13 @@ To modify an existing license:
1. In the **License Compliance** list, click the **Allow/Deny** dropdown to change it to the desired status.
- ![License Compliance Settings](img/license_compliance_settings_v12_3.png)
+ ![License Compliance Settings](img/license_compliance_settings_v13_0.png)
Searching for Licenses:
1. Use the **Search** box to search for a specific license.
- ![License Compliance Search](img/license_compliance_search_v12_3.png)
+ ![License Compliance Search](img/license_compliance_search_v13_0.png)
## License Compliance report under pipelines
@@ -465,8 +473,9 @@ in your project's sidebar, and you'll see the licenses displayed, where:
- **Name:** The name of the license.
- **Component:** The components which have this license.
+- **Policy Violation:** The license has a [license policy](#policies) marked as **Deny**.
-![License List](img/license_list_v12_6.png)
+![License List](img/license_list_v13_0.png)
## Policies
@@ -477,9 +486,9 @@ and the associated classifications for each.
Policies can be configured by maintainers of the project.
-![Edit Policy](img/policies_maintainer_edit_v12_9.png)
-![Add Policy](img/policies_maintainer_add_v12_9.png)
+![Edit Policy](img/policies_maintainer_edit_v13_0.png)
+![Add Policy](img/policies_maintainer_add_v13_0.png)
Developers of the project can view the policies configured in a project.
-![View Policies](img/policies_v12_9.png)
+![View Policies](img/policies_v13_0.png)
diff --git a/doc/user/group/contribution_analytics/index.md b/doc/user/group/contribution_analytics/index.md
index 1bbc40a14a4..03f0ad6ad1c 100644
--- a/doc/user/group/contribution_analytics/index.md
+++ b/doc/user/group/contribution_analytics/index.md
@@ -1,7 +1,10 @@
---
type: reference
+stage: Manage
+group: Analytics
+To determine the technical writer assigned to the Stage/Group associated with this page, see:
+ https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
-
# Contribution Analytics **(STARTER)**
> - Introduced in [GitLab Starter](https://about.gitlab.com/pricing/) 8.3.
diff --git a/doc/user/group/insights/index.md b/doc/user/group/insights/index.md
index edbb85962ed..cffbc013e66 100644
--- a/doc/user/group/insights/index.md
+++ b/doc/user/group/insights/index.md
@@ -1,5 +1,9 @@
---
type: reference, howto
+stage: Manage
+group: Analytics
+To determine the technical writer assigned to the Stage/Group associated with this page, see:
+ https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Insights **(ULTIMATE)**
diff --git a/doc/user/group/issues_analytics/index.md b/doc/user/group/issues_analytics/index.md
index 4477b9bb1e6..df96f2626e1 100644
--- a/doc/user/group/issues_analytics/index.md
+++ b/doc/user/group/issues_analytics/index.md
@@ -1,5 +1,9 @@
---
type: reference
+stage: Manage
+group: Analytics
+To determine the technical writer assigned to the Stage/Group associated with this page, see:
+ https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Issues Analytics **(PREMIUM)**
diff --git a/lib/api/helpers/pagination_strategies.rb b/lib/api/helpers/pagination_strategies.rb
index 6bebb4bfeac..823891d6fe7 100644
--- a/lib/api/helpers/pagination_strategies.rb
+++ b/lib/api/helpers/pagination_strategies.rb
@@ -3,19 +3,24 @@
module API
module Helpers
module PaginationStrategies
- def paginate_with_strategies(relation)
- paginator = paginator(relation)
+ def paginate_with_strategies(relation, request_scope)
+ paginator = paginator(relation, request_scope)
yield(paginator.paginate(relation)).tap do |records, _|
paginator.finalize(records)
end
end
- def paginator(relation)
- return Gitlab::Pagination::OffsetPagination.new(self) unless keyset_pagination_enabled?
+ def paginator(relation, request_scope = nil)
+ return keyset_paginator(relation) if keyset_pagination_enabled?
- request_context = Gitlab::Pagination::Keyset::RequestContext.new(self)
+ offset_paginator(relation, request_scope)
+ end
+
+ private
+ def keyset_paginator(relation)
+ request_context = Gitlab::Pagination::Keyset::RequestContext.new(self)
unless Gitlab::Pagination::Keyset.available?(request_context, relation)
return error!('Keyset pagination is not yet available for this type of request', 405)
end
@@ -23,11 +28,28 @@ module API
Gitlab::Pagination::Keyset::Pager.new(request_context)
end
- private
+ def offset_paginator(relation, request_scope)
+ offset_limit = limit_for_scope(request_scope)
+ if Gitlab::Pagination::Keyset.available_for_type?(relation) && offset_limit_exceeded?(offset_limit)
+ return error!("Offset pagination has a maximum allowed offset of #{offset_limit} " \
+ "for requests that return objects of type #{relation.klass}. " \
+ "Remaining records can be retrieved using keyset pagination.", 405)
+ end
+
+ Gitlab::Pagination::OffsetPagination.new(self)
+ end
def keyset_pagination_enabled?
params[:pagination] == 'keyset'
end
+
+ def limit_for_scope(scope)
+ (scope || Plan.default).actual_limits.offset_pagination_limit
+ end
+
+ def offset_limit_exceeded?(offset_limit)
+ offset_limit.positive? && params[:page] * params[:per_page] > offset_limit
+ end
end
end
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index ee0731a331f..732453cf1c4 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -95,7 +95,7 @@ module API
projects = reorder_projects(projects)
projects = apply_filters(projects)
- records, options = paginate_with_strategies(projects) do |projects|
+ records, options = paginate_with_strategies(projects, options[:request_scope]) do |projects|
projects, options = with_custom_attributes(projects, options)
options = options.reverse_merge(
@@ -313,7 +313,7 @@ module API
get ':id/forks' do
forks = ForkProjectsFinder.new(user_project, params: project_finder_params, current_user: current_user).execute
- present_projects forks
+ present_projects forks, request_scope: user_project
end
desc 'Check pages access of this project'
diff --git a/lib/banzai/pipeline.rb b/lib/banzai/pipeline.rb
index 8fdbc044861..01cadb11e83 100644
--- a/lib/banzai/pipeline.rb
+++ b/lib/banzai/pipeline.rb
@@ -9,7 +9,7 @@ module Banzai
# Examples:
# Pipeline[nil] # => Banzai::Pipeline::FullPipeline
# Pipeline[:label] # => Banzai::Pipeline::LabelPipeline
- # Pipeline[StatusPage::PostProcessPipeline] # => StatusPage::PostProcessPipeline
+ # Pipeline[StatusPage::Pipeline::PostProcessPipeline] # => StatusPage::Pipeline::PostProcessPipeline
#
# Pipeline['label'] # => raises ArgumentError - unsupport type
# Pipeline[Project] # => raises ArgumentError - not a subclass of BasePipeline
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index 3cb9ec21e8f..fbbd6135959 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -138,15 +138,18 @@ module Banzai
#
# html - String to process
# context - Hash of options to customize output
- # :pipeline - Symbol pipeline type
+ # :pipeline - Symbol pipeline type - for context transform only, defaults to :full
# :project - Project
# :user - User object
+ # :post_process_pipeline - pipeline to use for post_processing - defaults to PostProcessPipeline
#
# Returns an HTML-safe String
def self.post_process(html, context)
context = Pipeline[context[:pipeline]].transform_context(context)
- pipeline = Pipeline[:post_process]
+ # Use a passed class for the pipeline or default to PostProcessPipeline
+ pipeline = context.delete(:post_process_pipeline) || ::Banzai::Pipeline::PostProcessPipeline
+
if context[:xhtml]
pipeline.to_document(html, context).to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML)
else
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 8e14d21f591..44e8c9c04b9 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -337,6 +337,10 @@ module Gitlab
REGISTRY_SCOPES
end
+ def resource_bot_scopes
+ Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth.registry_scopes - [:read_user]
+ end
+
private
def non_admin_available_scopes
diff --git a/lib/gitlab/pagination/keyset.rb b/lib/gitlab/pagination/keyset.rb
index 8692f30e165..67a5530d46c 100644
--- a/lib/gitlab/pagination/keyset.rb
+++ b/lib/gitlab/pagination/keyset.rb
@@ -3,11 +3,18 @@
module Gitlab
module Pagination
module Keyset
+ SUPPORTED_TYPES = [
+ Project
+ ].freeze
+
+ def self.available_for_type?(relation)
+ SUPPORTED_TYPES.include?(relation.klass)
+ end
+
def self.available?(request_context, relation)
order_by = request_context.page.order_by
- # This is only available for Project and order-by id (asc/desc)
- return false unless relation.klass == Project
+ return false unless available_for_type?(relation)
return false unless order_by.size == 1 && order_by[:id]
true
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index faf8101ac3a..04d92df901e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6072,6 +6072,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -6620,6 +6623,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -17796,6 +17802,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -24521,6 +24530,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
diff --git a/spec/controllers/projects/settings/access_tokens_controller_spec.rb b/spec/controllers/projects/settings/access_tokens_controller_spec.rb
new file mode 100644
index 00000000000..884a5bc2836
--- /dev/null
+++ b/spec/controllers/projects/settings/access_tokens_controller_spec.rb
@@ -0,0 +1,190 @@
+# frozen_string_literal: true
+
+require('spec_helper')
+
+describe Projects::Settings::AccessTokensController do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples 'feature unavailability' do
+ context 'when flag is disabled' do
+ before do
+ stub_feature_flags(resource_access_token: false)
+ end
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+
+ context 'when environment is Gitlab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+ end
+
+ describe '#index' do
+ subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
+
+ it_behaves_like 'feature unavailability'
+
+ context 'when feature is available' do
+ let_it_be(:bot_user) { create(:user, :project_bot) }
+ let_it_be(:active_project_access_token) { create(:personal_access_token, user: bot_user) }
+ let_it_be(:inactive_project_access_token) { create(:personal_access_token, :revoked, user: bot_user) }
+
+ before_all do
+ project.add_maintainer(bot_user)
+ end
+
+ before do
+ enable_feature
+ end
+
+ it 'retrieves active project access tokens' do
+ subject
+
+ expect(assigns(:active_project_access_tokens)).to contain_exactly(active_project_access_token)
+ end
+
+ it 'retrieves inactive project access tokens' do
+ subject
+
+ expect(assigns(:inactive_project_access_tokens)).to contain_exactly(inactive_project_access_token)
+ end
+
+ it 'lists all available scopes' do
+ subject
+
+ expect(assigns(:scopes)).to eq(Gitlab::Auth.resource_bot_scopes)
+ end
+
+ it 'retrieves newly created personal access token value' do
+ token_value = 'random-value'
+ allow(PersonalAccessToken).to receive(:redis_getdel).with("#{user.id}:#{project.id}").and_return(token_value)
+
+ subject
+
+ expect(assigns(:new_project_access_token)).to eq(token_value)
+ end
+ end
+ end
+
+ describe '#create', :clean_gitlab_redis_shared_state do
+ subject { post :create, params: { namespace_id: project.namespace, project_id: project }.merge(project_access_token: access_token_params) }
+
+ let_it_be(:access_token_params) { {} }
+
+ it_behaves_like 'feature unavailability'
+
+ context 'when feature is available' do
+ let_it_be(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: 1.month.since.to_date } }
+
+ before do
+ enable_feature
+ end
+
+ def created_token
+ PersonalAccessToken.order(:created_at).last
+ end
+
+ it 'returns success message' do
+ subject
+
+ expect(response.flash[:notice]).to match(/\AYour new project access token has been created./i)
+ end
+
+ it 'creates project access token' do
+ subject
+
+ expect(created_token.name).to eq(access_token_params[:name])
+ expect(created_token.scopes).to eq(access_token_params[:scopes])
+ expect(created_token.expires_at).to eq(access_token_params[:expires_at])
+ end
+
+ it 'creates project bot user' do
+ subject
+
+ expect(created_token.user).to be_project_bot
+ end
+
+ it 'stores newly created token redis store' do
+ expect(PersonalAccessToken).to receive(:redis_store!)
+
+ subject
+ end
+
+ it { expect { subject }.to change { User.count }.by(1) }
+ it { expect { subject }.to change { PersonalAccessToken.count }.by(1) }
+
+ context 'when unsuccessful' do
+ before do
+ allow_next_instance_of(ResourceAccessTokens::CreateService) do |service|
+ allow(service).to receive(:execute).and_return ServiceResponse.error(message: 'Failed!')
+ end
+ end
+
+ it { expect(subject).to render_template(:index) }
+ end
+ end
+ end
+
+ describe '#revoke' do
+ subject { put :revoke, params: { namespace_id: project.namespace, project_id: project, id: project_access_token } }
+
+ let_it_be(:bot_user) { create(:user, :project_bot) }
+ let_it_be(:project_access_token) { create(:personal_access_token, user: bot_user) }
+
+ before_all do
+ project.add_maintainer(bot_user)
+ end
+
+ it_behaves_like 'feature unavailability'
+
+ context 'when feature is available' do
+ before do
+ enable_feature
+ end
+
+ it 'revokes token access' do
+ subject
+
+ expect(project_access_token.reload.revoked?).to be true
+ end
+
+ it 'removed membership of bot user' do
+ subject
+
+ expect(project.reload.bots).not_to include(bot_user)
+ end
+
+ it 'blocks project bot user' do
+ subject
+
+ expect(bot_user.reload.blocked?).to be true
+ end
+
+ it 'converts issuables of the bot user to ghost user' do
+ issue = create(:issue, author: bot_user)
+
+ subject
+
+ expect(issue.reload.author.ghost?).to be true
+ end
+ end
+ end
+
+ def enable_feature
+ allow(Gitlab).to receive(:com?).and_return(false)
+ stub_feature_flags(resource_access_token: true)
+ end
+end
diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb
index 19f82058be2..ebfb5ce796f 100644
--- a/spec/features/merge_request/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb
@@ -235,7 +235,9 @@ describe 'Merge request > User posts diff notes', :js do
def should_allow_dismissing_a_comment(line_holder, diff_side = nil)
write_comment_on_line(line_holder, diff_side)
- find('.js-close-discussion-note-form').click
+ accept_confirm do
+ find('.js-close-discussion-note-form').click
+ end
assert_comment_dismissal(line_holder)
end
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index b22f5a6c211..0548d958322 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -147,7 +147,10 @@ describe 'Merge request > User posts notes', :js do
it 'resets the edit note form textarea with the original content of the note if cancelled' do
within('.current-note-edit-form') do
fill_in 'note[note]', with: 'Some new content'
- find('.btn-cancel').click
+
+ accept_confirm do
+ find('.btn-cancel').click
+ end
end
expect(find('.js-note-text').text).to eq ''
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index aad57bd9b16..de81547887b 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -327,9 +327,10 @@ describe 'Pipeline', :js do
visit_pipeline
end
- it 'shows Pipeline, Jobs and Failed Jobs tabs with link' do
+ it 'shows Pipeline, Jobs, DAG and Failed Jobs tabs with link' do
expect(page).to have_link('Pipeline')
expect(page).to have_link('Jobs')
+ expect(page).to have_link('DAG')
expect(page).to have_link('Failed Jobs')
end
@@ -614,6 +615,20 @@ describe 'Pipeline', :js do
end
end
end
+
+ context 'when FF dag_pipeline_tab is disabled' do
+ before do
+ stub_feature_flags(dag_pipeline_tab: false)
+ visit_pipeline
+ end
+
+ it 'does not show DAG link' do
+ expect(page).to have_link('Pipeline')
+ expect(page).to have_link('Jobs')
+ expect(page).not_to have_link('DAG')
+ expect(page).to have_link('Failed Jobs')
+ end
+ end
end
context 'when user does not have access to read jobs' do
@@ -865,9 +880,10 @@ describe 'Pipeline', :js do
end
context 'page tabs' do
- it 'shows Pipeline and Jobs tabs with link' do
+ it 'shows Pipeline, Jobs and DAG tabs with link' do
expect(page).to have_link('Pipeline')
expect(page).to have_link('Jobs')
+ expect(page).to have_link('DAG')
end
it 'shows counter in Jobs tab' do
@@ -1057,6 +1073,37 @@ describe 'Pipeline', :js do
end
end
+ describe 'GET /:project/pipelines/:id/dag' do
+ include_context 'pipeline builds'
+
+ let(:project) { create(:project, :repository) }
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
+
+ before do
+ visit dag_project_pipeline_path(project, pipeline)
+ end
+
+ it 'shows DAG tab pane as active' do
+ expect(page).to have_css('#js-tab-dag.active', visible: false)
+ end
+
+ context 'page tabs' do
+ it 'shows Pipeline, Jobs and DAG tabs with link' do
+ expect(page).to have_link('Pipeline')
+ expect(page).to have_link('Jobs')
+ expect(page).to have_link('DAG')
+ end
+
+ it 'shows counter in Jobs tab' do
+ expect(page.find('.js-builds-counter').text).to eq(pipeline.total_size.to_s)
+ end
+
+ it 'shows DAG tab as active' do
+ expect(page).to have_css('li.js-dag-tab-link .active')
+ end
+ end
+ end
+
context 'when user sees pipeline flags in a pipeline detail page' do
let(:project) { create(:project, :repository) }
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index bccac03126c..8270c148fb5 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -161,18 +161,18 @@ describe('issue_note_form component', () => {
describe('actions', () => {
it('should be possible to cancel', () => {
- // TODO: do not spy on vm
- jest.spyOn(wrapper.vm, 'cancelHandler');
+ const cancelHandler = jest.fn();
wrapper.setProps({
...props,
isEditing: true,
});
+ wrapper.setMethods({ cancelHandler });
return wrapper.vm.$nextTick().then(() => {
- const cancelButton = wrapper.find('.note-edit-cancel');
+ const cancelButton = wrapper.find('[data-testid="cancel"]');
cancelButton.trigger('click');
- expect(wrapper.vm.cancelHandler).toHaveBeenCalled();
+ expect(cancelHandler).toHaveBeenCalledWith(true);
});
});
diff --git a/spec/lib/api/helpers/pagination_strategies_spec.rb b/spec/lib/api/helpers/pagination_strategies_spec.rb
index a418c09a824..eaa71159714 100644
--- a/spec/lib/api/helpers/pagination_strategies_spec.rb
+++ b/spec/lib/api/helpers/pagination_strategies_spec.rb
@@ -6,7 +6,7 @@ describe API::Helpers::PaginationStrategies do
subject { Class.new.include(described_class).new }
let(:expected_result) { double("result") }
- let(:relation) { double("relation") }
+ let(:relation) { double("relation", klass: "SomeClass") }
let(:params) { {} }
before do
@@ -17,18 +17,18 @@ describe API::Helpers::PaginationStrategies do
let(:paginator) { double("paginator", paginate: expected_result, finalize: nil) }
before do
- allow(subject).to receive(:paginator).with(relation).and_return(paginator)
+ allow(subject).to receive(:paginator).with(relation, nil).and_return(paginator)
end
it 'yields paginated relation' do
- expect { |b| subject.paginate_with_strategies(relation, &b) }.to yield_with_args(expected_result)
+ expect { |b| subject.paginate_with_strategies(relation, nil, &b) }.to yield_with_args(expected_result)
end
it 'calls #finalize with first value returned from block' do
return_value = double
expect(paginator).to receive(:finalize).with(return_value)
- subject.paginate_with_strategies(relation) do |records|
+ subject.paginate_with_strategies(relation, nil) do |records|
some_options = {}
[return_value, some_options]
end
@@ -37,7 +37,7 @@ describe API::Helpers::PaginationStrategies do
it 'returns whatever the block returns' do
return_value = [double, double]
- result = subject.paginate_with_strategies(relation) do |records|
+ result = subject.paginate_with_strategies(relation, nil) do |records|
return_value
end
@@ -47,16 +47,77 @@ describe API::Helpers::PaginationStrategies do
describe '#paginator' do
context 'offset pagination' do
+ let(:plan_limits) { Plan.default.actual_limits }
+ let(:offset_limit) { plan_limits.offset_pagination_limit }
let(:paginator) { double("paginator") }
before do
allow(subject).to receive(:keyset_pagination_enabled?).and_return(false)
end
- it 'delegates to OffsetPagination' do
- expect(Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(paginator)
+ context 'when keyset pagination is available for the relation' do
+ before do
+ allow(Gitlab::Pagination::Keyset).to receive(:available_for_type?).and_return(true)
+ end
+
+ context 'when a request scope is given' do
+ let(:params) { { per_page: 100, page: offset_limit / 100 + 1 } }
+ let(:request_scope) { double("scope", actual_limits: plan_limits) }
+
+ context 'when the scope limit is exceeded' do
+ it 'renders a 405 error' do
+ expect(subject).to receive(:error!).with(/maximum allowed offset/, 405)
+
+ subject.paginator(relation, request_scope)
+ end
+ end
+
+ context 'when the scope limit is not exceeded' do
+ let(:params) { { per_page: 100, page: offset_limit / 100 } }
+
+ it 'delegates to OffsetPagination' do
+ expect(Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(paginator)
+
+ expect(subject.paginator(relation, request_scope)).to eq(paginator)
+ end
+ end
+ end
+
+ context 'when a request scope is not given' do
+ context 'when the default limits are exceeded' do
+ let(:params) { { per_page: 100, page: offset_limit / 100 + 1 } }
+
+ it 'renders a 405 error' do
+ expect(subject).to receive(:error!).with(/maximum allowed offset/, 405)
+
+ subject.paginator(relation)
+ end
+ end
- expect(subject.paginator(relation)).to eq(paginator)
+ context 'when the default limits are not exceeded' do
+ let(:params) { { per_page: 100, page: offset_limit / 100 } }
+
+ it 'delegates to OffsetPagination' do
+ expect(Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(paginator)
+
+ expect(subject.paginator(relation)).to eq(paginator)
+ end
+ end
+ end
+ end
+
+ context 'when keyset pagination is not available for the relation' do
+ let(:params) { { per_page: 100, page: offset_limit / 100 + 1 } }
+
+ before do
+ allow(Gitlab::Pagination::Keyset).to receive(:available_for_type?).and_return(false)
+ end
+
+ it 'delegates to OffsetPagination' do
+ expect(Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(paginator)
+
+ expect(subject.paginator(relation)).to eq(paginator)
+ end
end
end
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
index 0d329b47aa3..b540a76face 100644
--- a/spec/lib/banzai/renderer_spec.rb
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Banzai::Renderer do
+ let(:renderer) { described_class }
+
def fake_object(fresh:)
object = double('object')
@@ -40,8 +42,6 @@ describe Banzai::Renderer do
end
describe '#render_field' do
- let(:renderer) { described_class }
-
context 'without cache' do
let(:commit) { fake_cacheless_object }
@@ -83,4 +83,57 @@ describe Banzai::Renderer do
end
end
end
+
+ describe '#post_process' do
+ let(:context_options) { {} }
+ let(:html) { 'Consequatur aperiam et nesciunt modi aut assumenda quo id. '}
+ let(:post_processed_html) { double(html_safe: 'safe doc') }
+ let(:doc) { double(to_html: post_processed_html) }
+
+ subject { renderer.post_process(html, context_options) }
+
+ context 'when xhtml' do
+ let(:context_options) { { xhtml: ' ' } }
+
+ context 'without :post_process_pipeline key' do
+ it 'uses PostProcessPipeline' do
+ expect(::Banzai::Pipeline::PostProcessPipeline).to receive(:to_document).and_return(doc)
+
+ subject
+ end
+ end
+
+ context 'with :post_process_pipeline key' do
+ let(:context_options) { { post_process_pipeline: Object, xhtml: ' ' } }
+
+ it 'uses passed post process pipeline' do
+ expect(Object).to receive(:to_document).and_return(doc)
+
+ subject
+ end
+ end
+ end
+
+ context 'when not xhtml' do
+ context 'without :post_process_pipeline key' do
+ it 'uses PostProcessPipeline' do
+ expect(::Banzai::Pipeline::PostProcessPipeline).to receive(:to_html)
+ .with(html, { only_path: true, disable_asset_proxy: true })
+ .and_return(post_processed_html)
+
+ subject
+ end
+ end
+
+ context 'with :post_process_pipeline key' do
+ let(:context_options) { { post_process_pipeline: Object } }
+
+ it 'uses passed post process pipeline' do
+ expect(Object).to receive(:to_html).and_return(post_processed_html)
+
+ subject
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index a0a8767637e..870f02b6933 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -715,6 +715,14 @@ describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
end
end
+ describe ".resource_bot_scopes" do
+ subject { described_class.resource_bot_scopes }
+
+ it { is_expected.to include(*described_class::API_SCOPES - [:read_user]) }
+ it { is_expected.to include(*described_class::REPOSITORY_SCOPES) }
+ it { is_expected.to include(*described_class.registry_scopes) }
+ end
+
private
def expect_results_with_abilities(personal_access_token, abilities, success = true)
diff --git a/spec/lib/gitlab/pagination/keyset_spec.rb b/spec/lib/gitlab/pagination/keyset_spec.rb
index bde280c5fca..0ac40080872 100644
--- a/spec/lib/gitlab/pagination/keyset_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset_spec.rb
@@ -3,6 +3,18 @@
require 'spec_helper'
describe Gitlab::Pagination::Keyset do
+ describe '.available_for_type?' do
+ subject { described_class }
+
+ it 'returns true for Project' do
+ expect(subject.available_for_type?(Project.all)).to be_truthy
+ end
+
+ it 'return false for other types of relations' do
+ expect(subject.available_for_type?(User.all)).to be_falsey
+ end
+ end
+
describe '.available?' do
subject { described_class }
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index e6544fda640..9aaaf536351 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -6,7 +6,7 @@ describe Milestone do
it_behaves_like 'a timebox', :milestone
describe 'MilestoneStruct#serializable_hash' do
- let(:predefined_milestone) { described_class::MilestoneStruct.new('Test Milestone', '#test', 1) }
+ let(:predefined_milestone) { described_class::TimeboxStruct.new('Test Milestone', '#test', 1) }
it 'presents the predefined milestone as a hash' do
expect(predefined_milestone.serializable_hash).to eq(
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index b16d1f58be5..596b11613b3 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -179,4 +179,27 @@ describe PersonalAccessToken do
end
end
end
+
+ describe '.simple_sorts' do
+ it 'includes overriden keys' do
+ expect(described_class.simple_sorts.keys).to include(*%w(expires_at_asc expires_at_desc))
+ end
+ end
+
+ describe 'ordering by expires_at' do
+ let_it_be(:earlier_token) { create(:personal_access_token, expires_at: 2.days.ago) }
+ let_it_be(:later_token) { create(:personal_access_token, expires_at: 1.day.ago) }
+
+ describe '.order_expires_at_asc' do
+ it 'returns ordered list in asc order of expiry date' do
+ expect(described_class.order_expires_at_asc).to match [earlier_token, later_token]
+ end
+ end
+
+ describe '.order_expires_at_desc' do
+ it 'returns ordered list in desc order of expiry date' do
+ expect(described_class.order_expires_at_desc).to match [later_token, earlier_token]
+ end
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index bcd28538e2c..8c2323eb0d8 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -6081,6 +6081,23 @@ describe Project do
end
end
+ describe '#bots' do
+ subject { project.bots }
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project_bot) { create(:user, :project_bot) }
+ let_it_be(:user) { create(:user) }
+
+ before_all do
+ [project_bot, user].each do |member|
+ project.add_maintainer(member)
+ end
+ end
+
+ it { is_expected.to contain_exactly(project_bot) }
+ it { is_expected.not_to include(user) }
+ end
+
def finish_job(export_job)
export_job.start
export_job.finish
diff --git a/spec/services/resources/create_access_token_service_spec.rb b/spec/services/resource_access_tokens/create_service_spec.rb
index 8c108d9937a..57e7e4e66de 100644
--- a/spec/services/resources/create_access_token_service_spec.rb
+++ b/spec/services/resource_access_tokens/create_service_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-describe Resources::CreateAccessTokenService do
- subject { described_class.new(resource_type, resource, user, params).execute }
+describe ResourceAccessTokens::CreateService do
+ subject { described_class.new(user, resource, params).execute }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :private) }
@@ -12,7 +12,7 @@ describe Resources::CreateAccessTokenService do
describe '#execute' do
# Created shared_examples as it will easy to include specs for group bots in https://gitlab.com/gitlab-org/gitlab/-/issues/214046
shared_examples 'fails when user does not have the permission to create a Resource Bot' do
- before do
+ before_all do
resource.add_developer(user)
end
@@ -56,7 +56,7 @@ describe Resources::CreateAccessTokenService do
end
context 'when user provides value' do
- let(:params) { { name: 'Random bot' } }
+ let_it_be(:params) { { name: 'Random bot' } }
it 'overrides the default value' do
response = subject
@@ -83,12 +83,12 @@ describe Resources::CreateAccessTokenService do
response = subject
access_token = response.payload[:access_token]
- expect(access_token.scopes).to eq(Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth.registry_scopes - [:read_user])
+ expect(access_token.scopes).to eq(Gitlab::Auth.resource_bot_scopes)
end
end
context 'when user provides scope explicitly' do
- let(:params) { { scopes: Gitlab::Auth::REPOSITORY_SCOPES } }
+ let_it_be(:params) { { scopes: Gitlab::Auth::REPOSITORY_SCOPES } }
it 'overrides the default value' do
response = subject
@@ -109,7 +109,7 @@ describe Resources::CreateAccessTokenService do
end
context 'when user provides value' do
- let(:params) { { expires_at: Date.today + 1.month } }
+ let_it_be(:params) { { expires_at: Date.today + 1.month } }
it 'overrides the default value' do
response = subject
@@ -120,7 +120,7 @@ describe Resources::CreateAccessTokenService do
end
context 'when invalid scope is passed' do
- let(:params) { { scopes: [:invalid_scope] } }
+ let_it_be(:params) { { scopes: [:invalid_scope] } }
it 'returns error' do
response = subject
@@ -145,14 +145,14 @@ describe Resources::CreateAccessTokenService do
end
context 'when resource is a project' do
- let(:resource_type) { 'project' }
- let(:resource) { project }
+ let_it_be(:resource_type) { 'project' }
+ let_it_be(:resource) { project }
it_behaves_like 'fails when user does not have the permission to create a Resource Bot'
it_behaves_like 'fails when flag is disabled'
context 'user with valid permission' do
- before do
+ before_all do
resource.add_maintainer(user)
end
diff --git a/spec/services/resource_access_tokens/revoke_service_spec.rb b/spec/services/resource_access_tokens/revoke_service_spec.rb
new file mode 100644
index 00000000000..3ce82745b9e
--- /dev/null
+++ b/spec/services/resource_access_tokens/revoke_service_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ResourceAccessTokens::RevokeService do
+ subject { described_class.new(user, resource, access_token).execute }
+
+ let_it_be(:user) { create(:user) }
+ let(:access_token) { create(:personal_access_token, user: resource_bot) }
+
+ describe '#execute' do
+ # Created shared_examples as it will easy to include specs for group bots in https://gitlab.com/gitlab-org/gitlab/-/issues/214046
+ shared_examples 'revokes access token' do
+ it { expect(subject.success?).to be true }
+
+ it { expect(subject.message).to eq("Revoked access token: #{access_token.name}") }
+
+ it 'revokes token access' do
+ subject
+
+ expect(access_token.reload.revoked?).to be true
+ end
+
+ it 'removes membership of bot user' do
+ subject
+
+ expect(resource.reload.users).not_to include(resource_bot)
+ end
+
+ it 'transfer issuables of bot user to ghost user' do
+ issue = create(:issue, author: resource_bot)
+
+ subject
+
+ expect(issue.reload.author.ghost?).to be true
+ end
+ end
+
+ shared_examples 'rollback revoke steps' do
+ it 'does not revoke the access token' do
+ subject
+
+ expect(access_token.reload.revoked?).to be false
+ end
+
+ it 'does not remove bot from member list' do
+ subject
+
+ expect(resource.reload.users).to include(resource_bot)
+ end
+
+ it 'does not transfer issuables of bot user to ghost user' do
+ issue = create(:issue, author: resource_bot)
+
+ subject
+
+ expect(issue.reload.author.ghost?).to be false
+ end
+ end
+
+ context 'when resource is a project' do
+ let_it_be(:resource) { create(:project, :private) }
+ let_it_be(:resource_bot) { create(:user, :project_bot) }
+
+ before_all do
+ resource.add_maintainer(user)
+ resource.add_maintainer(resource_bot)
+ end
+
+ it_behaves_like 'revokes access token'
+
+ context 'when revoke fails' do
+ context 'invalid resource type' do
+ subject { described_class.new(user, resource, access_token).execute }
+
+ let_it_be(:resource) { double }
+ let_it_be(:resource_bot) { create(:user, :project_bot) }
+
+ it 'returns error response' do
+ response = subject
+
+ expect(response.success?).to be false
+ expect(response.message).to eq("Failed to find bot user")
+ end
+
+ it { expect { subject }.not_to change(access_token.reload, :revoked) }
+ end
+
+ context 'when migration to ghost user fails' do
+ before do
+ allow_next_instance_of(::Members::DestroyService) do |service|
+ allow(service).to receive(:execute).and_return(false)
+ end
+ end
+
+ it_behaves_like 'rollback revoke steps'
+ end
+
+ context 'when migration to ghost user fails' do
+ before do
+ allow_next_instance_of(::Users::MigrateToGhostUserService) do |service|
+ allow(service).to receive(:execute).and_return(false)
+ end
+ end
+
+ it_behaves_like 'rollback revoke steps'
+ end
+ end
+ end
+ end
+end