Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2c90b9b579
commit
dc9ff5fda1
|
@ -11,7 +11,6 @@ import {
|
||||||
GlIntersectionObserver,
|
GlIntersectionObserver,
|
||||||
} from '@gitlab/ui';
|
} from '@gitlab/ui';
|
||||||
import { escapeRegExp } from 'lodash';
|
import { escapeRegExp } from 'lodash';
|
||||||
import filesQuery from 'shared_queries/repository/files.query.graphql';
|
|
||||||
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
|
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
|
||||||
import { escapeFileUrl } from '~/lib/utils/url_utility';
|
import { escapeFileUrl } from '~/lib/utils/url_utility';
|
||||||
import { TREE_PAGE_SIZE } from '~/repository/constants';
|
import { TREE_PAGE_SIZE } from '~/repository/constants';
|
||||||
|
@ -178,8 +177,7 @@ export default {
|
||||||
return this.isFolder ? this.loadFolder() : this.loadBlob();
|
return this.isFolder ? this.loadFolder() : this.loadBlob();
|
||||||
},
|
},
|
||||||
loadFolder() {
|
loadFolder() {
|
||||||
const query = this.glFeatures.paginatedTreeGraphqlQuery ? paginatedTreeQuery : filesQuery;
|
this.apolloQuery(paginatedTreeQuery, {
|
||||||
this.apolloQuery(query, {
|
|
||||||
projectPath: this.projectPath,
|
projectPath: this.projectPath,
|
||||||
ref: this.ref,
|
ref: this.ref,
|
||||||
path: this.path,
|
path: this.path,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import filesQuery from 'shared_queries/repository/files.query.graphql';
|
|
||||||
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
|
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
|
@ -72,9 +71,6 @@ export default {
|
||||||
hasShowMore() {
|
hasShowMore() {
|
||||||
return !this.clickedShowMore && this.pageLimitReached;
|
return !this.clickedShowMore && this.pageLimitReached;
|
||||||
},
|
},
|
||||||
paginatedTreeEnabled() {
|
|
||||||
return this.glFeatures.paginatedTreeGraphqlQuery;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -101,7 +97,7 @@ export default {
|
||||||
|
|
||||||
return this.$apollo
|
return this.$apollo
|
||||||
.query({
|
.query({
|
||||||
query: this.paginatedTreeEnabled ? paginatedTreeQuery : filesQuery,
|
query: paginatedTreeQuery,
|
||||||
variables: {
|
variables: {
|
||||||
projectPath: this.projectPath,
|
projectPath: this.projectPath,
|
||||||
ref: this.ref,
|
ref: this.ref,
|
||||||
|
@ -114,20 +110,19 @@ export default {
|
||||||
if (data.errors) throw data.errors;
|
if (data.errors) throw data.errors;
|
||||||
if (!data?.project?.repository || originalPath !== (this.path || '/')) return;
|
if (!data?.project?.repository || originalPath !== (this.path || '/')) return;
|
||||||
|
|
||||||
const pageInfo = this.paginatedTreeEnabled
|
const {
|
||||||
? data.project.repository.paginatedTree.pageInfo
|
project: {
|
||||||
: this.hasNextPage(data.project.repository.tree);
|
repository: {
|
||||||
|
paginatedTree: { pageInfo },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = data;
|
||||||
|
|
||||||
this.isLoadingFiles = false;
|
this.isLoadingFiles = false;
|
||||||
this.entries = Object.keys(this.entries).reduce(
|
this.entries = Object.keys(this.entries).reduce(
|
||||||
(acc, key) => ({
|
(acc, key) => ({
|
||||||
...acc,
|
...acc,
|
||||||
[key]: this.normalizeData(
|
[key]: this.normalizeData(key, data.project.repository.paginatedTree.nodes[0][key]),
|
||||||
key,
|
|
||||||
this.paginatedTreeEnabled
|
|
||||||
? data.project.repository.paginatedTree.nodes[0][key]
|
|
||||||
: data.project.repository.tree[key].edges,
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
@ -149,9 +144,7 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
normalizeData(key, data) {
|
normalizeData(key, data) {
|
||||||
return this.entries[key].concat(
|
return this.entries[key].concat(data.nodes);
|
||||||
this.paginatedTreeEnabled ? data.nodes : data.map(({ node }) => node),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
hasNextPage(data) {
|
hasNextPage(data) {
|
||||||
return []
|
return []
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import filesQuery from 'shared_queries/repository/files.query.graphql';
|
|
||||||
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
|
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
|
||||||
import projectPathQuery from '../queries/project_path.query.graphql';
|
import projectPathQuery from '../queries/project_path.query.graphql';
|
||||||
import getRefMixin from './get_ref';
|
import getRefMixin from './get_ref';
|
||||||
|
@ -22,7 +21,7 @@ export default {
|
||||||
|
|
||||||
return this.$apollo
|
return this.$apollo
|
||||||
.query({
|
.query({
|
||||||
query: gon.features.paginatedTreeGraphqlQuery ? paginatedTreeQuery : filesQuery,
|
query: paginatedTreeQuery,
|
||||||
variables: {
|
variables: {
|
||||||
projectPath: this.projectPath,
|
projectPath: this.projectPath,
|
||||||
ref: this.ref,
|
ref: this.ref,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlIcon } from '@gitlab/ui';
|
import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
|
||||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
import { HIGHLIGHT_CLASS_NAME } from './constants';
|
import { HIGHLIGHT_CLASS_NAME } from './constants';
|
||||||
import ViewerMixin from './mixins';
|
import ViewerMixin from './mixins';
|
||||||
|
@ -9,6 +9,9 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
GlIcon,
|
GlIcon,
|
||||||
},
|
},
|
||||||
|
directives: {
|
||||||
|
SafeHtml: GlSafeHtmlDirective,
|
||||||
|
},
|
||||||
mixins: [ViewerMixin, glFeatureFlagsMixin()],
|
mixins: [ViewerMixin, glFeatureFlagsMixin()],
|
||||||
inject: ['blobHash'],
|
inject: ['blobHash'],
|
||||||
data() {
|
data() {
|
||||||
|
@ -65,7 +68,7 @@ export default {
|
||||||
<div class="blob-content">
|
<div class="blob-content">
|
||||||
<pre
|
<pre
|
||||||
class="code highlight"
|
class="code highlight"
|
||||||
><code :data-blob-hash="blobHash" v-html="content /* eslint-disable-line vue/no-v-html */"></code></pre>
|
><code v-safe-html="content" :data-blob-hash="blobHash"></code></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,9 +5,6 @@ class Projects::ReleasesController < Projects::ApplicationController
|
||||||
before_action :require_non_empty_project, except: [:index]
|
before_action :require_non_empty_project, except: [:index]
|
||||||
before_action :release, only: %i[edit show update downloads]
|
before_action :release, only: %i[edit show update downloads]
|
||||||
before_action :authorize_read_release!
|
before_action :authorize_read_release!
|
||||||
# We have to check `download_code` permission because detail URL path
|
|
||||||
# contains git-tag name.
|
|
||||||
before_action :authorize_download_code!, except: [:index]
|
|
||||||
before_action :authorize_update_release!, only: %i[edit update]
|
before_action :authorize_update_release!, only: %i[edit update]
|
||||||
before_action :authorize_create_release!, only: :new
|
before_action :authorize_create_release!, only: :new
|
||||||
before_action only: :index do
|
before_action only: :index do
|
||||||
|
|
|
@ -17,7 +17,6 @@ class Projects::TreeController < Projects::ApplicationController
|
||||||
|
|
||||||
before_action do
|
before_action do
|
||||||
push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml)
|
push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml)
|
||||||
push_frontend_feature_flag(:paginated_tree_graphql_query, @project, default_enabled: :yaml)
|
|
||||||
push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml)
|
push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml)
|
||||||
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
|
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,7 +37,6 @@ class ProjectsController < Projects::ApplicationController
|
||||||
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
|
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
|
||||||
push_frontend_feature_flag(:refactor_text_viewer, @project, default_enabled: :yaml)
|
push_frontend_feature_flag(:refactor_text_viewer, @project, default_enabled: :yaml)
|
||||||
push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml)
|
push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml)
|
||||||
push_frontend_feature_flag(:paginated_tree_graphql_query, @project, default_enabled: :yaml)
|
|
||||||
push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml)
|
push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -135,11 +135,7 @@ module Issuables
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def label_link_query(target_model, label_ids: nil, label_names: nil)
|
def label_link_query(target_model, label_ids: nil, label_names: nil)
|
||||||
relation = LabelLink
|
relation = LabelLink.by_target_for_exists_query(target_model.name, target_model.arel_table['id'], label_ids)
|
||||||
.where(target_type: target_model.name)
|
|
||||||
.where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id']))
|
|
||||||
|
|
||||||
relation = relation.where(label_id: label_ids) if label_ids
|
|
||||||
relation = relation.joins(:label).where(labels: { name: label_names }) if label_names
|
relation = relation.joins(:label).where(labels: { name: label_names }) if label_names
|
||||||
|
|
||||||
relation
|
relation
|
||||||
|
|
|
@ -306,7 +306,7 @@ module Types
|
||||||
null: true,
|
null: true,
|
||||||
description: 'A single release of the project.',
|
description: 'A single release of the project.',
|
||||||
resolver: Resolvers::ReleasesResolver.single,
|
resolver: Resolvers::ReleasesResolver.single,
|
||||||
authorize: :download_code
|
authorize: :read_release
|
||||||
|
|
||||||
field :container_expiration_policy,
|
field :container_expiration_policy,
|
||||||
Types::ContainerExpirationPolicyType,
|
Types::ContainerExpirationPolicyType,
|
||||||
|
|
|
@ -4,7 +4,7 @@ module Types
|
||||||
class ReleaseLinksType < BaseObject
|
class ReleaseLinksType < BaseObject
|
||||||
graphql_name 'ReleaseLinks'
|
graphql_name 'ReleaseLinks'
|
||||||
|
|
||||||
authorize :download_code
|
authorize :read_release
|
||||||
|
|
||||||
alias_method :release, :object
|
alias_method :release, :object
|
||||||
|
|
||||||
|
@ -16,14 +16,19 @@ module Types
|
||||||
description: "HTTP URL of the release's edit page.",
|
description: "HTTP URL of the release's edit page.",
|
||||||
authorize: :update_release
|
authorize: :update_release
|
||||||
field :opened_merge_requests_url, GraphQL::Types::String, null: true,
|
field :opened_merge_requests_url, GraphQL::Types::String, null: true,
|
||||||
description: 'HTTP URL of the merge request page, filtered by this release and `state=open`.'
|
description: 'HTTP URL of the merge request page, filtered by this release and `state=open`.',
|
||||||
|
authorize: :download_code
|
||||||
field :merged_merge_requests_url, GraphQL::Types::String, null: true,
|
field :merged_merge_requests_url, GraphQL::Types::String, null: true,
|
||||||
description: 'HTTP URL of the merge request page , filtered by this release and `state=merged`.'
|
description: 'HTTP URL of the merge request page , filtered by this release and `state=merged`.',
|
||||||
|
authorize: :download_code
|
||||||
field :closed_merge_requests_url, GraphQL::Types::String, null: true,
|
field :closed_merge_requests_url, GraphQL::Types::String, null: true,
|
||||||
description: 'HTTP URL of the merge request page , filtered by this release and `state=closed`.'
|
description: 'HTTP URL of the merge request page , filtered by this release and `state=closed`.',
|
||||||
|
authorize: :download_code
|
||||||
field :opened_issues_url, GraphQL::Types::String, null: true,
|
field :opened_issues_url, GraphQL::Types::String, null: true,
|
||||||
description: 'HTTP URL of the issues page, filtered by this release and `state=open`.'
|
description: 'HTTP URL of the issues page, filtered by this release and `state=open`.',
|
||||||
|
authorize: :download_code
|
||||||
field :closed_issues_url, GraphQL::Types::String, null: true,
|
field :closed_issues_url, GraphQL::Types::String, null: true,
|
||||||
description: 'HTTP URL of the issues page, filtered by this release and `state=closed`.'
|
description: 'HTTP URL of the issues page, filtered by this release and `state=closed`.',
|
||||||
|
authorize: :download_code
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,8 +14,7 @@ module Types
|
||||||
present_using ReleasePresenter
|
present_using ReleasePresenter
|
||||||
|
|
||||||
field :tag_name, GraphQL::Types::String, null: true, method: :tag,
|
field :tag_name, GraphQL::Types::String, null: true, method: :tag,
|
||||||
description: 'Name of the tag associated with the release.',
|
description: 'Name of the tag associated with the release.'
|
||||||
authorize: :download_code
|
|
||||||
field :tag_path, GraphQL::Types::String, null: true,
|
field :tag_path, GraphQL::Types::String, null: true,
|
||||||
description: 'Relative web path to the tag associated with the release.',
|
description: 'Relative web path to the tag associated with the release.',
|
||||||
authorize: :download_code
|
authorize: :download_code
|
||||||
|
|
|
@ -16,8 +16,7 @@ module Types
|
||||||
description: 'Tree of the repository.'
|
description: 'Tree of the repository.'
|
||||||
field :paginated_tree, Types::Tree::TreeType.connection_type, null: true, resolver: Resolvers::PaginatedTreeResolver, calls_gitaly: true,
|
field :paginated_tree, Types::Tree::TreeType.connection_type, null: true, resolver: Resolvers::PaginatedTreeResolver, calls_gitaly: true,
|
||||||
max_page_size: 100,
|
max_page_size: 100,
|
||||||
description: 'Paginated tree of the repository.',
|
description: 'Paginated tree of the repository.'
|
||||||
feature_flag: :paginated_tree_graphql_query
|
|
||||||
field :blobs, Types::Repository::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true,
|
field :blobs, Types::Repository::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true,
|
||||||
description: 'Blobs contained within the repository'
|
description: 'Blobs contained within the repository'
|
||||||
field :branch_names, [GraphQL::Types::String], null: true, calls_gitaly: true,
|
field :branch_names, [GraphQL::Types::String], null: true, calls_gitaly: true,
|
||||||
|
|
|
@ -20,6 +20,10 @@ module Analytics
|
||||||
def self.issuable_id_column
|
def self.issuable_id_column
|
||||||
:issue_id
|
:issue_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.issuable_model
|
||||||
|
::Issue
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,6 +20,10 @@ module Analytics
|
||||||
def self.issuable_id_column
|
def self.issuable_id_column
|
||||||
:merge_request_id
|
:merge_request_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.issuable_model
|
||||||
|
::MergeRequest
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,6 +18,10 @@ module Analytics
|
||||||
scope :end_event_is_not_happened_yet, -> { where(end_event_timestamp: nil) }
|
scope :end_event_is_not_happened_yet, -> { where(end_event_timestamp: nil) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def issuable_id
|
||||||
|
attributes[self.class.issuable_id_column.to_s]
|
||||||
|
end
|
||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def upsert_data(data)
|
def upsert_data(data)
|
||||||
upsert_values = data.map do |row|
|
upsert_values = data.map do |row|
|
||||||
|
@ -26,8 +30,8 @@ module Analytics
|
||||||
:issuable_id,
|
:issuable_id,
|
||||||
:group_id,
|
:group_id,
|
||||||
:project_id,
|
:project_id,
|
||||||
:author_id,
|
|
||||||
:milestone_id,
|
:milestone_id,
|
||||||
|
:author_id,
|
||||||
:state_id,
|
:state_id,
|
||||||
:start_event_timestamp,
|
:start_event_timestamp,
|
||||||
:end_event_timestamp
|
:end_event_timestamp
|
||||||
|
|
|
@ -11,4 +11,16 @@ class LabelLink < ApplicationRecord
|
||||||
validates :label, presence: true, unless: :importing?
|
validates :label, presence: true, unless: :importing?
|
||||||
|
|
||||||
scope :for_target, -> (target_id, target_type) { where(target_id: target_id, target_type: target_type) }
|
scope :for_target, -> (target_id, target_type) { where(target_id: target_id, target_type: target_type) }
|
||||||
|
|
||||||
|
# Example: Issues has at least one label within a project
|
||||||
|
# > Issue.where(project_id: 100) # or any scope on issues
|
||||||
|
# > .where(LabelLink.by_target_for_exists_query('Issue', Issue.arel_table[:id]).arel.exists)
|
||||||
|
scope :by_target_for_exists_query, -> (target_type, arel_join_column, label_ids = nil) do
|
||||||
|
relation = LabelLink
|
||||||
|
.where(target_type: target_type)
|
||||||
|
.where(arel_table['target_id'].eq(arel_join_column))
|
||||||
|
|
||||||
|
relation = relation.where(label_id: label_ids) if label_ids
|
||||||
|
relation
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
class WebauthnRegistration < ApplicationRecord
|
class WebauthnRegistration < ApplicationRecord
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
validates :credential_xid, :public_key, :name, :counter, presence: true
|
validates :credential_xid, :public_key, :counter, presence: true
|
||||||
|
validates :name, length: { minimum: 0, allow_nil: false }
|
||||||
validates :counter,
|
validates :counter,
|
||||||
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 }
|
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 }
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,8 +22,6 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
|
||||||
end
|
end
|
||||||
|
|
||||||
def self_url
|
def self_url
|
||||||
return unless can_download_code?
|
|
||||||
|
|
||||||
project_release_url(project, release)
|
project_release_url(project, release)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -64,7 +62,7 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
|
||||||
|
|
||||||
delegator_override :name
|
delegator_override :name
|
||||||
def name
|
def name
|
||||||
can_download_code? ? release.name : "Release-#{release.id}"
|
release.name
|
||||||
end
|
end
|
||||||
|
|
||||||
def download_url(filepath)
|
def download_url(filepath)
|
||||||
|
|
|
@ -4,6 +4,8 @@ module Packages
|
||||||
class CreatePackageService < ::Packages::CreatePackageService
|
class CreatePackageService < ::Packages::CreatePackageService
|
||||||
include Gitlab::Utils::StrongMemoize
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
|
PACKAGE_JSON_NOT_ALLOWED_FIELDS = %w[readme readmeFilename].freeze
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
return error('Version is empty.', 400) if version.blank?
|
return error('Version is empty.', 400) if version.blank?
|
||||||
return error('Package already exists.', 403) if current_package_exists?
|
return error('Package already exists.', 403) if current_package_exists?
|
||||||
|
@ -22,7 +24,7 @@ module Packages
|
||||||
::Packages::Npm::CreateTagService.new(package, dist_tag).execute
|
::Packages::Npm::CreateTagService.new(package, dist_tag).execute
|
||||||
|
|
||||||
if Feature.enabled?(:packages_npm_abbreviated_metadata, project, default_enabled: :yaml)
|
if Feature.enabled?(:packages_npm_abbreviated_metadata, project, default_enabled: :yaml)
|
||||||
package.create_npm_metadatum!(package_json: version_data)
|
package.create_npm_metadatum!(package_json: package_json)
|
||||||
end
|
end
|
||||||
|
|
||||||
package
|
package
|
||||||
|
@ -50,6 +52,10 @@ module Packages
|
||||||
params[:versions][version]
|
params[:versions][version]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def package_json
|
||||||
|
version_data.except(*PACKAGE_JSON_NOT_ALLOWED_FIELDS)
|
||||||
|
end
|
||||||
|
|
||||||
def dist_tag
|
def dist_tag
|
||||||
params['dist-tags'].each_key.first
|
params['dist-tags'].each_key.first
|
||||||
end
|
end
|
||||||
|
|
|
@ -224,7 +224,7 @@ module Gitlab
|
||||||
config.assets.precompile << "page_bundles/build.css"
|
config.assets.precompile << "page_bundles/build.css"
|
||||||
config.assets.precompile << "page_bundles/ci_status.css"
|
config.assets.precompile << "page_bundles/ci_status.css"
|
||||||
config.assets.precompile << "page_bundles/cycle_analytics.css"
|
config.assets.precompile << "page_bundles/cycle_analytics.css"
|
||||||
config.assets.precompile << "page_bundles/dev_ops_report.css"
|
config.assets.precompile << "page_bundles/dev_ops_reports.css"
|
||||||
config.assets.precompile << "page_bundles/environments.css"
|
config.assets.precompile << "page_bundles/environments.css"
|
||||||
config.assets.precompile << "page_bundles/epics.css"
|
config.assets.precompile << "page_bundles/epics.css"
|
||||||
config.assets.precompile << "page_bundles/error_tracking_details.css"
|
config.assets.precompile << "page_bundles/error_tracking_details.css"
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
---
|
|
||||||
name: paginated_tree_graphql_query
|
|
||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66751
|
|
||||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337214
|
|
||||||
milestone: '14.2'
|
|
||||||
type: development
|
|
||||||
group: group::source code
|
|
||||||
default_enabled: true
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RemoveCiPipelineChatDataFkOnChatNames < Gitlab::Database::Migration[1.0]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
with_lock_retries do
|
||||||
|
remove_foreign_key_if_exists(:ci_pipeline_chat_data, :chat_names, name: "fk_rails_f300456b63")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
# Remove orphaned rows
|
||||||
|
execute <<~SQL
|
||||||
|
DELETE FROM ci_pipeline_chat_data
|
||||||
|
WHERE
|
||||||
|
NOT EXISTS (SELECT 1 FROM chat_names WHERE chat_names.id=ci_pipeline_chat_data.chat_name_id)
|
||||||
|
SQL
|
||||||
|
|
||||||
|
add_concurrent_foreign_key(:ci_pipeline_chat_data, :chat_names, name: "fk_rails_f300456b63", column: :chat_name_id, target_column: :id, on_delete: "cascade")
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1 @@
|
||||||
|
be11c0b1c7b9c99c28d44c164742815da57bfc4a32afd54df9135e3ce6edeff9
|
|
@ -31078,9 +31078,6 @@ ALTER TABLE ONLY snippet_repositories
|
||||||
ALTER TABLE ONLY elastic_reindexing_subtasks
|
ALTER TABLE ONLY elastic_reindexing_subtasks
|
||||||
ADD CONSTRAINT fk_rails_f2cc190164 FOREIGN KEY (elastic_reindexing_task_id) REFERENCES elastic_reindexing_tasks(id) ON DELETE CASCADE;
|
ADD CONSTRAINT fk_rails_f2cc190164 FOREIGN KEY (elastic_reindexing_task_id) REFERENCES elastic_reindexing_tasks(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
ALTER TABLE ONLY ci_pipeline_chat_data
|
|
||||||
ADD CONSTRAINT fk_rails_f300456b63 FOREIGN KEY (chat_name_id) REFERENCES chat_names(id) ON DELETE CASCADE;
|
|
||||||
|
|
||||||
ALTER TABLE ONLY approval_project_rules_users
|
ALTER TABLE ONLY approval_project_rules_users
|
||||||
ADD CONSTRAINT fk_rails_f365da8250 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
ADD CONSTRAINT fk_rails_f365da8250 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
|
|
@ -14030,7 +14030,7 @@ Returns [`[String!]`](#string).
|
||||||
|
|
||||||
##### `Repository.paginatedTree`
|
##### `Repository.paginatedTree`
|
||||||
|
|
||||||
Paginated tree of the repository. Available only when feature flag `paginated_tree_graphql_query` is enabled. This flag is enabled by default.
|
Paginated tree of the repository.
|
||||||
|
|
||||||
Returns [`TreeConnection`](#treeconnection).
|
Returns [`TreeConnection`](#treeconnection).
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,7 @@ The following are example projects that demonstrate Review App configuration:
|
||||||
|
|
||||||
- [NGINX](https://gitlab.com/gitlab-examples/review-apps-nginx).
|
- [NGINX](https://gitlab.com/gitlab-examples/review-apps-nginx).
|
||||||
- [OpenShift](https://gitlab.com/gitlab-examples/review-apps-openshift).
|
- [OpenShift](https://gitlab.com/gitlab-examples/review-apps-openshift).
|
||||||
- [HashiCorp Nomad](https://gitlab.com/gitlab-examples/review-apps-nomad)
|
- [HashiCorp Nomad](https://gitlab.com/gitlab-examples/review-apps-nomad).
|
||||||
|
|
||||||
Other examples of Review Apps:
|
Other examples of Review Apps:
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,9 @@ use external test planning tools, which require additional overhead, context swi
|
||||||
|
|
||||||
## Create a test case
|
## Create a test case
|
||||||
|
|
||||||
Users with Reporter or higher [permissions](../../user/permissions.md) can create test cases.
|
Prerequisite:
|
||||||
|
|
||||||
|
- You must have at least the Reporter [role](../../user/permissions.md).
|
||||||
|
|
||||||
To create a test case in a GitLab project:
|
To create a test case in a GitLab project:
|
||||||
|
|
||||||
|
@ -32,7 +34,9 @@ To create a test case in a GitLab project:
|
||||||
You can view all test cases in the project in the Test Cases list. Filter the
|
You can view all test cases in the project in the Test Cases list. Filter the
|
||||||
issue list with a search query, including labels or the test case's title.
|
issue list with a search query, including labels or the test case's title.
|
||||||
|
|
||||||
Users with Guest or higher [permissions](../../user/permissions.md) can view test cases.
|
Prerequisite:
|
||||||
|
|
||||||
|
- You must have at least the Guest [role](../../user/permissions.md).
|
||||||
|
|
||||||
To view a test case:
|
To view a test case:
|
||||||
|
|
||||||
|
@ -45,8 +49,10 @@ To view a test case:
|
||||||
|
|
||||||
You can edit a test case's title and description.
|
You can edit a test case's title and description.
|
||||||
|
|
||||||
Users with Reporter or higher [permissions](../../user/permissions.md) can edit test cases.
|
Prerequisite:
|
||||||
Users demoted to the Guest role can continue to edit the test cases they created
|
|
||||||
|
- You must have at least the Reporter [role](../../user/permissions.md).
|
||||||
|
- Users demoted to the Guest role can continue to edit the test cases they created
|
||||||
when they were in the higher role.
|
when they were in the higher role.
|
||||||
|
|
||||||
To edit a test case:
|
To edit a test case:
|
||||||
|
@ -60,7 +66,9 @@ To edit a test case:
|
||||||
|
|
||||||
When you want to stop using a test case, you can archive it. You can [reopen an archived test case](#reopen-an-archived-test-case) later.
|
When you want to stop using a test case, you can archive it. You can [reopen an archived test case](#reopen-an-archived-test-case) later.
|
||||||
|
|
||||||
Users with Reporter or higher [permissions](../../user/permissions.md) can archive test cases.
|
Prerequisite:
|
||||||
|
|
||||||
|
- You must have at least the Reporter [role](../../user/permissions.md).
|
||||||
|
|
||||||
To archive a test case, on the test case's page, select the **Archive test case** button.
|
To archive a test case, on the test case's page, select the **Archive test case** button.
|
||||||
|
|
||||||
|
@ -73,6 +81,6 @@ To view archived test cases:
|
||||||
|
|
||||||
If you decide to start using an archived test case again, you can reopen it.
|
If you decide to start using an archived test case again, you can reopen it.
|
||||||
|
|
||||||
Users with Reporter or higher [permissions](../../user/permissions.md) can reopen test cases.
|
You must have at least the Reporter [role](../../user/permissions.md).
|
||||||
|
|
||||||
To reopen an archived test case, on the test case's page, select **Reopen test case**.
|
To reopen an archived test case, on the test case's page, select **Reopen test case**.
|
||||||
|
|
|
@ -90,7 +90,7 @@ The following examples show the progression of a feature flag.
|
||||||
|
|
||||||
FLAG:
|
FLAG:
|
||||||
On self-managed GitLab, by default this feature is not available. To make it available,
|
On self-managed GitLab, by default this feature is not available. To make it available,
|
||||||
ask an administrator to [enable the featured flag](../administration/feature_flags.md) named `forti_token_cloud`.
|
ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `forti_token_cloud`.
|
||||||
The feature is not ready for production use.
|
The feature is not ready for production use.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,17 @@ they found that "ARIA correlated to higher detectable errors".
|
||||||
It is likely that *misuse* of ARIA is a big cause of increased errors,
|
It is likely that *misuse* of ARIA is a big cause of increased errors,
|
||||||
so when in doubt don't use `aria-*`, `role`, and `tabindex` and stick with semantic HTML.
|
so when in doubt don't use `aria-*`, `role`, and `tabindex` and stick with semantic HTML.
|
||||||
|
|
||||||
|
## Enable keyboard navigation on macOS
|
||||||
|
|
||||||
|
By default, macOS limits the <kbd>tab</kbd> key to **Text boxes and lists only**. To enable full keyboard navigation:
|
||||||
|
|
||||||
|
1. Open **System Preferences**.
|
||||||
|
1. Select **Keyboard**.
|
||||||
|
1. Open the **Shortcuts** tab.
|
||||||
|
1. Enable the setting **Use keyboard navigation to move focus between controls**.
|
||||||
|
|
||||||
|
You can read more about enabling browser-specific keyboard navigation on [a11yproject](https://www.a11yproject.com/posts/2017-12-29-macos-browser-keyboard-navigation/).
|
||||||
|
|
||||||
## Quick checklist
|
## Quick checklist
|
||||||
|
|
||||||
- [Text](#text-inputs-with-accessible-names),
|
- [Text](#text-inputs-with-accessible-names),
|
||||||
|
|
|
@ -338,6 +338,23 @@ or [init scripts](upgrading_from_source.md#configure-sysv-init-script) by [follo
|
||||||
Workhorse can no longer connect. As a workaround, [disable the temporary `workhorse_use_sidechannel`](../administration/feature_flags.md#enable-or-disable-the-feature)
|
Workhorse can no longer connect. As a workaround, [disable the temporary `workhorse_use_sidechannel`](../administration/feature_flags.md#enable-or-disable-the-feature)
|
||||||
feature flag. If you need a proxy between Workhorse and Gitaly, use a TCP proxy. If you have feedback about this change, please go to [this issue](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1301).
|
feature flag. If you need a proxy between Workhorse and Gitaly, use a TCP proxy. If you have feedback about this change, please go to [this issue](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1301).
|
||||||
|
|
||||||
|
- In 14.1 we introduced a background migration that changes how we store merge request diff commits
|
||||||
|
in order to significantly reduce the amount of storage needed.
|
||||||
|
In 14.5 we introduce a set of migrations that wrap up this process by making sure
|
||||||
|
that all remaining jobs over the `merge_request_diff_commits` table are completed.
|
||||||
|
These jobs will have already been processed in most cases so that no extra time is necessary during an upgrade to 14.5.
|
||||||
|
But if there are remaining jobs, the deployment may take a few extra minutes to complete.
|
||||||
|
|
||||||
|
All merge request diff commits will automatically incorporate these changes, and there are no
|
||||||
|
additional requirements to perform the upgrade.
|
||||||
|
Existing data in the `merge_request_diff_commits` table remains unpacked until you run `VACUUM FULL merge_request_diff_commits`.
|
||||||
|
But note that the `VACUUM FULL` operation locks and rewrites the entire `merge_request_diff_commits` table,
|
||||||
|
so the operation takes some time to complete and it blocks access to this table until the end of the process.
|
||||||
|
We advise you to only run this command while GitLab is not actively used or it is taken offline for the duration of the process.
|
||||||
|
The time it takes to complete depends on the size of the table, which can be obtained by using `select pg_size_pretty(pg_total_relation_size('merge_request_diff_commits'));`.
|
||||||
|
|
||||||
|
For more information, refer to [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/331823).
|
||||||
|
|
||||||
### 14.4.0
|
### 14.4.0
|
||||||
|
|
||||||
Git 2.33.x and later is required. We recommend you use the
|
Git 2.33.x and later is required. We recommend you use the
|
||||||
|
|
|
@ -70,7 +70,8 @@ synchronizations is:
|
||||||
```yaml
|
```yaml
|
||||||
gitops:
|
gitops:
|
||||||
manifest_projects:
|
manifest_projects:
|
||||||
- id: "path-to/your-manifest-project-1"
|
# The `id` is the path to the Git repository holding your manifest files
|
||||||
|
- id: "path/to/your-manifest-project-1"
|
||||||
paths:
|
paths:
|
||||||
- glob: '/**/*.{yaml,yml,json}'
|
- glob: '/**/*.{yaml,yml,json}'
|
||||||
```
|
```
|
||||||
|
|
|
@ -298,6 +298,7 @@ after the limits change in January, 2021:
|
||||||
| **All** traffic (from a given **IP address**) | **600** requests per minute | **2,000** requests per minute | **2,000** requests per minute |
|
| **All** traffic (from a given **IP address**) | **600** requests per minute | **2,000** requests per minute | **2,000** requests per minute |
|
||||||
| **Issue creation** | | **300** requests per minute | **300** requests per minute |
|
| **Issue creation** | | **300** requests per minute | **300** requests per minute |
|
||||||
| **Note creation** (on issues and merge requests) | | **300** requests per minute | **60** requests per minute |
|
| **Note creation** (on issues and merge requests) | | **300** requests per minute | **60** requests per minute |
|
||||||
|
| **Advanced, project, and group search** API (for a given **IP address**) | | | **10** requests per minute |
|
||||||
|
|
||||||
More details are available on the rate limits for [protected
|
More details are available on the rate limits for [protected
|
||||||
paths](#protected-paths-throttle) and [raw
|
paths](#protected-paths-throttle) and [raw
|
||||||
|
|
|
@ -216,7 +216,7 @@ The following table lists project permissions available for each role:
|
||||||
1. If **Public pipelines** is enabled in **Project Settings > CI/CD**.
|
1. If **Public pipelines** is enabled in **Project Settings > CI/CD**.
|
||||||
1. Not allowed for Guest, Reporter, Developer, Maintainer, or Owner. See [protected branches](project/protected_branches.md).
|
1. Not allowed for Guest, Reporter, Developer, Maintainer, or Owner. See [protected branches](project/protected_branches.md).
|
||||||
1. If the [branch is protected](project/protected_branches.md), this depends on the access Developers and Maintainers are given.
|
1. If the [branch is protected](project/protected_branches.md), this depends on the access Developers and Maintainers are given.
|
||||||
1. Guest users can access GitLab [**Releases**](project/releases/index.md) for downloading assets but are not allowed to download the source code nor see repository information like tags and commits.
|
1. Guest users can access GitLab [**Releases**](project/releases/index.md) for downloading assets but are not allowed to download the source code nor see [repository information like commits and release evidence](project/releases/index.md#view-a-release-and-download-assets).
|
||||||
1. Actions are limited only to records owned (referenced) by user.
|
1. Actions are limited only to records owned (referenced) by user.
|
||||||
1. When [Share Group Lock](group/index.md#prevent-a-project-from-being-shared-with-groups) is enabled the project can't be shared with other groups. It does not affect group with group sharing.
|
1. When [Share Group Lock](group/index.md#prevent-a-project-from-being-shared-with-groups) is enabled the project can't be shared with other groups. It does not affect group with group sharing.
|
||||||
1. For information on eligible approvers for merge requests, see
|
1. For information on eligible approvers for merge requests, see
|
||||||
|
|
|
@ -393,14 +393,6 @@ The release title can be customized using the **Release title** field when
|
||||||
creating or editing a release. If no title is provided, the release's tag name
|
creating or editing a release. If no title is provided, the release's tag name
|
||||||
is used instead.
|
is used instead.
|
||||||
|
|
||||||
Guest users of private projects are allowed to view the **Releases** page
|
|
||||||
but are _not_ allowed to view details about the Git repository (in particular,
|
|
||||||
tag names). Because of this, release titles are replaced with a generic
|
|
||||||
title like "Release-1234" for Guest users to avoid leaking tag name information.
|
|
||||||
|
|
||||||
See the [Release permissions](#release-permissions) section for
|
|
||||||
more information about permissions.
|
|
||||||
|
|
||||||
### Tag name
|
### Tag name
|
||||||
|
|
||||||
The release tag name should include the release version. GitLab uses [Semantic Versioning](https://semver.org/)
|
The release tag name should include the release version. GitLab uses [Semantic Versioning](https://semver.org/)
|
||||||
|
@ -724,11 +716,14 @@ In the API:
|
||||||
|
|
||||||
### View a release and download assets
|
### View a release and download assets
|
||||||
|
|
||||||
|
> [The Guest permission for read action was adjusted](https://gitlab.com/gitlab-org/gitlab/-/issues/335209) in GitLab 14.5.
|
||||||
|
|
||||||
- Users with [Reporter role or above](../../../user/permissions.md#project-members-permissions)
|
- Users with [Reporter role or above](../../../user/permissions.md#project-members-permissions)
|
||||||
have read and download access to the project releases.
|
have read and download access to the project releases.
|
||||||
- Users with [Guest role](../../../user/permissions.md#project-members-permissions)
|
- Users with [Guest role](../../../user/permissions.md#project-members-permissions)
|
||||||
have read and download access to the project releases, however,
|
have read and download access to the project releases.
|
||||||
repository-related information are redacted (for example the Git tag name).
|
This includes associated Git-tag-names, release description, author information of the releases.
|
||||||
|
However, other repository-related information, such as [source code](#source-code), [release evidence](#release-evidence) are redacted.
|
||||||
|
|
||||||
### Create, update, and delete a release and its assets
|
### Create, update, and delete a release and its assets
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ For the latest updates, check the [Tasks Roadmap](https://gitlab.com/groups/gitl
|
||||||
|
|
||||||
FLAG:
|
FLAG:
|
||||||
On self-managed GitLab, by default this feature is not available. To make it available,
|
On self-managed GitLab, by default this feature is not available. To make it available,
|
||||||
ask an administrator to [enable the featured flag](../administration/feature_flags.md) named `work_items`.
|
ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `work_items`.
|
||||||
The feature is not ready for production use.
|
The feature is not ready for production use.
|
||||||
|
|
||||||
Use tasks to track steps needed for the [issue](project/issues/index.md) to be closed.
|
Use tasks to track steps needed for the [issue](project/issues/index.md) to be closed.
|
||||||
|
|
|
@ -63,23 +63,12 @@ module Gitlab
|
||||||
def filter_label_names(query)
|
def filter_label_names(query)
|
||||||
return query if params[:label_name].blank?
|
return query if params[:label_name].blank?
|
||||||
|
|
||||||
all_label_ids = Issuables::LabelFilter
|
LabelFilter.new(
|
||||||
.new(group: root_ancestor, project: nil, params: { label_name: params[:label_name] })
|
stage: stage,
|
||||||
.find_label_ids(params[:label_name])
|
params: params,
|
||||||
|
project: nil,
|
||||||
return query.none if params[:label_name].size != all_label_ids.size
|
group: root_ancestor
|
||||||
|
).filter(query)
|
||||||
all_label_ids.each do |label_ids|
|
|
||||||
relation = LabelLink
|
|
||||||
.where(target_type: stage.subject_class.name)
|
|
||||||
.where(LabelLink.arel_table['target_id'].eq(query.model.arel_table[query.model.issuable_id_column]))
|
|
||||||
|
|
||||||
relation = relation.where(label_id: label_ids)
|
|
||||||
|
|
||||||
query = query.where(relation.arel.exists)
|
|
||||||
end
|
|
||||||
|
|
||||||
query
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_assignees(query)
|
def filter_assignees(query)
|
||||||
|
|
|
@ -30,6 +30,12 @@ module Gitlab
|
||||||
strong_memoize(:count) { limit_count }
|
strong_memoize(:count) { limit_count }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def records_fetcher
|
||||||
|
strong_memoize(:records_fetcher) do
|
||||||
|
RecordsFetcher.new(stage: stage, query: query, params: params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
attr_reader :stage, :params
|
attr_reader :stage, :params
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Analytics
|
||||||
|
module CycleAnalytics
|
||||||
|
module Aggregated
|
||||||
|
# This class makes it possible to add label filters to stage event tables
|
||||||
|
class LabelFilter < Issuables::LabelFilter
|
||||||
|
extend ::Gitlab::Utils::Override
|
||||||
|
|
||||||
|
def initialize(stage:, project:, group:, **kwargs)
|
||||||
|
@stage = stage
|
||||||
|
|
||||||
|
super(project: project, group: group, **kwargs)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :stage
|
||||||
|
|
||||||
|
override :label_link_query
|
||||||
|
def label_link_query(target_model, label_ids: nil)
|
||||||
|
join_column = target_model.arel_table[target_model.issuable_id_column]
|
||||||
|
|
||||||
|
LabelLink.by_target_for_exists_query(stage.subject_class.name, join_column, label_ids)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,116 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Analytics
|
||||||
|
module CycleAnalytics
|
||||||
|
module Aggregated
|
||||||
|
class RecordsFetcher
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
include StageQueryHelpers
|
||||||
|
|
||||||
|
MAX_RECORDS = 20
|
||||||
|
|
||||||
|
MAPPINGS = {
|
||||||
|
Issue => {
|
||||||
|
serializer_class: AnalyticsIssueSerializer,
|
||||||
|
includes_for_query: { project: { namespace: [:route] }, author: [] },
|
||||||
|
columns_for_select: %I[title iid id created_at author_id project_id]
|
||||||
|
},
|
||||||
|
MergeRequest => {
|
||||||
|
serializer_class: AnalyticsMergeRequestSerializer,
|
||||||
|
includes_for_query: { target_project: [:namespace], author: [] },
|
||||||
|
columns_for_select: %I[title iid id created_at author_id state_id target_project_id]
|
||||||
|
}
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def initialize(stage:, query:, params: {})
|
||||||
|
@stage = stage
|
||||||
|
@query = query
|
||||||
|
@params = params
|
||||||
|
@sort = params[:sort] || :end_event
|
||||||
|
@direction = params[:direction] || :desc
|
||||||
|
@page = params[:page] || 1
|
||||||
|
@per_page = MAX_RECORDS
|
||||||
|
@stage_event_model = query.model
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialized_records
|
||||||
|
strong_memoize(:serialized_records) do
|
||||||
|
records = ordered_and_limited_query.select(stage_event_model.arel_table[Arel.star], duration.as('total_time'))
|
||||||
|
|
||||||
|
yield records if block_given?
|
||||||
|
issuables_and_records = load_issuables(records)
|
||||||
|
|
||||||
|
preload_associations(issuables_and_records.map(&:first))
|
||||||
|
|
||||||
|
issuables_and_records.map do |issuable, record|
|
||||||
|
project = issuable.project
|
||||||
|
attributes = issuable.attributes.merge({
|
||||||
|
project_path: project.path,
|
||||||
|
namespace_path: project.namespace.route.path,
|
||||||
|
author: issuable.author,
|
||||||
|
total_time: record.total_time
|
||||||
|
})
|
||||||
|
serializer.represent(attributes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
def ordered_and_limited_query
|
||||||
|
sorting_options = {
|
||||||
|
end_event: {
|
||||||
|
asc: -> { query.order(end_event_timestamp: :asc) },
|
||||||
|
desc: -> { query.order(end_event_timestamp: :desc) }
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
asc: -> { query.order(duration.asc) },
|
||||||
|
desc: -> { query.order(duration.desc) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort_lambda = sorting_options.dig(sort, direction) || sorting_options.dig(:end_event, :desc)
|
||||||
|
|
||||||
|
sort_lambda.call
|
||||||
|
.page(page)
|
||||||
|
.per(per_page)
|
||||||
|
.without_count
|
||||||
|
end
|
||||||
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :stage, :query, :sort, :direction, :params, :page, :per_page, :stage_event_model
|
||||||
|
|
||||||
|
delegate :subject_class, to: :stage
|
||||||
|
|
||||||
|
def load_issuables(stage_event_records)
|
||||||
|
stage_event_records_by_issuable_id = stage_event_records.index_by(&:issuable_id)
|
||||||
|
|
||||||
|
issuable_model = stage_event_model.issuable_model
|
||||||
|
issuables_by_id = issuable_model.id_in(stage_event_records_by_issuable_id.keys).index_by(&:id)
|
||||||
|
|
||||||
|
stage_event_records_by_issuable_id.map do |issuable_id, record|
|
||||||
|
[issuables_by_id[issuable_id], record] if issuables_by_id[issuable_id]
|
||||||
|
end.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def serializer
|
||||||
|
MAPPINGS.fetch(subject_class).fetch(:serializer_class).new
|
||||||
|
end
|
||||||
|
|
||||||
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
def preload_associations(records)
|
||||||
|
ActiveRecord::Associations::Preloader.new.preload(
|
||||||
|
records,
|
||||||
|
MAPPINGS.fetch(subject_class).fetch(:includes_for_query)
|
||||||
|
)
|
||||||
|
|
||||||
|
records
|
||||||
|
end
|
||||||
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -23,7 +23,11 @@ module Gitlab
|
||||||
|
|
||||||
def records_fetcher
|
def records_fetcher
|
||||||
strong_memoize(:records_fetcher) do
|
strong_memoize(:records_fetcher) do
|
||||||
RecordsFetcher.new(stage: stage, query: query, params: params)
|
if use_aggregated_data_collector?
|
||||||
|
aggregated_data_collector.records_fetcher
|
||||||
|
else
|
||||||
|
RecordsFetcher.new(stage: stage, query: query, params: params)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -8,23 +8,11 @@ module Gitlab
|
||||||
include StageQueryHelpers
|
include StageQueryHelpers
|
||||||
include Gitlab::CycleAnalytics::MetricsTables
|
include Gitlab::CycleAnalytics::MetricsTables
|
||||||
|
|
||||||
MAX_RECORDS = 20
|
|
||||||
|
|
||||||
MAPPINGS = {
|
|
||||||
Issue => {
|
|
||||||
serializer_class: AnalyticsIssueSerializer,
|
|
||||||
includes_for_query: { project: { namespace: [:route] }, author: [] },
|
|
||||||
columns_for_select: %I[title iid id created_at author_id project_id]
|
|
||||||
},
|
|
||||||
MergeRequest => {
|
|
||||||
serializer_class: AnalyticsMergeRequestSerializer,
|
|
||||||
includes_for_query: { target_project: [:namespace], author: [] },
|
|
||||||
columns_for_select: %I[title iid id created_at author_id state_id target_project_id]
|
|
||||||
}
|
|
||||||
}.freeze
|
|
||||||
|
|
||||||
delegate :subject_class, to: :stage
|
delegate :subject_class, to: :stage
|
||||||
|
|
||||||
|
MAX_RECORDS = Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher::MAX_RECORDS
|
||||||
|
MAPPINGS = Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher::MAPPINGS
|
||||||
|
|
||||||
def initialize(stage:, query:, params: {})
|
def initialize(stage:, query:, params: {})
|
||||||
@stage = stage
|
@stage = stage
|
||||||
@query = query
|
@query = query
|
||||||
|
|
|
@ -114,7 +114,7 @@ module Gitlab
|
||||||
flags: flags,
|
flags: flags,
|
||||||
links: links,
|
links: links,
|
||||||
remediations: remediations,
|
remediations: remediations,
|
||||||
raw_metadata: data.to_json,
|
original_data: data,
|
||||||
metadata_version: report_version,
|
metadata_version: report_version,
|
||||||
details: data['details'] || {},
|
details: data['details'] || {},
|
||||||
signatures: signatures,
|
signatures: signatures,
|
||||||
|
|
|
@ -17,7 +17,6 @@ module Gitlab
|
||||||
attr_reader :name
|
attr_reader :name
|
||||||
attr_reader :old_location
|
attr_reader :old_location
|
||||||
attr_reader :project_fingerprint
|
attr_reader :project_fingerprint
|
||||||
attr_reader :raw_metadata
|
|
||||||
attr_reader :report_type
|
attr_reader :report_type
|
||||||
attr_reader :scanner
|
attr_reader :scanner
|
||||||
attr_reader :scan
|
attr_reader :scan
|
||||||
|
@ -28,10 +27,13 @@ module Gitlab
|
||||||
attr_reader :details
|
attr_reader :details
|
||||||
attr_reader :signatures
|
attr_reader :signatures
|
||||||
attr_reader :project_id
|
attr_reader :project_id
|
||||||
|
attr_reader :original_data
|
||||||
|
|
||||||
delegate :file_path, :start_line, :end_line, to: :location
|
delegate :file_path, :start_line, :end_line, to: :location
|
||||||
|
|
||||||
def initialize(compare_key:, identifiers:, flags: [], links: [], remediations: [], location:, metadata_version:, name:, raw_metadata:, report_type:, scanner:, scan:, uuid:, confidence: nil, severity: nil, details: {}, signatures: [], project_id: nil, vulnerability_finding_signatures_enabled: false) # rubocop:disable Metrics/ParameterLists
|
alias_method :cve, :compare_key
|
||||||
|
|
||||||
|
def initialize(compare_key:, identifiers:, flags: [], links: [], remediations: [], location:, metadata_version:, name:, original_data:, report_type:, scanner:, scan:, uuid:, confidence: nil, severity: nil, details: {}, signatures: [], project_id: nil, vulnerability_finding_signatures_enabled: false) # rubocop:disable Metrics/ParameterLists
|
||||||
@compare_key = compare_key
|
@compare_key = compare_key
|
||||||
@confidence = confidence
|
@confidence = confidence
|
||||||
@identifiers = identifiers
|
@identifiers = identifiers
|
||||||
|
@ -40,7 +42,7 @@ module Gitlab
|
||||||
@location = location
|
@location = location
|
||||||
@metadata_version = metadata_version
|
@metadata_version = metadata_version
|
||||||
@name = name
|
@name = name
|
||||||
@raw_metadata = raw_metadata
|
@original_data = original_data
|
||||||
@report_type = report_type
|
@report_type = report_type
|
||||||
@scanner = scanner
|
@scanner = scanner
|
||||||
@scan = scan
|
@scan = scan
|
||||||
|
@ -74,6 +76,10 @@ module Gitlab
|
||||||
uuid
|
uuid
|
||||||
details
|
details
|
||||||
signatures
|
signatures
|
||||||
|
description
|
||||||
|
message
|
||||||
|
cve
|
||||||
|
solution
|
||||||
].each_with_object({}) do |key, hash|
|
].each_with_object({}) do |key, hash|
|
||||||
hash[key] = public_send(key) # rubocop:disable GitlabSecurity/PublicSend
|
hash[key] = public_send(key) # rubocop:disable GitlabSecurity/PublicSend
|
||||||
end
|
end
|
||||||
|
@ -145,6 +151,26 @@ module Gitlab
|
||||||
signatures.present?
|
signatures.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def raw_metadata
|
||||||
|
@raw_metadata ||= original_data.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def description
|
||||||
|
original_data['description']
|
||||||
|
end
|
||||||
|
|
||||||
|
def message
|
||||||
|
original_data['message']
|
||||||
|
end
|
||||||
|
|
||||||
|
def solution
|
||||||
|
original_data['solution']
|
||||||
|
end
|
||||||
|
|
||||||
|
def location_data
|
||||||
|
original_data['location']
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def generate_project_fingerprint
|
def generate_project_fingerprint
|
||||||
|
|
|
@ -207,7 +207,18 @@ RSpec.describe Projects::ReleasesController do
|
||||||
let(:project) { private_project }
|
let(:project) { private_project }
|
||||||
let(:user) { guest }
|
let(:user) { guest }
|
||||||
|
|
||||||
it_behaves_like 'not found'
|
it_behaves_like 'successful request'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is an external user for the project' do
|
||||||
|
let(:project) { private_project }
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
it 'behaves like not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ RSpec.describe 'Database schema' do
|
||||||
ci_builds: %w[erased_by_id runner_id trigger_request_id user_id],
|
ci_builds: %w[erased_by_id runner_id trigger_request_id user_id],
|
||||||
ci_namespace_monthly_usages: %w[namespace_id],
|
ci_namespace_monthly_usages: %w[namespace_id],
|
||||||
ci_pipelines: %w[user_id],
|
ci_pipelines: %w[user_id],
|
||||||
|
ci_pipeline_chat_data: %w[chat_name_id], # it uses the loose foreign key featue
|
||||||
ci_runner_projects: %w[runner_id],
|
ci_runner_projects: %w[runner_id],
|
||||||
ci_trigger_requests: %w[commit_id],
|
ci_trigger_requests: %w[commit_id],
|
||||||
cluster_providers_aws: %w[security_group_id vpc_id access_key_id],
|
cluster_providers_aws: %w[security_group_id vpc_id access_key_id],
|
||||||
|
|
|
@ -9,7 +9,7 @@ FactoryBot.define do
|
||||||
metadata_version { 'sast:1.0' }
|
metadata_version { 'sast:1.0' }
|
||||||
name { 'Cipher with no integrity' }
|
name { 'Cipher with no integrity' }
|
||||||
report_type { :sast }
|
report_type { :sast }
|
||||||
raw_metadata do
|
original_data do
|
||||||
{
|
{
|
||||||
description: "The cipher does not provide data integrity update 1",
|
description: "The cipher does not provide data integrity update 1",
|
||||||
solution: "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
|
solution: "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
|
||||||
|
@ -26,7 +26,7 @@ FactoryBot.define do
|
||||||
url: "https://crypto.stackexchange.com/questions/31428/pbewithmd5anddes-cipher-does-not-check-for-integrity-first"
|
url: "https://crypto.stackexchange.com/questions/31428/pbewithmd5anddes-cipher-does-not-check-for-integrity-first"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}.to_json
|
}.deep_stringify_keys
|
||||||
end
|
end
|
||||||
scanner factory: :ci_reports_security_scanner
|
scanner factory: :ci_reports_security_scanner
|
||||||
severity { :high }
|
severity { :high }
|
||||||
|
|
|
@ -34,6 +34,10 @@ RSpec.describe 'Value Stream Analytics', :js do
|
||||||
wait_for_all_requests
|
wait_for_all_requests
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_feature_flags(use_vsa_aggregated_tables: false)
|
||||||
|
end
|
||||||
|
|
||||||
context 'as an allowed user' do
|
context 'as an allowed user' do
|
||||||
context 'when project is new' do
|
context 'when project is new' do
|
||||||
before do
|
before do
|
||||||
|
|
|
@ -123,11 +123,11 @@ RSpec.describe 'User views releases', :js do
|
||||||
|
|
||||||
within('.release-block', match: :first) do
|
within('.release-block', match: :first) do
|
||||||
expect(page).to have_content(release_v3.description)
|
expect(page).to have_content(release_v3.description)
|
||||||
|
expect(page).to have_content(release_v3.tag)
|
||||||
|
expect(page).to have_content(release_v3.name)
|
||||||
|
|
||||||
# The following properties (sometimes) include Git info,
|
# The following properties (sometimes) include Git info,
|
||||||
# so they are not rendered for Guest users
|
# so they are not rendered for Guest users
|
||||||
expect(page).not_to have_content(release_v3.name)
|
|
||||||
expect(page).not_to have_content(release_v3.tag)
|
|
||||||
expect(page).not_to have_content(release_v3.commit.short_id)
|
expect(page).not_to have_content(release_v3.commit.short_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe GitlabSchema.types['ReleaseLinks'] do
|
RSpec.describe GitlabSchema.types['ReleaseLinks'] do
|
||||||
it { expect(described_class).to require_graphql_authorizations(:download_code) }
|
it { expect(described_class).to require_graphql_authorizations(:read_release) }
|
||||||
|
|
||||||
it 'has the expected fields' do
|
it 'has the expected fields' do
|
||||||
expected_fields = %w[
|
expected_fields = %w[
|
||||||
|
@ -18,4 +18,46 @@ RSpec.describe GitlabSchema.types['ReleaseLinks'] do
|
||||||
|
|
||||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'individual field authorization' do
|
||||||
|
def fetch_authorizations(field_name)
|
||||||
|
described_class.fields.dig(field_name).instance_variable_get(:@authorize)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'openedMergeRequestsUrl' do
|
||||||
|
it 'has valid authorization' do
|
||||||
|
expect(fetch_authorizations('openedMergeRequestsUrl')).to include(:download_code)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'mergedMergeRequestsUrl' do
|
||||||
|
it 'has valid authorization' do
|
||||||
|
expect(fetch_authorizations('mergedMergeRequestsUrl')).to include(:download_code)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'closedMergeRequestsUrl' do
|
||||||
|
it 'has valid authorization' do
|
||||||
|
expect(fetch_authorizations('closedMergeRequestsUrl')).to include(:download_code)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'openedIssuesUrl' do
|
||||||
|
it 'has valid authorization' do
|
||||||
|
expect(fetch_authorizations('openedIssuesUrl')).to include(:download_code)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'closedIssuesUrl' do
|
||||||
|
it 'has valid authorization' do
|
||||||
|
expect(fetch_authorizations('closedIssuesUrl')).to include(:download_code)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'editUrl' do
|
||||||
|
it 'has valid authorization' do
|
||||||
|
expect(fetch_authorizations('editUrl')).to include(:update_release)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher do
|
||||||
|
let_it_be(:project) { create(:project) }
|
||||||
|
let_it_be(:issue_1) { create(:issue, project: project) }
|
||||||
|
let_it_be(:issue_2) { create(:issue, project: project) }
|
||||||
|
let_it_be(:issue_3) { create(:issue, project: project) }
|
||||||
|
|
||||||
|
let_it_be(:stage_event_1) { create(:cycle_analytics_issue_stage_event, issue_id: issue_1.id, start_event_timestamp: 2.years.ago, end_event_timestamp: 1.year.ago) } # duration: 1 year
|
||||||
|
let_it_be(:stage_event_2) { create(:cycle_analytics_issue_stage_event, issue_id: issue_2.id, start_event_timestamp: 5.years.ago, end_event_timestamp: 2.years.ago) } # duration: 3 years
|
||||||
|
let_it_be(:stage_event_3) { create(:cycle_analytics_issue_stage_event, issue_id: issue_3.id, start_event_timestamp: 6.years.ago, end_event_timestamp: 3.months.ago) } # duration: 5+ years
|
||||||
|
|
||||||
|
let_it_be(:stage) { create(:cycle_analytics_project_stage, start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production, project: project) }
|
||||||
|
|
||||||
|
let(:params) { {} }
|
||||||
|
|
||||||
|
subject(:records_fetcher) do
|
||||||
|
described_class.new(stage: stage, query: Analytics::CycleAnalytics::IssueStageEvent.all, params: params)
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'match returned records' do
|
||||||
|
it 'returns issues in the correct order' do
|
||||||
|
returned_iids = records_fetcher.serialized_records.pluck(:iid).map(&:to_i)
|
||||||
|
|
||||||
|
expect(returned_iids).to eq(expected_issue_ids)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#serialized_records' do
|
||||||
|
describe 'sorting' do
|
||||||
|
context 'when sorting by end event DESC' do
|
||||||
|
let(:expected_issue_ids) { [issue_3.iid, issue_1.iid, issue_2.iid] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
params[:sort] = :end_event
|
||||||
|
params[:direction] = :desc
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'match returned records'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when sorting by end event ASC' do
|
||||||
|
let(:expected_issue_ids) { [issue_2.iid, issue_1.iid, issue_3.iid] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
params[:sort] = :end_event
|
||||||
|
params[:direction] = :asc
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'match returned records'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when sorting by duration DESC' do
|
||||||
|
let(:expected_issue_ids) { [issue_3.iid, issue_2.iid, issue_1.iid] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
params[:sort] = :duration
|
||||||
|
params[:direction] = :desc
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'match returned records'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when sorting by duration ASC' do
|
||||||
|
let(:expected_issue_ids) { [issue_1.iid, issue_2.iid, issue_3.iid] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
params[:sort] = :duration
|
||||||
|
params[:direction] = :asc
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'match returned records'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'pagination' do
|
||||||
|
let(:expected_issue_ids) { [issue_3.iid] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
params[:sort] = :duration
|
||||||
|
params[:direction] = :asc
|
||||||
|
params[:page] = 2
|
||||||
|
|
||||||
|
stub_const('Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher::MAX_RECORDS', 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'match returned records'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when passing a block to serialized_records method' do
|
||||||
|
before do
|
||||||
|
params[:sort] = :duration
|
||||||
|
params[:direction] = :asc
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'yields the underlying stage event scope' do
|
||||||
|
stage_event_records = []
|
||||||
|
|
||||||
|
records_fetcher.serialized_records do |scope|
|
||||||
|
stage_event_records.concat(scope.to_a)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(stage_event_records.map(&:issue_id)).to eq([issue_1.id, issue_2.id, issue_3.id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the issue record no longer exists' do
|
||||||
|
it 'skips non-existing issue records' do
|
||||||
|
create(:cycle_analytics_issue_stage_event, {
|
||||||
|
issue_id: 0, # non-existing id
|
||||||
|
start_event_timestamp: 5.months.ago,
|
||||||
|
end_event_timestamp: 3.months.ago
|
||||||
|
})
|
||||||
|
|
||||||
|
stage_event_count = nil
|
||||||
|
|
||||||
|
records_fetcher.serialized_records do |scope|
|
||||||
|
stage_event_count = scope.to_a.size
|
||||||
|
end
|
||||||
|
|
||||||
|
issue_count = records_fetcher.serialized_records.to_a.size
|
||||||
|
|
||||||
|
expect(stage_event_count).to eq(4)
|
||||||
|
expect(issue_count).to eq(3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,9 +5,11 @@ require 'spec_helper'
|
||||||
RSpec.describe U2fRegistration do
|
RSpec.describe U2fRegistration do
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
let(:u2f_registration_name) { 'u2f_device' }
|
||||||
|
|
||||||
let(:u2f_registration) do
|
let(:u2f_registration) do
|
||||||
device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5))
|
device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5))
|
||||||
create(:u2f_registration, name: 'u2f_device',
|
create(:u2f_registration, name: u2f_registration_name,
|
||||||
user: user,
|
user: user,
|
||||||
certificate: Base64.strict_encode64(device.cert_raw),
|
certificate: Base64.strict_encode64(device.cert_raw),
|
||||||
key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
|
key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
|
||||||
|
@ -16,11 +18,27 @@ RSpec.describe U2fRegistration do
|
||||||
|
|
||||||
describe 'callbacks' do
|
describe 'callbacks' do
|
||||||
describe '#create_webauthn_registration' do
|
describe '#create_webauthn_registration' do
|
||||||
it 'creates webauthn registration' do
|
shared_examples_for 'creates webauthn registration' do
|
||||||
u2f_registration.save!
|
it 'creates webauthn registration' do
|
||||||
|
u2f_registration.save!
|
||||||
|
|
||||||
webauthn_registration = WebauthnRegistration.where(u2f_registration_id: u2f_registration.id)
|
webauthn_registration = WebauthnRegistration.where(u2f_registration_id: u2f_registration.id)
|
||||||
expect(webauthn_registration).to exist
|
expect(webauthn_registration).to exist
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'creates webauthn registration'
|
||||||
|
|
||||||
|
context 'when the u2f_registration has a blank name' do
|
||||||
|
let(:u2f_registration_name) { '' }
|
||||||
|
|
||||||
|
it_behaves_like 'creates webauthn registration'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the u2f_registration has the name as `nil`' do
|
||||||
|
let(:u2f_registration_name) { nil }
|
||||||
|
|
||||||
|
it_behaves_like 'creates webauthn registration'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'logs error' do
|
it 'logs error' do
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe WebauthnRegistration do
|
||||||
|
describe 'relations' do
|
||||||
|
it { is_expected.to belong_to(:user) }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'validations' do
|
||||||
|
it { is_expected.to validate_presence_of(:credential_xid) }
|
||||||
|
it { is_expected.to validate_presence_of(:public_key) }
|
||||||
|
it { is_expected.to validate_presence_of(:counter) }
|
||||||
|
it { is_expected.to validate_length_of(:name).is_at_least(0) }
|
||||||
|
it { is_expected.not_to allow_value(nil).for(:name) }
|
||||||
|
it do
|
||||||
|
is_expected.to validate_numericality_of(:counter)
|
||||||
|
.only_integer
|
||||||
|
.is_greater_than_or_equal_to(0)
|
||||||
|
.is_less_than_or_equal_to(4294967295)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -63,12 +63,6 @@ RSpec.describe ReleasePresenter do
|
||||||
it 'returns its own url' do
|
it 'returns its own url' do
|
||||||
is_expected.to eq(project_release_url(project, release))
|
is_expected.to eq(project_release_url(project, release))
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user is guest' do
|
|
||||||
let(:user) { guest }
|
|
||||||
|
|
||||||
it { is_expected.to be_nil }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#opened_merge_requests_url' do
|
describe '#opened_merge_requests_url' do
|
||||||
|
@ -147,13 +141,5 @@ RSpec.describe ReleasePresenter do
|
||||||
it 'returns the release name' do
|
it 'returns the release name' do
|
||||||
is_expected.to eq release.name
|
is_expected.to eq release.name
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when a user is not allowed to access any repository information" do
|
|
||||||
let(:presenter) { described_class.new(release, current_user: guest) }
|
|
||||||
|
|
||||||
it 'returns a replacement name to avoid potentially leaking tag information' do
|
|
||||||
is_expected.to eq "Release-#{release.id}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -228,6 +228,189 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
shared_examples 'restricted access to release fields' do
|
||||||
|
describe 'scalar fields' do
|
||||||
|
let(:path) { path_prefix }
|
||||||
|
|
||||||
|
let(:release_fields) do
|
||||||
|
%{
|
||||||
|
tagName
|
||||||
|
tagPath
|
||||||
|
description
|
||||||
|
descriptionHtml
|
||||||
|
name
|
||||||
|
createdAt
|
||||||
|
releasedAt
|
||||||
|
upcomingRelease
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
post_query
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'finds all release data' do
|
||||||
|
expect(data).to eq({
|
||||||
|
'tagName' => release.tag,
|
||||||
|
'tagPath' => nil,
|
||||||
|
'description' => release.description,
|
||||||
|
'descriptionHtml' => release.description_html,
|
||||||
|
'name' => release.name,
|
||||||
|
'createdAt' => release.created_at.iso8601,
|
||||||
|
'releasedAt' => release.released_at.iso8601,
|
||||||
|
'upcomingRelease' => false
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'milestones' do
|
||||||
|
let(:path) { path_prefix + %w[milestones nodes] }
|
||||||
|
|
||||||
|
let(:release_fields) do
|
||||||
|
query_graphql_field(:milestones, nil, 'nodes { id title }')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'finds milestones associated to a release' do
|
||||||
|
post_query
|
||||||
|
|
||||||
|
expected = release.milestones.order_by_dates_and_title.map do |milestone|
|
||||||
|
{ 'id' => global_id_of(milestone), 'title' => milestone.title }
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(data).to eq(expected)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'author' do
|
||||||
|
let(:path) { path_prefix + %w[author] }
|
||||||
|
|
||||||
|
let(:release_fields) do
|
||||||
|
query_graphql_field(:author, nil, 'id username')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'finds the author of the release' do
|
||||||
|
post_query
|
||||||
|
|
||||||
|
expect(data).to eq(
|
||||||
|
'id' => global_id_of(release.author),
|
||||||
|
'username' => release.author.username
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'commit' do
|
||||||
|
let(:path) { path_prefix + %w[commit] }
|
||||||
|
|
||||||
|
let(:release_fields) do
|
||||||
|
query_graphql_field(:commit, nil, 'sha')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'restricts commit associated with the release' do
|
||||||
|
post_query
|
||||||
|
|
||||||
|
expect(data).to eq(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'assets' do
|
||||||
|
describe 'count' do
|
||||||
|
let(:path) { path_prefix + %w[assets] }
|
||||||
|
|
||||||
|
let(:release_fields) do
|
||||||
|
query_graphql_field(:assets, nil, 'count')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns non source release links count' do
|
||||||
|
post_query
|
||||||
|
|
||||||
|
expect(data).to eq('count' => release.assets_count(except: [:sources]))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'links' do
|
||||||
|
let(:path) { path_prefix + %w[assets links nodes] }
|
||||||
|
|
||||||
|
let(:release_fields) do
|
||||||
|
query_graphql_field(:assets, nil,
|
||||||
|
query_graphql_field(:links, nil, 'nodes { id name url external, directAssetUrl }'))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'finds all non source external release links' do
|
||||||
|
post_query
|
||||||
|
|
||||||
|
expected = release.links.map do |link|
|
||||||
|
{
|
||||||
|
'id' => global_id_of(link),
|
||||||
|
'name' => link.name,
|
||||||
|
'url' => link.url,
|
||||||
|
'external' => true,
|
||||||
|
'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(data).to match_array(expected)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'sources' do
|
||||||
|
let(:path) { path_prefix + %w[assets sources nodes] }
|
||||||
|
|
||||||
|
let(:release_fields) do
|
||||||
|
query_graphql_field(:assets, nil,
|
||||||
|
query_graphql_field(:sources, nil, 'nodes { format url }'))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'restricts release sources' do
|
||||||
|
post_query
|
||||||
|
|
||||||
|
expect(data).to match_array([])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'links' do
|
||||||
|
let(:path) { path_prefix + %w[links] }
|
||||||
|
|
||||||
|
let(:release_fields) do
|
||||||
|
query_graphql_field(:links, nil, %{
|
||||||
|
selfUrl
|
||||||
|
openedMergeRequestsUrl
|
||||||
|
mergedMergeRequestsUrl
|
||||||
|
closedMergeRequestsUrl
|
||||||
|
openedIssuesUrl
|
||||||
|
closedIssuesUrl
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'finds only selfUrl' do
|
||||||
|
post_query
|
||||||
|
|
||||||
|
expect(data).to eq(
|
||||||
|
'selfUrl' => project_release_url(project, release),
|
||||||
|
'openedMergeRequestsUrl' => nil,
|
||||||
|
'mergedMergeRequestsUrl' => nil,
|
||||||
|
'closedMergeRequestsUrl' => nil,
|
||||||
|
'openedIssuesUrl' => nil,
|
||||||
|
'closedIssuesUrl' => nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'evidences' do
|
||||||
|
let(:path) { path_prefix + %w[evidences] }
|
||||||
|
|
||||||
|
let(:release_fields) do
|
||||||
|
query_graphql_field(:evidences, nil, 'nodes { id sha filepath collectedAt }')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'restricts all evidence fields' do
|
||||||
|
post_query
|
||||||
|
|
||||||
|
expect(data).to eq('nodes' => [])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
shared_examples 'no access to the release field' do
|
shared_examples 'no access to the release field' do
|
||||||
describe 'repository-related fields' do
|
describe 'repository-related fields' do
|
||||||
let(:path) { path_prefix }
|
let(:path) { path_prefix }
|
||||||
|
@ -302,7 +485,8 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
|
||||||
context 'when the user has Guest permissions' do
|
context 'when the user has Guest permissions' do
|
||||||
let(:current_user) { guest }
|
let(:current_user) { guest }
|
||||||
|
|
||||||
it_behaves_like 'no access to the release field'
|
it_behaves_like 'restricted access to release fields'
|
||||||
|
it_behaves_like 'no access to editUrl'
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the user has Reporter permissions' do
|
context 'when the user has Reporter permissions' do
|
||||||
|
|
|
@ -129,10 +129,12 @@ RSpec.describe 'Query.project(fullPath).releases()' do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not return data for fields that expose repository information' do
|
it 'does not return data for fields that expose repository information' do
|
||||||
|
tag_name = release.tag
|
||||||
|
release_name = release.name
|
||||||
expect(data).to eq(
|
expect(data).to eq(
|
||||||
'tagName' => nil,
|
'tagName' => tag_name,
|
||||||
'tagPath' => nil,
|
'tagPath' => nil,
|
||||||
'name' => "Release-#{release.id}",
|
'name' => release_name,
|
||||||
'commit' => nil,
|
'commit' => nil,
|
||||||
'assets' => {
|
'assets' => {
|
||||||
'count' => release.assets_count(except: [:sources]),
|
'count' => release.assets_count(except: [:sources]),
|
||||||
|
@ -143,7 +145,14 @@ RSpec.describe 'Query.project(fullPath).releases()' do
|
||||||
'evidences' => {
|
'evidences' => {
|
||||||
'nodes' => []
|
'nodes' => []
|
||||||
},
|
},
|
||||||
'links' => nil
|
'links' => {
|
||||||
|
'closedIssuesUrl' => nil,
|
||||||
|
'closedMergeRequestsUrl' => nil,
|
||||||
|
'mergedMergeRequestsUrl' => nil,
|
||||||
|
'openedIssuesUrl' => nil,
|
||||||
|
'openedMergeRequestsUrl' => nil,
|
||||||
|
'selfUrl' => project_release_url(project, release)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -73,6 +73,23 @@ RSpec.describe Packages::Npm::CreatePackageService do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
described_class::PACKAGE_JSON_NOT_ALLOWED_FIELDS.each do |field|
|
||||||
|
context "with not allowed #{field} field" do
|
||||||
|
before do
|
||||||
|
params[:versions][version][field] = 'test'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is persisted without the field' do
|
||||||
|
expect { subject }
|
||||||
|
.to change { Packages::Package.count }.by(1)
|
||||||
|
.and change { Packages::Package.npm.count }.by(1)
|
||||||
|
.and change { Packages::Tag.count }.by(1)
|
||||||
|
.and change { Packages::Npm::Metadatum.count }.by(1)
|
||||||
|
expect(subject.npm_metadatum.package_json[field]).to be_blank
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with packages_npm_abbreviated_metadata disabled' do
|
context 'with packages_npm_abbreviated_metadata disabled' do
|
||||||
before do
|
before do
|
||||||
stub_feature_flags(packages_npm_abbreviated_metadata: false)
|
stub_feature_flags(packages_npm_abbreviated_metadata: false)
|
||||||
|
|
|
@ -36,8 +36,8 @@ RSpec.shared_examples 'StageEventModel' do
|
||||||
described_class.issuable_id_column,
|
described_class.issuable_id_column,
|
||||||
:group_id,
|
:group_id,
|
||||||
:project_id,
|
:project_id,
|
||||||
:milestone_id,
|
|
||||||
:author_id,
|
:author_id,
|
||||||
|
:milestone_id,
|
||||||
:state_id,
|
:state_id,
|
||||||
:start_event_timestamp,
|
:start_event_timestamp,
|
||||||
:end_event_timestamp
|
:end_event_timestamp
|
||||||
|
|
Loading…
Reference in New Issue