Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2c90b9b579
commit
dc9ff5fda1
|
@ -11,7 +11,6 @@ import {
|
|||
GlIntersectionObserver,
|
||||
} from '@gitlab/ui';
|
||||
import { escapeRegExp } from 'lodash';
|
||||
import filesQuery from 'shared_queries/repository/files.query.graphql';
|
||||
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
|
||||
import { escapeFileUrl } from '~/lib/utils/url_utility';
|
||||
import { TREE_PAGE_SIZE } from '~/repository/constants';
|
||||
|
@ -178,8 +177,7 @@ export default {
|
|||
return this.isFolder ? this.loadFolder() : this.loadBlob();
|
||||
},
|
||||
loadFolder() {
|
||||
const query = this.glFeatures.paginatedTreeGraphqlQuery ? paginatedTreeQuery : filesQuery;
|
||||
this.apolloQuery(query, {
|
||||
this.apolloQuery(paginatedTreeQuery, {
|
||||
projectPath: this.projectPath,
|
||||
ref: this.ref,
|
||||
path: this.path,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script>
|
||||
import filesQuery from 'shared_queries/repository/files.query.graphql';
|
||||
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
|
||||
import createFlash from '~/flash';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
@ -72,9 +71,6 @@ export default {
|
|||
hasShowMore() {
|
||||
return !this.clickedShowMore && this.pageLimitReached;
|
||||
},
|
||||
paginatedTreeEnabled() {
|
||||
return this.glFeatures.paginatedTreeGraphqlQuery;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
@ -101,7 +97,7 @@ export default {
|
|||
|
||||
return this.$apollo
|
||||
.query({
|
||||
query: this.paginatedTreeEnabled ? paginatedTreeQuery : filesQuery,
|
||||
query: paginatedTreeQuery,
|
||||
variables: {
|
||||
projectPath: this.projectPath,
|
||||
ref: this.ref,
|
||||
|
@ -114,20 +110,19 @@ export default {
|
|||
if (data.errors) throw data.errors;
|
||||
if (!data?.project?.repository || originalPath !== (this.path || '/')) return;
|
||||
|
||||
const pageInfo = this.paginatedTreeEnabled
|
||||
? data.project.repository.paginatedTree.pageInfo
|
||||
: this.hasNextPage(data.project.repository.tree);
|
||||
const {
|
||||
project: {
|
||||
repository: {
|
||||
paginatedTree: { pageInfo },
|
||||
},
|
||||
},
|
||||
} = data;
|
||||
|
||||
this.isLoadingFiles = false;
|
||||
this.entries = Object.keys(this.entries).reduce(
|
||||
(acc, key) => ({
|
||||
...acc,
|
||||
[key]: this.normalizeData(
|
||||
key,
|
||||
this.paginatedTreeEnabled
|
||||
? data.project.repository.paginatedTree.nodes[0][key]
|
||||
: data.project.repository.tree[key].edges,
|
||||
),
|
||||
[key]: this.normalizeData(key, data.project.repository.paginatedTree.nodes[0][key]),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
@ -149,9 +144,7 @@ export default {
|
|||
});
|
||||
},
|
||||
normalizeData(key, data) {
|
||||
return this.entries[key].concat(
|
||||
this.paginatedTreeEnabled ? data.nodes : data.map(({ node }) => node),
|
||||
);
|
||||
return this.entries[key].concat(data.nodes);
|
||||
},
|
||||
hasNextPage(data) {
|
||||
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 projectPathQuery from '../queries/project_path.query.graphql';
|
||||
import getRefMixin from './get_ref';
|
||||
|
@ -22,7 +21,7 @@ export default {
|
|||
|
||||
return this.$apollo
|
||||
.query({
|
||||
query: gon.features.paginatedTreeGraphqlQuery ? paginatedTreeQuery : filesQuery,
|
||||
query: paginatedTreeQuery,
|
||||
variables: {
|
||||
projectPath: this.projectPath,
|
||||
ref: this.ref,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { HIGHLIGHT_CLASS_NAME } from './constants';
|
||||
import ViewerMixin from './mixins';
|
||||
|
@ -9,6 +9,9 @@ export default {
|
|||
components: {
|
||||
GlIcon,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml: GlSafeHtmlDirective,
|
||||
},
|
||||
mixins: [ViewerMixin, glFeatureFlagsMixin()],
|
||||
inject: ['blobHash'],
|
||||
data() {
|
||||
|
@ -65,7 +68,7 @@ export default {
|
|||
<div class="blob-content">
|
||||
<pre
|
||||
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>
|
||||
|
|
|
@ -5,9 +5,6 @@ class Projects::ReleasesController < Projects::ApplicationController
|
|||
before_action :require_non_empty_project, except: [:index]
|
||||
before_action :release, only: %i[edit show update downloads]
|
||||
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_create_release!, only: :new
|
||||
before_action only: :index do
|
||||
|
|
|
@ -17,7 +17,6 @@ class Projects::TreeController < Projects::ApplicationController
|
|||
|
||||
before_action do
|
||||
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(:refactor_blob_viewer, @project, default_enabled: :yaml)
|
||||
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_text_viewer, @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)
|
||||
end
|
||||
|
||||
|
|
|
@ -135,11 +135,7 @@ module Issuables
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def label_link_query(target_model, label_ids: nil, label_names: nil)
|
||||
relation = LabelLink
|
||||
.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 = LabelLink.by_target_for_exists_query(target_model.name, target_model.arel_table['id'], label_ids)
|
||||
relation = relation.joins(:label).where(labels: { name: label_names }) if label_names
|
||||
|
||||
relation
|
||||
|
|
|
@ -306,7 +306,7 @@ module Types
|
|||
null: true,
|
||||
description: 'A single release of the project.',
|
||||
resolver: Resolvers::ReleasesResolver.single,
|
||||
authorize: :download_code
|
||||
authorize: :read_release
|
||||
|
||||
field :container_expiration_policy,
|
||||
Types::ContainerExpirationPolicyType,
|
||||
|
|
|
@ -4,7 +4,7 @@ module Types
|
|||
class ReleaseLinksType < BaseObject
|
||||
graphql_name 'ReleaseLinks'
|
||||
|
||||
authorize :download_code
|
||||
authorize :read_release
|
||||
|
||||
alias_method :release, :object
|
||||
|
||||
|
@ -16,14 +16,19 @@ module Types
|
|||
description: "HTTP URL of the release's edit page.",
|
||||
authorize: :update_release
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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
|
||||
|
|
|
@ -14,8 +14,7 @@ module Types
|
|||
present_using ReleasePresenter
|
||||
|
||||
field :tag_name, GraphQL::Types::String, null: true, method: :tag,
|
||||
description: 'Name of the tag associated with the release.',
|
||||
authorize: :download_code
|
||||
description: 'Name of the tag associated with the release.'
|
||||
field :tag_path, GraphQL::Types::String, null: true,
|
||||
description: 'Relative web path to the tag associated with the release.',
|
||||
authorize: :download_code
|
||||
|
|
|
@ -16,8 +16,7 @@ module Types
|
|||
description: 'Tree of the repository.'
|
||||
field :paginated_tree, Types::Tree::TreeType.connection_type, null: true, resolver: Resolvers::PaginatedTreeResolver, calls_gitaly: true,
|
||||
max_page_size: 100,
|
||||
description: 'Paginated tree of the repository.',
|
||||
feature_flag: :paginated_tree_graphql_query
|
||||
description: 'Paginated tree of the repository.'
|
||||
field :blobs, Types::Repository::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true,
|
||||
description: 'Blobs contained within the repository'
|
||||
field :branch_names, [GraphQL::Types::String], null: true, calls_gitaly: true,
|
||||
|
|
|
@ -20,6 +20,10 @@ module Analytics
|
|||
def self.issuable_id_column
|
||||
:issue_id
|
||||
end
|
||||
|
||||
def self.issuable_model
|
||||
::Issue
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,6 +20,10 @@ module Analytics
|
|||
def self.issuable_id_column
|
||||
:merge_request_id
|
||||
end
|
||||
|
||||
def self.issuable_model
|
||||
::MergeRequest
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,6 +18,10 @@ module Analytics
|
|||
scope :end_event_is_not_happened_yet, -> { where(end_event_timestamp: nil) }
|
||||
end
|
||||
|
||||
def issuable_id
|
||||
attributes[self.class.issuable_id_column.to_s]
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def upsert_data(data)
|
||||
upsert_values = data.map do |row|
|
||||
|
@ -26,8 +30,8 @@ module Analytics
|
|||
:issuable_id,
|
||||
:group_id,
|
||||
:project_id,
|
||||
:author_id,
|
||||
:milestone_id,
|
||||
:author_id,
|
||||
:state_id,
|
||||
:start_event_timestamp,
|
||||
:end_event_timestamp
|
||||
|
|
|
@ -11,4 +11,16 @@ class LabelLink < ApplicationRecord
|
|||
validates :label, presence: true, unless: :importing?
|
||||
|
||||
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
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
class WebauthnRegistration < ApplicationRecord
|
||||
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,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 }
|
||||
end
|
||||
|
|
|
@ -22,8 +22,6 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
|
|||
end
|
||||
|
||||
def self_url
|
||||
return unless can_download_code?
|
||||
|
||||
project_release_url(project, release)
|
||||
end
|
||||
|
||||
|
@ -64,7 +62,7 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
|
|||
|
||||
delegator_override :name
|
||||
def name
|
||||
can_download_code? ? release.name : "Release-#{release.id}"
|
||||
release.name
|
||||
end
|
||||
|
||||
def download_url(filepath)
|
||||
|
|
|
@ -4,6 +4,8 @@ module Packages
|
|||
class CreatePackageService < ::Packages::CreatePackageService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
PACKAGE_JSON_NOT_ALLOWED_FIELDS = %w[readme readmeFilename].freeze
|
||||
|
||||
def execute
|
||||
return error('Version is empty.', 400) if version.blank?
|
||||
return error('Package already exists.', 403) if current_package_exists?
|
||||
|
@ -22,7 +24,7 @@ module Packages
|
|||
::Packages::Npm::CreateTagService.new(package, dist_tag).execute
|
||||
|
||||
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
|
||||
|
||||
package
|
||||
|
@ -50,6 +52,10 @@ module Packages
|
|||
params[:versions][version]
|
||||
end
|
||||
|
||||
def package_json
|
||||
version_data.except(*PACKAGE_JSON_NOT_ALLOWED_FIELDS)
|
||||
end
|
||||
|
||||
def dist_tag
|
||||
params['dist-tags'].each_key.first
|
||||
end
|
||||
|
|
|
@ -224,7 +224,7 @@ module Gitlab
|
|||
config.assets.precompile << "page_bundles/build.css"
|
||||
config.assets.precompile << "page_bundles/ci_status.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/epics.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
|
||||
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
|
||||
ADD CONSTRAINT fk_rails_f365da8250 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -14030,7 +14030,7 @@ Returns [`[String!]`](#string).
|
|||
|
||||
##### `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).
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ The following are example projects that demonstrate Review App configuration:
|
|||
|
||||
- [NGINX](https://gitlab.com/gitlab-examples/review-apps-nginx).
|
||||
- [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:
|
||||
|
||||
|
|
|
@ -18,7 +18,9 @@ use external test planning tools, which require additional overhead, context swi
|
|||
|
||||
## 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:
|
||||
|
||||
|
@ -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
|
||||
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:
|
||||
|
||||
|
@ -45,8 +49,10 @@ To view a test case:
|
|||
|
||||
You can edit a test case's title and description.
|
||||
|
||||
Users with Reporter or higher [permissions](../../user/permissions.md) can edit test cases.
|
||||
Users demoted to the Guest role can continue to edit the test cases they created
|
||||
Prerequisite:
|
||||
|
||||
- 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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
@ -73,6 +81,6 @@ To view archived test cases:
|
|||
|
||||
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**.
|
||||
|
|
|
@ -90,7 +90,7 @@ The following examples show the progression of a feature flag.
|
|||
|
||||
FLAG:
|
||||
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.
|
||||
```
|
||||
|
||||
|
|
|
@ -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,
|
||||
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
|
||||
|
||||
- [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)
|
||||
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
|
||||
|
||||
Git 2.33.x and later is required. We recommend you use the
|
||||
|
|
|
@ -70,7 +70,8 @@ synchronizations is:
|
|||
```yaml
|
||||
gitops:
|
||||
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:
|
||||
- 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 |
|
||||
| **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 |
|
||||
| **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
|
||||
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. 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. 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. 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
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
> [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)
|
||||
have read and download access to the project releases.
|
||||
- Users with [Guest role](../../../user/permissions.md#project-members-permissions)
|
||||
have read and download access to the project releases, however,
|
||||
repository-related information are redacted (for example the Git tag name).
|
||||
have read and download access to the project releases.
|
||||
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
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ For the latest updates, check the [Tasks Roadmap](https://gitlab.com/groups/gitl
|
|||
|
||||
FLAG:
|
||||
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.
|
||||
|
||||
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)
|
||||
return query if params[:label_name].blank?
|
||||
|
||||
all_label_ids = Issuables::LabelFilter
|
||||
.new(group: root_ancestor, project: nil, params: { label_name: params[:label_name] })
|
||||
.find_label_ids(params[:label_name])
|
||||
|
||||
return query.none if params[:label_name].size != all_label_ids.size
|
||||
|
||||
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
|
||||
LabelFilter.new(
|
||||
stage: stage,
|
||||
params: params,
|
||||
project: nil,
|
||||
group: root_ancestor
|
||||
).filter(query)
|
||||
end
|
||||
|
||||
def filter_assignees(query)
|
||||
|
|
|
@ -30,6 +30,12 @@ module Gitlab
|
|||
strong_memoize(:count) { limit_count }
|
||||
end
|
||||
|
||||
def records_fetcher
|
||||
strong_memoize(:records_fetcher) do
|
||||
RecordsFetcher.new(stage: stage, query: query, params: params)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
|
|
@ -8,23 +8,11 @@ module Gitlab
|
|||
include StageQueryHelpers
|
||||
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
|
||||
|
||||
MAX_RECORDS = Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher::MAX_RECORDS
|
||||
MAPPINGS = Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher::MAPPINGS
|
||||
|
||||
def initialize(stage:, query:, params: {})
|
||||
@stage = stage
|
||||
@query = query
|
||||
|
|
|
@ -114,7 +114,7 @@ module Gitlab
|
|||
flags: flags,
|
||||
links: links,
|
||||
remediations: remediations,
|
||||
raw_metadata: data.to_json,
|
||||
original_data: data,
|
||||
metadata_version: report_version,
|
||||
details: data['details'] || {},
|
||||
signatures: signatures,
|
||||
|
|
|
@ -17,7 +17,6 @@ module Gitlab
|
|||
attr_reader :name
|
||||
attr_reader :old_location
|
||||
attr_reader :project_fingerprint
|
||||
attr_reader :raw_metadata
|
||||
attr_reader :report_type
|
||||
attr_reader :scanner
|
||||
attr_reader :scan
|
||||
|
@ -28,10 +27,13 @@ module Gitlab
|
|||
attr_reader :details
|
||||
attr_reader :signatures
|
||||
attr_reader :project_id
|
||||
attr_reader :original_data
|
||||
|
||||
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
|
||||
@confidence = confidence
|
||||
@identifiers = identifiers
|
||||
|
@ -40,7 +42,7 @@ module Gitlab
|
|||
@location = location
|
||||
@metadata_version = metadata_version
|
||||
@name = name
|
||||
@raw_metadata = raw_metadata
|
||||
@original_data = original_data
|
||||
@report_type = report_type
|
||||
@scanner = scanner
|
||||
@scan = scan
|
||||
|
@ -74,6 +76,10 @@ module Gitlab
|
|||
uuid
|
||||
details
|
||||
signatures
|
||||
description
|
||||
message
|
||||
cve
|
||||
solution
|
||||
].each_with_object({}) do |key, hash|
|
||||
hash[key] = public_send(key) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
|
@ -145,6 +151,26 @@ module Gitlab
|
|||
signatures.present?
|
||||
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
|
||||
|
||||
def generate_project_fingerprint
|
||||
|
|
|
@ -207,7 +207,18 @@ RSpec.describe Projects::ReleasesController do
|
|||
let(:project) { private_project }
|
||||
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
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ RSpec.describe 'Database schema' do
|
|||
ci_builds: %w[erased_by_id runner_id trigger_request_id user_id],
|
||||
ci_namespace_monthly_usages: %w[namespace_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_trigger_requests: %w[commit_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' }
|
||||
name { 'Cipher with no integrity' }
|
||||
report_type { :sast }
|
||||
raw_metadata do
|
||||
original_data do
|
||||
{
|
||||
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.",
|
||||
|
@ -26,7 +26,7 @@ FactoryBot.define do
|
|||
url: "https://crypto.stackexchange.com/questions/31428/pbewithmd5anddes-cipher-does-not-check-for-integrity-first"
|
||||
}
|
||||
]
|
||||
}.to_json
|
||||
}.deep_stringify_keys
|
||||
end
|
||||
scanner factory: :ci_reports_security_scanner
|
||||
severity { :high }
|
||||
|
|
|
@ -34,6 +34,10 @@ RSpec.describe 'Value Stream Analytics', :js do
|
|||
wait_for_all_requests
|
||||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(use_vsa_aggregated_tables: false)
|
||||
end
|
||||
|
||||
context 'as an allowed user' do
|
||||
context 'when project is new' do
|
||||
before do
|
||||
|
|
|
@ -123,11 +123,11 @@ RSpec.describe 'User views releases', :js do
|
|||
|
||||
within('.release-block', match: :first) do
|
||||
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,
|
||||
# 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)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
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
|
||||
expected_fields = %w[
|
||||
|
@ -18,4 +18,46 @@ RSpec.describe GitlabSchema.types['ReleaseLinks'] do
|
|||
|
||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||
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
|
||||
|
|
|
@ -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
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:u2f_registration_name) { 'u2f_device' }
|
||||
|
||||
let(:u2f_registration) do
|
||||
device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5))
|
||||
create(:u2f_registration, name: 'u2f_device',
|
||||
create(:u2f_registration, name: u2f_registration_name,
|
||||
user: user,
|
||||
certificate: Base64.strict_encode64(device.cert_raw),
|
||||
key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
|
||||
|
@ -16,11 +18,27 @@ RSpec.describe U2fRegistration do
|
|||
|
||||
describe 'callbacks' do
|
||||
describe '#create_webauthn_registration' do
|
||||
it 'creates webauthn registration' do
|
||||
u2f_registration.save!
|
||||
shared_examples_for 'creates webauthn registration' do
|
||||
it 'creates webauthn registration' do
|
||||
u2f_registration.save!
|
||||
|
||||
webauthn_registration = WebauthnRegistration.where(u2f_registration_id: u2f_registration.id)
|
||||
expect(webauthn_registration).to exist
|
||||
webauthn_registration = WebauthnRegistration.where(u2f_registration_id: u2f_registration.id)
|
||||
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
|
||||
|
||||
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
|
||||
is_expected.to eq(project_release_url(project, release))
|
||||
end
|
||||
|
||||
context 'when user is guest' do
|
||||
let(:user) { guest }
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#opened_merge_requests_url' do
|
||||
|
@ -147,13 +141,5 @@ RSpec.describe ReleasePresenter do
|
|||
it 'returns the release name' do
|
||||
is_expected.to eq release.name
|
||||
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
|
||||
|
|
|
@ -228,6 +228,189 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
|
|||
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
|
||||
describe 'repository-related fields' do
|
||||
let(:path) { path_prefix }
|
||||
|
@ -302,7 +485,8 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
|
|||
context 'when the user has Guest permissions' do
|
||||
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
|
||||
|
||||
context 'when the user has Reporter permissions' do
|
||||
|
|
|
@ -129,10 +129,12 @@ RSpec.describe 'Query.project(fullPath).releases()' do
|
|||
end
|
||||
|
||||
it 'does not return data for fields that expose repository information' do
|
||||
tag_name = release.tag
|
||||
release_name = release.name
|
||||
expect(data).to eq(
|
||||
'tagName' => nil,
|
||||
'tagName' => tag_name,
|
||||
'tagPath' => nil,
|
||||
'name' => "Release-#{release.id}",
|
||||
'name' => release_name,
|
||||
'commit' => nil,
|
||||
'assets' => {
|
||||
'count' => release.assets_count(except: [:sources]),
|
||||
|
@ -143,7 +145,14 @@ RSpec.describe 'Query.project(fullPath).releases()' do
|
|||
'evidences' => {
|
||||
'nodes' => []
|
||||
},
|
||||
'links' => nil
|
||||
'links' => {
|
||||
'closedIssuesUrl' => nil,
|
||||
'closedMergeRequestsUrl' => nil,
|
||||
'mergedMergeRequestsUrl' => nil,
|
||||
'openedIssuesUrl' => nil,
|
||||
'openedMergeRequestsUrl' => nil,
|
||||
'selfUrl' => project_release_url(project, release)
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -73,6 +73,23 @@ RSpec.describe Packages::Npm::CreatePackageService do
|
|||
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
|
||||
before do
|
||||
stub_feature_flags(packages_npm_abbreviated_metadata: false)
|
||||
|
|
|
@ -36,8 +36,8 @@ RSpec.shared_examples 'StageEventModel' do
|
|||
described_class.issuable_id_column,
|
||||
:group_id,
|
||||
:project_id,
|
||||
:milestone_id,
|
||||
:author_id,
|
||||
:milestone_id,
|
||||
:state_id,
|
||||
:start_event_timestamp,
|
||||
:end_event_timestamp
|
||||
|
|
Loading…
Reference in New Issue