summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md9
-rw-r--r--app/assets/javascripts/jobs/components/table/cells/job_cell.vue4
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js40
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue167
-rw-r--r--app/helpers/commits_helper.rb31
-rw-r--r--app/helpers/notify_helper.rb2
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/snippet.rb2
-rw-r--r--app/views/admin/application_settings/_initial_branch_name.html.haml4
-rw-r--r--app/views/groups/settings/repository/_initial_branch_name.html.haml4
-rw-r--r--app/views/projects/commits/_commit.html.haml26
-rw-r--r--app/views/projects/commits/_commits.html.haml14
-rw-r--r--app/views/projects/compare/index.html.haml6
-rw-r--r--changelogs/unreleased/330787-omits-trailing-slash-when-checking-for-allowed-requests.yml5
-rw-r--r--changelogs/unreleased/id-rename-main-to-master-in-views.yml5
-rw-r--r--changelogs/unreleased/sh-avoid-trailing-slash-in-proxy.yml5
-rw-r--r--changelogs/unreleased/sh-fix-nplus-one-pipelines-show.yml5
-rw-r--r--config/feature_flags/development/cached_commits.yml8
-rw-r--r--config/helpers/vendor_dll_hash.js2
-rw-r--r--config/karma.config.js2
-rw-r--r--config/webpack.config.js2
-rw-r--r--doc/ci/jobs/job_control.md44
-rw-r--r--doc/ci/yaml/README.md10
-rw-r--r--doc/development/stage_group_dashboards.md2
-rw-r--r--doc/development/usage_ping/index.md12
-rw-r--r--lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.latest.gitlab-ci.yml77
-rw-r--r--lib/gitlab/default_branch.rb4
-rw-r--r--locale/gitlab.pot4
-rw-r--r--package.json4
-rwxr-xr-xscripts/frontend/check_page_bundle_mixins_css_for_sideeffects.js2
-rw-r--r--scripts/frontend/merge_coverage_frontend.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js87
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js228
-rw-r--r--spec/helpers/commits_helper_spec.rb34
-rw-r--r--yarn.lock79
37 files changed, 843 insertions, 97 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79d8c52461a..ee8af966567 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,15 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 13.11.4 (2021-05-14)
+
+### Fixed (3 changes)
+
+- Fix N+1 SQL queries in PipelinesController#show. !60794
+- Omit trailing slash when proxying pre-authorized routes with no suffix. !61638
+- Omit trailing slash when checking allowed requests in the read-only middleware. !61641
+
+
## 13.11.3 (2021-04-30)
### Fixed (1 change)
diff --git a/app/assets/javascripts/jobs/components/table/cells/job_cell.vue b/app/assets/javascripts/jobs/components/table/cells/job_cell.vue
index 46108572a88..88a9f73258f 100644
--- a/app/assets/javascripts/jobs/components/table/cells/job_cell.vue
+++ b/app/assets/javascripts/jobs/components/table/cells/job_cell.vue
@@ -91,7 +91,9 @@ export default {
data-testid="stuck-icon"
/>
- <div class="gl-display-flex gl-align-items-center">
+ <div
+ class="gl-display-flex gl-align-items-center gl-lg-justify-content-start gl-justify-content-end"
+ >
<div v-if="jobRef" class="gl-max-w-15 gl-text-truncate">
<gl-icon
v-if="createdByTag"
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table.vue b/app/assets/javascripts/jobs/components/table/jobs_table.vue
index 2736aa42684..4fe5bbf79cd 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table.vue
@@ -11,6 +11,8 @@ const defaultTableClasses = {
tdClass: 'gl-p-5!',
thClass: 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!',
};
+// eslint-disable-next-line @gitlab/require-i18n-strings
+const coverageTdClasses = `${defaultTableClasses.tdClass} gl-display-none! gl-lg-display-table-cell!`;
export default {
i18n: {
@@ -56,7 +58,8 @@ export default {
{
key: 'coverage',
label: __('Coverage'),
- ...defaultTableClasses,
+ tdClass: coverageTdClasses,
+ thClass: defaultTableClasses.thClass,
columnClass: 'gl-w-10p',
},
{
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index 784f7cccf15..2cb1b6a195f 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -1,6 +1,7 @@
import { __ } from '~/locale';
export const DEBOUNCE_DELAY = 200;
+export const MAX_RECENT_TOKENS_SIZE = 3;
export const FILTER_NONE = 'None';
export const FILTER_ANY = 'Any';
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
index a15cf220ee5..e5c8d29e09b 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
@@ -1,6 +1,9 @@
-import { isEmpty } from 'lodash';
+import { isEmpty, uniqWith, isEqual } from 'lodash';
+import AccessorUtilities from '~/lib/utils/accessor';
import { queryToObject } from '~/lib/utils/url_utility';
+import { MAX_RECENT_TOKENS_SIZE } from './constants';
+
/**
* Strips enclosing quotations from a string if it has one.
*
@@ -162,3 +165,38 @@ export function urlQueryToFilter(query = '') {
return { ...memo, [filterName]: { value, operator } };
}, {});
}
+
+/**
+ * Returns array of token values from localStorage
+ * based on provided recentTokenValuesStorageKey
+ *
+ * @param {String} recentTokenValuesStorageKey
+ * @returns
+ */
+export function getRecentlyUsedTokenValues(recentTokenValuesStorageKey) {
+ let recentlyUsedTokenValues = [];
+ if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ recentlyUsedTokenValues = JSON.parse(localStorage.getItem(recentTokenValuesStorageKey)) || [];
+ }
+ return recentlyUsedTokenValues;
+}
+
+/**
+ * Sets provided token value to recently used array
+ * within localStorage for provided recentTokenValuesStorageKey
+ *
+ * @param {String} recentTokenValuesStorageKey
+ * @param {Object} tokenValue
+ */
+export function setTokenValueToRecentlyUsed(recentTokenValuesStorageKey, tokenValue) {
+ const recentlyUsedTokenValues = getRecentlyUsedTokenValues(recentTokenValuesStorageKey);
+
+ recentlyUsedTokenValues.splice(0, 0, { ...tokenValue });
+
+ if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ localStorage.setItem(
+ recentTokenValuesStorageKey,
+ JSON.stringify(uniqWith(recentlyUsedTokenValues, isEqual).slice(0, MAX_RECENT_TOKENS_SIZE)),
+ );
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
new file mode 100644
index 00000000000..6ebc5431012
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -0,0 +1,167 @@
+<script>
+import {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+
+import { DEBOUNCE_DELAY } from '../constants';
+import { getRecentlyUsedTokenValues, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
+
+export default {
+ components: {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlLoadingIcon,
+ },
+ props: {
+ tokenConfig: {
+ type: Object,
+ required: true,
+ },
+ tokenValue: {
+ type: Object,
+ required: true,
+ },
+ tokenActive: {
+ type: Boolean,
+ required: true,
+ },
+ tokensListLoading: {
+ type: Boolean,
+ required: true,
+ },
+ tokenValues: {
+ type: Array,
+ required: true,
+ },
+ fnActiveTokenValue: {
+ type: Function,
+ required: true,
+ },
+ defaultTokenValues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ recentTokenValuesStorageKey: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ valueIdentifier: {
+ type: String,
+ required: false,
+ default: 'id',
+ },
+ fnCurrentTokenValue: {
+ type: Function,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ searchKey: '',
+ recentTokenValues: this.recentTokenValuesStorageKey
+ ? getRecentlyUsedTokenValues(this.recentTokenValuesStorageKey)
+ : [],
+ loading: false,
+ };
+ },
+ computed: {
+ isRecentTokenValuesEnabled() {
+ return Boolean(this.recentTokenValuesStorageKey);
+ },
+ recentTokenIds() {
+ return this.recentTokenValues.map((tokenValue) => tokenValue.id || tokenValue.name);
+ },
+ currentTokenValue() {
+ if (this.fnCurrentTokenValue) {
+ return this.fnCurrentTokenValue(this.tokenValue.data);
+ }
+ return this.tokenValue.data.toLowerCase();
+ },
+ activeTokenValue() {
+ return this.fnActiveTokenValue(this.tokenValues, this.currentTokenValue);
+ },
+ /**
+ * Return all the tokenValues when searchKey is present
+ * otherwise return only the tokenValues which aren't
+ * present in "Recently used"
+ */
+ availableTokenValues() {
+ return this.searchKey
+ ? this.tokenValues
+ : this.tokenValues.filter(
+ (tokenValue) => !this.recentTokenIds.includes(tokenValue[this.valueIdentifier]),
+ );
+ },
+ },
+ watch: {
+ tokenActive: {
+ immediate: true,
+ handler(newValue) {
+ if (!newValue && !this.tokenValues.length) {
+ this.$emit('fetch-token-values', this.tokenValue.data);
+ }
+ },
+ },
+ },
+ methods: {
+ handleInput({ data }) {
+ this.searchKey = data;
+ setTimeout(() => {
+ if (!this.tokensListLoading) this.$emit('fetch-token-values', data);
+ }, DEBOUNCE_DELAY);
+ },
+ handleTokenValueSelected(activeTokenValue) {
+ if (this.isRecentTokenValuesEnabled) {
+ setTokenValueToRecentlyUsed(this.recentTokenValuesStorageKey, activeTokenValue);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token
+ :config="tokenConfig"
+ v-bind="{ ...this.$parent.$props, ...this.$parent.$attrs }"
+ v-on="this.$parent.$listeners"
+ @input="handleInput"
+ @select="handleTokenValueSelected(activeTokenValue)"
+ >
+ <template #view-token="viewTokenProps">
+ <slot name="view-token" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot>
+ </template>
+ <template #view="viewTokenProps">
+ <slot name="view" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot>
+ </template>
+ <template #suggestions>
+ <template v-if="defaultTokenValues.length">
+ <gl-filtered-search-suggestion
+ v-for="token in defaultTokenValues"
+ :key="token.value"
+ :value="token.value"
+ >
+ {{ token.text }}
+ </gl-filtered-search-suggestion>
+ <gl-dropdown-divider />
+ </template>
+ <template v-if="isRecentTokenValuesEnabled && recentTokenValues.length && !searchKey">
+ <gl-dropdown-section-header>{{ __('Recently used') }}</gl-dropdown-section-header>
+ <slot name="token-values-list" :token-values="recentTokenValues"></slot>
+ <gl-dropdown-divider />
+ </template>
+ <gl-loading-icon v-if="tokensListLoading" />
+ <template v-else>
+ <slot name="token-values-list" :token-values="availableTokenValues"></slot>
+ </template>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 4b71f9b3065..d7ec2e699a0 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -148,6 +148,27 @@ module CommitsHelper
end
end
+ # This is used to calculate a cache key for the app/views/projects/commits/_commit.html.haml
+ # partial. It takes some of the same parameters as used in the partial and will hash the
+ # current pipeline status.
+ #
+ # This includes a keyed hash for values that can be nil, to prevent invalid cache entries
+ # being served if the order should change in future.
+ def commit_partial_cache_key(commit, ref:, merge_request:, request:)
+ [
+ commit,
+ commit.author,
+ ref,
+ {
+ merge_request: merge_request,
+ pipeline_status: hashed_pipeline_status(commit, ref),
+ xhr: request.xhr?,
+ controller: controller.controller_path,
+ path: @path # referred to in #link_to_browse_code
+ }
+ ]
+ end
+
protected
# Private: Returns a link to a person. If the person has a matching user and
@@ -221,4 +242,14 @@ module CommitsHelper
project_commit_path(project, commit)
end
end
+
+ private
+
+ def hashed_pipeline_status(commit, ref)
+ status = commit.status_for(ref)
+
+ return if status.nil?
+
+ Digest::SHA1.hexdigest(status.to_s)
+ end
end
diff --git a/app/helpers/notify_helper.rb b/app/helpers/notify_helper.rb
index 03da679cfdd..38c98776fdf 100644
--- a/app/helpers/notify_helper.rb
+++ b/app/helpers/notify_helper.rb
@@ -18,7 +18,7 @@ module NotifyHelper
when "Developer"
s_("InviteEmail|As a developer, you have full access to projects, so you can take an idea from concept to production.")
when "Maintainer"
- s_("InviteEmail|As a maintainer, you have full access to projects. You can push commits to master and deploy to production.")
+ s_("InviteEmail|As a maintainer, you have full access to projects. You can push commits to the default branch and deploy to production.")
when "Owner"
s_("InviteEmail|As an owner, you have full access to projects and can manage access to the group, including inviting new members.")
when "Minimal Access"
diff --git a/app/models/project.rb b/app/models/project.rb
index 9d8bd2dbb36..6d8f46d9ea6 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2539,7 +2539,7 @@ class Project < ApplicationRecord
def default_branch_or_main
return default_branch if default_branch
- Gitlab::DefaultBranch.value(project: self)
+ Gitlab::DefaultBranch.value(object: self)
end
def ci_config_path_or_default
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 7beeb106735..68957dd6b22 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -315,7 +315,7 @@ class Snippet < ApplicationRecord
override :default_branch
def default_branch
- super || Gitlab::DefaultBranch.value(project: project)
+ super || Gitlab::DefaultBranch.value(object: project)
end
def repository_storage
diff --git a/app/views/admin/application_settings/_initial_branch_name.html.haml b/app/views/admin/application_settings/_initial_branch_name.html.haml
index b5c178641df..f881808e51f 100644
--- a/app/views/admin/application_settings/_initial_branch_name.html.haml
+++ b/app/views/admin/application_settings/_initial_branch_name.html.haml
@@ -1,12 +1,12 @@
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-default-branch-name'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
- - fallback_branch_name = '<code>master</code>'
+ - fallback_branch_name = "<code>#{Gitlab::DefaultBranch.value}</code>"
%fieldset
.form-group
= f.label :default_branch_name, _('Default initial branch name'), class: 'label-light'
- = f.text_field :default_branch_name, placeholder: 'master', class: 'form-control gl-form-input'
+ = f.text_field :default_branch_name, placeholder: Gitlab::DefaultBranch.value, class: 'form-control gl-form-input'
%span.form-text.text-muted
= (_("Changes affect new repositories only. If not specified, Git's default name %{branch_name_default} will be used.") % { branch_name_default: fallback_branch_name } ).html_safe
diff --git a/app/views/groups/settings/repository/_initial_branch_name.html.haml b/app/views/groups/settings/repository/_initial_branch_name.html.haml
index efe690a0c2d..23ac7d51e4f 100644
--- a/app/views/groups/settings/repository/_initial_branch_name.html.haml
+++ b/app/views/groups/settings/repository/_initial_branch_name.html.haml
@@ -9,12 +9,12 @@
.settings-content
= form_for @group, url: group_path(@group, anchor: 'js-default-branch-name'), html: { class: 'fieldset-form' } do |f|
= form_errors(@group)
- - fallback_branch_name = '<code>master</code>'
+ - fallback_branch_name = "<code>#{Gitlab::DefaultBranch.value(object: @group)}</code>"
%fieldset
.form-group
= f.label :default_branch_name, _('Default initial branch name'), class: 'label-light'
- = f.text_field :default_branch_name, value: group.namespace_settings&.default_branch_name, placeholder: 'master', class: 'form-control'
+ = f.text_field :default_branch_name, value: group.namespace_settings&.default_branch_name, placeholder: Gitlab::DefaultBranch.value(object: @group), class: 'form-control'
%span.form-text.text-muted
= (_("Changes affect new repositories only. If not specified, either the configured application-wide default or Git's default name %{branch_name_default} will be used.") % { branch_name_default: fallback_branch_name }).html_safe
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index ceb312450be..bc0d14743b9 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -3,22 +3,24 @@
- `assets/javascripts/diffs/components/commit_item.vue`
EXCEPTION WARNING - see above `.vue` file for de-sync drift
--#-----------------------------------------------------------------
-- view_details = local_assigns.fetch(:view_details, false)
-- merge_request = local_assigns.fetch(:merge_request, nil)
-- project = local_assigns.fetch(:project) { merge_request&.project }
-- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
-- commit = commit.present(current_user: current_user)
-- commit_status = commit.status_for(ref)
-- collapsible = local_assigns.fetch(:collapsible, true)
-- link_data_attrs = local_assigns.fetch(:link_data_attrs, {})
-
-- link = commit_path(project, commit, merge_request: merge_request)
+ WARNING: When introducing new content here, please consider what
+ changes may need to be made in the cache keys used to
+ wrap this view, found in
+ CommitsHelper#commit_partial_cache_key
+-#-----------------------------------------------------------------
+- view_details = local_assigns.fetch(:view_details, false)
+- merge_request = local_assigns.fetch(:merge_request, nil)
+- project = local_assigns.fetch(:project) { merge_request&.project }
+- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
+- commit = commit.present(current_user: current_user)
+- commit_status = commit.status_for(ref)
+- collapsible = local_assigns.fetch(:collapsible, true)
+- link_data_attrs = local_assigns.fetch(:link_data_attrs, {})
+- link = commit_path(project, commit, merge_request: merge_request)
- show_project_name = local_assigns.fetch(:show_project_name, false)
%li{ class: ["commit flex-row", ("js-toggle-container" if collapsible)], id: "commit-#{commit.short_id}" }
-
.avatar-cell.d-none.d-sm-block
= author_avatar(commit, size: 40, has_tooltip: false)
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index 9e2dca3ad71..e6c9a7166a9 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -3,8 +3,8 @@
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
-- commits = @commits
-- context_commits = @context_commits
+- commits = @commits&.map { |commit| commit.present(current_user: current_user) }
+- context_commits = @context_commits&.map { |commit| commit.present(current_user: current_user) }
- hidden = @hidden_commit_count
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, daily_commits|
@@ -14,7 +14,10 @@
%li.commits-row{ data: { day: day } }
%ul.content-list.commit-list.flex-list
- = render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request }
+ - if Feature.enabled?(:cached_commits, project, default_enabled: :yaml)
+ = render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request }, cached: -> (commit) { commit_partial_cache_key(commit, ref: ref, merge_request: merge_request, request: request) }
+ - else
+ = render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request }
- if context_commits.present?
%li.commit-header.js-commit-header
@@ -25,7 +28,10 @@
%li.commits-row
%ul.content-list.commit-list.flex-list
- = render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }
+ - if Feature.enabled?(:cached_commits, project, default_enabled: :yaml)
+ = render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }, cached: -> (commit) { commit_partial_cache_key(commit, ref: ref, merge_request: merge_request, request: request) }
+ - else
+ = render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }
- if hidden > 0
%li.gl-alert.gl-alert-warning
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index e3ab184ec6f..426d022da26 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -4,11 +4,11 @@
%h3.page-title
= _("Compare Git revisions")
.sub-header-block
- - example_master = capture do
- %code.ref-name master
+ - example_branch = capture do
+ %code.ref-name= @project.default_branch_or_main
- example_sha = capture do
%code.ref-name 4eedf23
- = html_escape(_("Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request.")) % { master: example_master.html_safe, sha: example_sha.html_safe }
+ = html_escape(_("Choose a branch/tag (e.g. %{branch}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request.")) % { branch: example_branch.html_safe, sha: example_sha.html_safe }
%br
= html_escape(_("Changes are shown as if the %{b_open}source%{b_close} revision was being merged into the %{b_open}target%{b_close} revision.")) % { b_open: '<b>'.html_safe, b_close: '</b>'.html_safe }
diff --git a/changelogs/unreleased/330787-omits-trailing-slash-when-checking-for-allowed-requests.yml b/changelogs/unreleased/330787-omits-trailing-slash-when-checking-for-allowed-requests.yml
deleted file mode 100644
index 6e04471fa13..00000000000
--- a/changelogs/unreleased/330787-omits-trailing-slash-when-checking-for-allowed-requests.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Omit trailing slash when checking allowed requests in the read-only middleware
-merge_request: 61641
-author:
-type: fixed
diff --git a/changelogs/unreleased/id-rename-main-to-master-in-views.yml b/changelogs/unreleased/id-rename-main-to-master-in-views.yml
new file mode 100644
index 00000000000..7e3f45274f1
--- /dev/null
+++ b/changelogs/unreleased/id-rename-main-to-master-in-views.yml
@@ -0,0 +1,5 @@
+---
+title: Rename master to main in views placeholders
+merge_request: 61252
+author:
+type: changed
diff --git a/changelogs/unreleased/sh-avoid-trailing-slash-in-proxy.yml b/changelogs/unreleased/sh-avoid-trailing-slash-in-proxy.yml
deleted file mode 100644
index 3bea1874ff3..00000000000
--- a/changelogs/unreleased/sh-avoid-trailing-slash-in-proxy.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Omit trailing slash when proxying pre-authorized routes with no suffix
-merge_request: 61638
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-nplus-one-pipelines-show.yml b/changelogs/unreleased/sh-fix-nplus-one-pipelines-show.yml
deleted file mode 100644
index ebaf2aee123..00000000000
--- a/changelogs/unreleased/sh-fix-nplus-one-pipelines-show.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix N+1 SQL queries in PipelinesController#show
-merge_request: 60794
-author:
-type: fixed
diff --git a/config/feature_flags/development/cached_commits.yml b/config/feature_flags/development/cached_commits.yml
new file mode 100644
index 00000000000..0758f8a3f53
--- /dev/null
+++ b/config/feature_flags/development/cached_commits.yml
@@ -0,0 +1,8 @@
+---
+name: cached_commits
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61617
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330968
+milestone: '13.12'
+type: development
+group: group::source code
+default_enabled: false
diff --git a/config/helpers/vendor_dll_hash.js b/config/helpers/vendor_dll_hash.js
index cdbaebc9789..9b99b4c4ae9 100644
--- a/config/helpers/vendor_dll_hash.js
+++ b/config/helpers/vendor_dll_hash.js
@@ -1,5 +1,5 @@
-const fs = require('fs');
const crypto = require('crypto');
+const fs = require('fs');
const path = require('path');
const CACHE_PATHS = [
diff --git a/config/karma.config.js b/config/karma.config.js
index 1c2dd21c189..3e125759357 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -1,8 +1,8 @@
/* eslint-disable no-inner-declarations, no-param-reassign */
+const path = require('path');
const chalk = require('chalk');
const argumentsParser = require('commander');
const glob = require('glob');
-const path = require('path');
const webpack = require('webpack');
const IS_EE = require('./helpers/is_ee_env');
const webpackConfig = require('./webpack.config.js');
diff --git a/config/webpack.config.js b/config/webpack.config.js
index d48b22f8024..d40d14e1158 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -1,11 +1,11 @@
const fs = require('fs');
+const path = require('path');
const SOURCEGRAPH_VERSION = require('@sourcegraph/code-host-integration/package.json').version;
const CompressionPlugin = require('compression-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const glob = require('glob');
-const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const VUE_LOADER_VERSION = require('vue-loader/package.json').version;
const VUE_VERSION = require('vue/package.json').version;
diff --git a/doc/ci/jobs/job_control.md b/doc/ci/jobs/job_control.md
index 14555a05672..6e9197c223b 100644
--- a/doc/ci/jobs/job_control.md
+++ b/doc/ci/jobs/job_control.md
@@ -63,7 +63,7 @@ except `main` and branches that start with `release/`.
### `only: variables` / `except: variables` examples
-You can use `except:variables` to exclude jobs based on a commit message:
+You can use [`except:variables`](../yaml/README.md#onlyvariables--exceptvariables) to exclude jobs based on a commit message:
```yaml
end-to-end:
@@ -223,6 +223,48 @@ test:
- "README.md"
```
+## Use predefined CI/CD variables to run jobs only in specific pipeline types
+
+You can use [predefined CI/CD variables](../variables/predefined_variables.md) to choose
+which pipeline types jobs run in, with:
+
+- [`rules`](../yaml/README.md#rules)
+- [`only:variables`](../yaml/README.md#onlyvariables--exceptvariables)
+- [`except:variables`](../yaml/README.md#onlyvariables--exceptvariables)
+
+The following table lists some of the variables that you can use, and the pipeline
+types the variables can control for:
+
+- Branch pipelines that run for Git `push` events to a branch, like new commits or tags.
+- Tag pipelines that run only when a new Git tag is pushed to a branch.
+- [Merge request pipelines](../merge_request_pipelines/index.md) that run for changes
+ to a merge request, like new commits or selecting the **Run pipeline** button
+ in a merge request's pipelines tab.
+- [Scheduled pipelines](../pipelines/schedules.md).
+
+| Variables | Branch | Tag | Merge request | Scheduled |
+|--------------------------------------------|--------|-----|---------------|-----------|
+| `CI_COMMIT_BRANCH` | Yes | | | Yes |
+| `CI_COMMIT_TAG` | | Yes | | Yes, if the scheduled pipeline is configured to run on a tag. |
+| `CI_PIPELINE_SOURCE = push` | Yes | Yes | | |
+| `CI_PIPELINE_SOURCE = scheduled` | | | | Yes |
+| `CI_PIPELINE_SOURCE = merge_request_event` | | | Yes | |
+| `CI_MERGE_REQUEST_IID` | | | Yes | |
+
+For example, to configure a job to run for merge request pipelines and scheduled pipelines,
+but not branch or tag pipelines:
+
+```yaml
+job1:
+ script:
+ - echo
+ rules:
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+ - if: $CI_PIPELINE_SOURCE == "scheduled"
+ - if: $CI_PIPELINE_SOURCE == "push"
+ when: never
+```
+
## Regular expressions
The `@` symbol denotes the beginning of a ref's repository path.
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 4b30d08ff10..1f323a311e1 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -36,14 +36,14 @@ The keywords available for jobs are:
| [`coverage`](#coverage) | Code coverage settings for a given job. |
| [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. |
| [`environment`](#environment) | Name of an environment to which the job deploys. |
-| [`except`](#only--except) | Control when jobs are not created. |
+| [`except`](#only--except) | Control when jobs are not created. |
| [`extends`](#extends) | Configuration entries that this job inherits from. |
| [`image`](#image) | Use Docker images. |
| [`include`](#include) | Include external YAML files. |
| [`inherit`](#inherit) | Select which global defaults all jobs inherit. |
| [`interruptible`](#interruptible) | Defines if a job can be canceled when made redundant by a newer run. |
| [`needs`](#needs) | Execute jobs earlier than the stage ordering. |
-| [`only`](#only--except) | Control when jobs are created. |
+| [`only`](#only--except) | Control when jobs are created. |
| [`pages`](#pages) | Upload the result of a job to use with GitLab Pages. |
| [`parallel`](#parallel) | How many instances of a job should be run in parallel. |
| [`release`](#release) | Instructs the runner to generate a [release](../../user/project/releases/index.md) object. |
@@ -1615,7 +1615,7 @@ Four keywords can be used with `only` and `except`:
- [`changes`](#onlychanges--exceptchanges)
- [`kubernetes`](#onlykubernetes--exceptkubernetes)
-See [control jobs with `only` and `except`](../jobs/job_control.md#specify-when-jobs-run-with-only-and-except)
+See [specify when jobs run with `only` and `except`](../jobs/job_control.md#specify-when-jobs-run-with-only-and-except)
for more details and examples.
#### `only:refs` / `except:refs`
@@ -3632,10 +3632,10 @@ failure.
`artifacts:when` can be set to one of the following values:
1. `on_success` (default): Upload artifacts only when the job succeeds.
-1. `on_failure`: Upload artifacts only when the job fails. Useful, for example, when
+1. `on_failure`: Upload artifacts only when the job fails.
+1. `always`: Always upload artifacts. Useful, for example, when
[uploading artifacts](../unit_test_reports.md#viewing-junit-screenshots-on-gitlab) required to
troubleshoot failing tests.
-1. `always`: Always upload artifacts.
For example, to upload artifacts only when a job fails:
diff --git a/doc/development/stage_group_dashboards.md b/doc/development/stage_group_dashboards.md
index 58e998e46a8..d6d607df2dd 100644
--- a/doc/development/stage_group_dashboards.md
+++ b/doc/development/stage_group_dashboards.md
@@ -57,7 +57,7 @@ component has 2 indicators:
The calculation to a ratio then happens as follows:
```math
-\frac {operations\_meeting\_apdex + (total\_operations - operations\_with_\errors)} {total\_apdex\_measurements + total\_operations}
+\frac {operations\_meeting\_apdex + (total\_operations - operations\_with\_errors)} {total\_apdex\_measurements + total\_operations}
```
*Caveat:* Not all components are included, causing the
diff --git a/doc/development/usage_ping/index.md b/doc/development/usage_ping/index.md
index e79cd127d5a..292e1256cb8 100644
--- a/doc/development/usage_ping/index.md
+++ b/doc/development/usage_ping/index.md
@@ -266,7 +266,7 @@ To remove a deprecated metric:
[fixtures](https://gitlab.com/gitlab-services/version-gitlab-com/-/blob/master/spec/support/usage_data_helpers.rb#L540)
used to test
[`UsageDataController#create`](https://gitlab.com/gitlab-services/version-gitlab-com/-/blob/3760ef28/spec/controllers/usage_data_controller_spec.rb#L75)
- endpoint, and assure that test suite does not fail when metric that you wish to remove is not included into test payload.
+ endpoint, and assure that test suite does not fail when metric that you wish to remove is not included into test payload.
1. Create an issue in the
[GitLab Data Team project](https://gitlab.com/gitlab-data/analytics/-/issues).
@@ -276,7 +276,7 @@ To remove a deprecated metric:
This step can be skipped if verification done during [deprecation process](#3-deprecate-a-metric)
reported that metric is not required by any data transformation in Snowflake data warehouse nor it is
used by any of SiSense dashboards.
-
+
1. After you verify the metric can be safely removed,
update the attributes of the metric's YAML definition:
@@ -1024,7 +1024,13 @@ On GitLab.com, we have DangerBot setup to monitor Product Intelligence related f
### 10. Verify your metric
-On GitLab.com, the Product Intelligence team regularly monitors Usage Ping. They may alert you that your metrics need further optimization to run quicker and with greater success. You may also use the [Usage Ping QA dashboard](https://app.periscopedata.com/app/gitlab/632033/Usage-Ping-QA) to check how well your metric performs. The dashboard allows filtering by GitLab version, by "Self-managed" & "SaaS" and shows you how many failures have occurred for each metric. Whenever you notice a high failure rate, you may re-optimize your metric.
+On GitLab.com, the Product Intelligence team regularly [monitors Usage Ping](https://gitlab.com/groups/gitlab-org/-/epics/6000).
+They may alert you that your metrics need further optimization to run quicker and with greater success.
+
+The Usage Ping JSON payload for GitLab.com is shared in the
+[#g_product_intelligence](https://gitlab.slack.com/archives/CL3A7GFPF) Slack channel every week.
+
+You may also use the [Usage Ping QA dashboard](https://app.periscopedata.com/app/gitlab/632033/Usage-Ping-QA) to check how well your metric performs. The dashboard allows filtering by GitLab version, by "Self-managed" & "SaaS" and shows you how many failures have occurred for each metric. Whenever you notice a high failure rate, you may re-optimize your metric.
### Usage Ping local setup
diff --git a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.latest.gitlab-ci.yml
new file mode 100644
index 00000000000..5216a46745c
--- /dev/null
+++ b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.latest.gitlab-ci.yml
@@ -0,0 +1,77 @@
+# Read more about the feature here: https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html
+
+browser_performance:
+ stage: performance
+ image: docker:19.03.12
+ allow_failure: true
+ variables:
+ DOCKER_TLS_CERTDIR: ""
+ SITESPEED_IMAGE: sitespeedio/sitespeed.io
+ SITESPEED_VERSION: 14.1.0
+ SITESPEED_OPTIONS: ''
+ services:
+ - docker:19.03.12-dind
+ script:
+ - |
+ if ! docker info &>/dev/null; then
+ if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
+ export DOCKER_HOST='tcp://localhost:2375'
+ fi
+ fi
+ - export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
+ - mkdir gitlab-exporter
+ # Busybox wget does not support proxied HTTPS, get the real thing.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/287611.
+ - (env | grep -i _proxy= 2>&1 >/dev/null) && apk --no-cache add wget
+ - wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.1.0/index.js
+ - mkdir sitespeed-results
+ - |
+ function propagate_env_vars() {
+ CURRENT_ENV=$(printenv)
+
+ for VAR_NAME; do
+ echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME "
+ done
+ }
+ - |
+ if [ -f .gitlab-urls.txt ]
+ then
+ sed -i -e 's@^@'"$CI_ENVIRONMENT_URL"'@' .gitlab-urls.txt
+ docker run \
+ $(propagate_env_vars \
+ auto_proxy \
+ https_proxy \
+ http_proxy \
+ no_proxy \
+ AUTO_PROXY \
+ HTTPS_PROXY \
+ HTTP_PROXY \
+ NO_PROXY \
+ ) \
+ --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results .gitlab-urls.txt $SITESPEED_OPTIONS
+ else
+ docker run \
+ $(propagate_env_vars \
+ auto_proxy \
+ https_proxy \
+ http_proxy \
+ no_proxy \
+ AUTO_PROXY \
+ HTTPS_PROXY \
+ HTTP_PROXY \
+ NO_PROXY \
+ ) \
+ --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" $SITESPEED_OPTIONS
+ fi
+ - mv sitespeed-results/data/performance.json browser-performance.json
+ artifacts:
+ paths:
+ - sitespeed-results/
+ reports:
+ browser_performance: browser-performance.json
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$PERFORMANCE_DISABLED'
+ when: never
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
diff --git a/lib/gitlab/default_branch.rb b/lib/gitlab/default_branch.rb
index 73c9ac07753..6bd9a5675c4 100644
--- a/lib/gitlab/default_branch.rb
+++ b/lib/gitlab/default_branch.rb
@@ -3,8 +3,8 @@
# Class is used while we're migrating from master to main
module Gitlab
module DefaultBranch
- def self.value(project: nil)
- Feature.enabled?(:main_branch_over_master, project, default_enabled: :yaml) ? 'main' : 'master'
+ def self.value(object: nil)
+ Feature.enabled?(:main_branch_over_master, object, default_enabled: :yaml) ? 'main' : 'master'
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8087d3c3c32..01a5e40941a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6334,7 +6334,7 @@ msgstr ""
msgid "Choose File..."
msgstr ""
-msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
+msgid "Choose a branch/tag (e.g. %{branch}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
msgid "Choose a file"
@@ -17894,7 +17894,7 @@ msgstr ""
msgid "InviteEmail|As a guest, you can view projects, leave comments, and create issues."
msgstr ""
-msgid "InviteEmail|As a maintainer, you have full access to projects. You can push commits to master and deploy to production."
+msgid "InviteEmail|As a maintainer, you have full access to projects. You can push commits to the default branch and deploy to production."
msgstr ""
msgid "InviteEmail|As a reporter, you can view projects and reports, and leave comments on issues."
diff --git a/package.json b/package.json
index 57d229cd8e9..173f618e5e9 100644
--- a/package.json
+++ b/package.json
@@ -186,7 +186,7 @@
},
"devDependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.10.1",
- "@gitlab/eslint-plugin": "8.3.0",
+ "@gitlab/eslint-plugin": "8.4.0",
"@gitlab/stylelint-config": "2.3.0",
"@testing-library/dom": "^7.16.2",
"@vue/test-utils": "1.1.2",
@@ -201,7 +201,7 @@
"docdash": "^1.0.2",
"eslint": "7.26.0",
"eslint-import-resolver-jest": "3.0.0",
- "eslint-import-resolver-webpack": "0.13.0",
+ "eslint-import-resolver-webpack": "0.13.1",
"eslint-plugin-jasmine": "4.1.2",
"eslint-plugin-no-jquery": "2.6.0",
"gettext-extractor": "^3.5.3",
diff --git a/scripts/frontend/check_page_bundle_mixins_css_for_sideeffects.js b/scripts/frontend/check_page_bundle_mixins_css_for_sideeffects.js
index 22a4aac762b..4c439caa1da 100755
--- a/scripts/frontend/check_page_bundle_mixins_css_for_sideeffects.js
+++ b/scripts/frontend/check_page_bundle_mixins_css_for_sideeffects.js
@@ -8,8 +8,8 @@ if (process.env.RAILS_ENV !== 'production') {
}
const fs = require('fs');
-const glob = require('glob');
const path = require('path');
+const glob = require('glob');
const pjs = require('postcss');
const paths = glob.sync('public/assets/page_bundles/_mixins_and_variables_and_functions*.css', {
diff --git a/scripts/frontend/merge_coverage_frontend.js b/scripts/frontend/merge_coverage_frontend.js
index 6b3826ddac7..bd56b0d5868 100644
--- a/scripts/frontend/merge_coverage_frontend.js
+++ b/scripts/frontend/merge_coverage_frontend.js
@@ -1,8 +1,8 @@
+const { resolve } = require('path');
const { sync } = require('glob');
const { createCoverageMap } = require('istanbul-lib-coverage');
const { createContext } = require('istanbul-lib-report');
const { create } = require('istanbul-reports');
-const { resolve } = require('path');
const coverageMap = createCoverageMap();
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
index 9e96c154546..b2ed79cd75a 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
@@ -1,3 +1,6 @@
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+
+import AccessorUtilities from '~/lib/utils/accessor';
import {
stripQuotes,
uniqueTokens,
@@ -5,6 +8,8 @@ import {
processFilters,
filterToQueryObject,
urlQueryToFilter,
+ getRecentlyUsedTokenValues,
+ setTokenValueToRecentlyUsed,
} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import {
@@ -14,6 +19,12 @@ import {
tokenValuePlain,
} from './mock_data';
+const mockStorageKey = 'recent-tokens';
+
+function setLocalStorageAvailability(isAvailable) {
+ jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(isAvailable);
+}
+
describe('Filtered Search Utils', () => {
describe('stripQuotes', () => {
it.each`
@@ -249,3 +260,79 @@ describe('urlQueryToFilter', () => {
expect(res).toEqual(result);
});
});
+
+describe('getRecentlyUsedTokenValues', () => {
+ useLocalStorageSpy();
+
+ beforeEach(() => {
+ localStorage.removeItem(mockStorageKey);
+ });
+
+ it('returns array containing recently used token values from provided recentTokenValuesStorageKey', () => {
+ setLocalStorageAvailability(true);
+
+ const mockExpectedArray = [{ foo: 'bar' }];
+ localStorage.setItem(mockStorageKey, JSON.stringify(mockExpectedArray));
+
+ expect(getRecentlyUsedTokenValues(mockStorageKey)).toEqual(mockExpectedArray);
+ });
+
+ it('returns empty array when provided recentTokenValuesStorageKey does not have anything in localStorage', () => {
+ setLocalStorageAvailability(true);
+
+ expect(getRecentlyUsedTokenValues(mockStorageKey)).toEqual([]);
+ });
+
+ it('returns empty array when when access to localStorage is not available', () => {
+ setLocalStorageAvailability(false);
+
+ expect(getRecentlyUsedTokenValues(mockStorageKey)).toEqual([]);
+ });
+});
+
+describe('setTokenValueToRecentlyUsed', () => {
+ const mockTokenValue1 = { foo: 'bar' };
+ const mockTokenValue2 = { bar: 'baz' };
+ useLocalStorageSpy();
+
+ beforeEach(() => {
+ localStorage.removeItem(mockStorageKey);
+ });
+
+ it('adds provided tokenValue to localStorage for recentTokenValuesStorageKey', () => {
+ setLocalStorageAvailability(true);
+
+ setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1);
+
+ expect(JSON.parse(localStorage.getItem(mockStorageKey))).toEqual([mockTokenValue1]);
+ });
+
+ it('adds provided tokenValue to localStorage at the top of existing values (i.e. Stack order)', () => {
+ setLocalStorageAvailability(true);
+
+ setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1);
+ setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue2);
+
+ expect(JSON.parse(localStorage.getItem(mockStorageKey))).toEqual([
+ mockTokenValue2,
+ mockTokenValue1,
+ ]);
+ });
+
+ it('ensures that provided tokenValue is not added twice', () => {
+ setLocalStorageAvailability(true);
+
+ setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1);
+ setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1);
+
+ expect(JSON.parse(localStorage.getItem(mockStorageKey))).toEqual([mockTokenValue1]);
+ });
+
+ it('does not add any value when acess to localStorage is not available', () => {
+ setLocalStorageAvailability(false);
+
+ setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1);
+
+ expect(JSON.parse(localStorage.getItem(mockStorageKey))).toBeNull();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
new file mode 100644
index 00000000000..0db47f1f189
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
@@ -0,0 +1,228 @@
+import { GlFilteredSearchToken } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import {
+ mockRegularLabel,
+ mockLabels,
+} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
+
+import { DEFAULT_LABELS } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ getRecentlyUsedTokenValues,
+ setTokenValueToRecentlyUsed,
+} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+
+import { mockLabelToken } from '../mock_data';
+
+jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils');
+
+const mockStorageKey = 'recent-tokens-label_name';
+
+const defaultStubs = {
+ Portal: true,
+ GlFilteredSearchToken: {
+ template: `
+ <div>
+ <slot name="view-token"></slot>
+ <slot name="view"></slot>
+ </div>
+ `,
+ },
+ GlFilteredSearchSuggestionList: {
+ template: '<div></div>',
+ methods: {
+ getValue: () => '=',
+ },
+ },
+};
+
+const defaultSlots = {
+ 'view-token': `
+ <div class="js-view-token">${mockRegularLabel.title}</div>
+ `,
+ view: `
+ <div class="js-view">${mockRegularLabel.title}</div>
+ `,
+};
+
+const mockProps = {
+ tokenConfig: mockLabelToken,
+ tokenValue: { data: '' },
+ tokenActive: false,
+ tokensListLoading: false,
+ tokenValues: [],
+ fnActiveTokenValue: jest.fn(),
+ defaultTokenValues: DEFAULT_LABELS,
+ recentTokenValuesStorageKey: mockStorageKey,
+ fnCurrentTokenValue: jest.fn(),
+};
+
+function createComponent({
+ props = { ...mockProps },
+ stubs = defaultStubs,
+ slots = defaultSlots,
+} = {}) {
+ return mount(BaseToken, {
+ propsData: {
+ ...props,
+ },
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: jest.fn(),
+ suggestionsListClass: 'custom-class',
+ },
+ stubs,
+ slots,
+ });
+}
+
+describe('BaseToken', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent({
+ props: {
+ ...mockProps,
+ tokenValue: { data: `"${mockRegularLabel.title}"` },
+ tokenValues: mockLabels,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('data', () => {
+ it('calls `getRecentlyUsedTokenValues` to populate `recentTokenValues` when `recentTokenValuesStorageKey` is defined', () => {
+ expect(getRecentlyUsedTokenValues).toHaveBeenCalledWith(mockStorageKey);
+ });
+ });
+
+ describe('computed', () => {
+ describe('currentTokenValue', () => {
+ it('calls `fnCurrentTokenValue` when it is provided', () => {
+ // We're disabling lint to trigger computed prop execution for this test.
+ // eslint-disable-next-line no-unused-vars
+ const { currentTokenValue } = wrapper.vm;
+
+ expect(wrapper.vm.fnCurrentTokenValue).toHaveBeenCalledWith(`"${mockRegularLabel.title}"`);
+ });
+ });
+
+ describe('activeTokenValue', () => {
+ it('calls `fnActiveTokenValue` when it is provided', async () => {
+ wrapper.setProps({
+ fnCurrentTokenValue: undefined,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ // We're disabling lint to trigger computed prop execution for this test.
+ // eslint-disable-next-line no-unused-vars
+ const { activeTokenValue } = wrapper.vm;
+
+ expect(wrapper.vm.fnActiveTokenValue).toHaveBeenCalledWith(
+ mockLabels,
+ `"${mockRegularLabel.title.toLowerCase()}"`,
+ );
+ });
+ });
+ });
+
+ describe('watch', () => {
+ describe('tokenActive', () => {
+ let wrapperWithTokenActive;
+
+ beforeEach(() => {
+ wrapperWithTokenActive = createComponent({
+ props: {
+ ...mockProps,
+ tokenActive: true,
+ tokenValue: { data: `"${mockRegularLabel.title}"` },
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapperWithTokenActive.destroy();
+ });
+
+ it('emits `fetch-token-values` event on the component when value of this prop is changed to false and `tokenValues` array is empty', async () => {
+ wrapperWithTokenActive.setProps({
+ tokenActive: false,
+ });
+
+ await wrapperWithTokenActive.vm.$nextTick();
+
+ expect(wrapperWithTokenActive.emitted('fetch-token-values')).toBeTruthy();
+ expect(wrapperWithTokenActive.emitted('fetch-token-values')).toEqual([
+ [`"${mockRegularLabel.title}"`],
+ ]);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('handleTokenValueSelected', () => {
+ it('calls `setTokenValueToRecentlyUsed` when `recentTokenValuesStorageKey` is defined', () => {
+ const mockTokenValue = {
+ id: 1,
+ title: 'Foo',
+ };
+
+ wrapper.vm.handleTokenValueSelected(mockTokenValue);
+
+ expect(setTokenValueToRecentlyUsed).toHaveBeenCalledWith(mockStorageKey, mockTokenValue);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders gl-filtered-search-token component', () => {
+ const wrapperWithNoStubs = createComponent({
+ stubs: {},
+ });
+ const glFilteredSearchToken = wrapperWithNoStubs.find(GlFilteredSearchToken);
+
+ expect(glFilteredSearchToken.exists()).toBe(true);
+ expect(glFilteredSearchToken.props('config')).toBe(mockLabelToken);
+
+ wrapperWithNoStubs.destroy();
+ });
+
+ it('renders `view-token` slot when present', () => {
+ expect(wrapper.find('.js-view-token').exists()).toBe(true);
+ });
+
+ it('renders `view` slot when present', () => {
+ expect(wrapper.find('.js-view').exists()).toBe(true);
+ });
+
+ describe('events', () => {
+ let wrapperWithNoStubs;
+
+ beforeEach(() => {
+ wrapperWithNoStubs = createComponent({
+ stubs: { Portal: true },
+ });
+ });
+
+ afterEach(() => {
+ wrapperWithNoStubs.destroy();
+ });
+
+ it('emits `fetch-token-values` event on component after a delay when component emits `input` event', async () => {
+ jest.useFakeTimers();
+
+ wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' });
+ await wrapperWithNoStubs.vm.$nextTick();
+
+ jest.runAllTimers();
+
+ expect(wrapperWithNoStubs.emitted('fetch-token-values')).toBeTruthy();
+ expect(wrapperWithNoStubs.emitted('fetch-token-values')[1]).toEqual(['foo']);
+ });
+ });
+ });
+});
diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb
index 53d204a697d..657ab8a81f0 100644
--- a/spec/helpers/commits_helper_spec.rb
+++ b/spec/helpers/commits_helper_spec.rb
@@ -289,4 +289,38 @@ RSpec.describe CommitsHelper do
}
end
end
+
+ describe "#commit_partial_cache_key" do
+ subject { helper.commit_partial_cache_key(commit, ref: ref, merge_request: merge_request, request: request) }
+
+ let(:commit) { create(:commit).present(current_user: user) }
+ let(:commit_status) { create(:commit_status) }
+ let(:user) { create(:user) }
+ let(:ref) { "master" }
+ let(:merge_request) { nil }
+ let(:request) { double(xhr?: true) }
+ let(:current_path) { "test" }
+
+ before do
+ expect(commit).to receive(:status_for).with(ref).and_return(commit_status)
+ assign(:path, current_path)
+ end
+
+ it { is_expected.to be_an(Array) }
+ it { is_expected.to include(commit) }
+ it { is_expected.to include(commit.author) }
+ it { is_expected.to include(ref) }
+
+ it do
+ is_expected.to include(
+ {
+ merge_request: merge_request,
+ pipeline_status: Digest::SHA1.hexdigest(commit_status.to_s),
+ xhr: true,
+ controller: "commits",
+ path: current_path
+ }
+ )
+ end
+ end
end
diff --git a/yarn.lock b/yarn.lock
index 2353ff71ad5..c0d7f547a46 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -867,10 +867,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.7.tgz#1ee6f838cc4410a1d797770934df91d90df8179e"
integrity sha512-c6ySRK/Ma7lxwpIVbSAF3P+xiTLrNTGTLRx4/pHK111AdFxwgUwrYF6aVZFXvmG65jHOJHoa0eQQ21RW6rm0Rg==
-"@gitlab/eslint-plugin@8.3.0":
- version "8.3.0"
- resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-8.3.0.tgz#156a375c6ab9e578ba39080932bca27006413486"
- integrity sha512-AuJ6ddKVbfjVUd9DLaNLhpflThZKULWatpUuI+0RhcqyRTmcb1KL5YPxxKDlE1K+faeefgiWaGB+vSNmyNNPQQ==
+"@gitlab/eslint-plugin@8.4.0":
+ version "8.4.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-8.4.0.tgz#094fa4d41676a71146f82e1b19257a7ceabefd88"
+ integrity sha512-VE/c1yIMrj2igJWAALQtAKpnXL8fN5wJ1uKteZfi8xYbWouoUK6hizXSPrrEUWiM2FqcBI4Igcpz2JlJzDlAnA==
dependencies:
babel-eslint "^10.0.3"
eslint-config-airbnb-base "^14.2.1"
@@ -881,6 +881,7 @@
eslint-plugin-jest "^23.8.2"
eslint-plugin-promise "^4.2.1"
eslint-plugin-vue "^7.5.0"
+ lodash "4.17.20"
vue-eslint-parser "^7.0.0"
"@gitlab/favicon-overlay@2.0.0":
@@ -4142,10 +4143,10 @@ debug@3.1.0, debug@~3.1.0:
dependencies:
ms "2.0.0"
-debug@^3.1.0, debug@^3.1.1, debug@^3.2.6:
- version "3.2.6"
- resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
- integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+debug@^3.1.0, debug@^3.1.1, debug@^3.2.6, debug@^3.2.7:
+ version "3.2.7"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+ integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
dependencies:
ms "^2.1.1"
@@ -4817,20 +4818,21 @@ eslint-import-resolver-node@^0.3.4:
debug "^2.6.9"
resolve "^1.13.1"
-eslint-import-resolver-webpack@0.13.0:
- version "0.13.0"
- resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.0.tgz#5cb19cf4b6996c8a2514aeb10f909e2c70488dc3"
- integrity sha512-hZWGcmjaJZK/WSCYGI/y4+FMGQZT+cwW/1E/P4rDwFj2PbanlQHISViw4ccDJ+2wxAqjgwBfxwy3seABbVKDEw==
+eslint-import-resolver-webpack@0.13.1:
+ version "0.13.1"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.1.tgz#6d2fb928091daf2da46efa1e568055555b2de902"
+ integrity sha512-O/8mG6AHmaKYSMb4lWxiXPpaARxOJ4rMQEHJ8vTgjS1MXooJA3KPgBPPAdOPoV17v5ML5120qod5FBLM+DtgEw==
dependencies:
array-find "^1.0.0"
- debug "^2.6.9"
+ debug "^3.2.7"
enhanced-resolve "^0.9.1"
find-root "^1.1.0"
has "^1.0.3"
- interpret "^1.2.0"
- lodash "^4.17.15"
- node-libs-browser "^1.0.0 || ^2.0.0"
- resolve "^1.13.1"
+ interpret "^1.4.0"
+ is-core-module "^2.4.0"
+ is-regex "^1.1.3"
+ lodash "^4.17.21"
+ resolve "^1.20.0"
semver "^5.7.1"
eslint-module-utils@^2.6.0:
@@ -5896,10 +5898,10 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
-has-symbols@^1.0.0, has-symbols@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
- integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
+ integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
has-value@^0.3.1:
version "0.3.1"
@@ -6311,7 +6313,7 @@ internal-ip@^4.3.0:
default-gateway "^4.2.0"
ipaddr.js "^1.9.0"
-interpret@^1.2.0, interpret@^1.4.0:
+interpret@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
@@ -6409,6 +6411,13 @@ is-ci@^2.0.0:
dependencies:
ci-info "^2.0.0"
+is-core-module@^2.2.0, is-core-module@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1"
+ integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==
+ dependencies:
+ has "^1.0.3"
+
is-data-descriptor@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -6581,13 +6590,13 @@ is-potential-custom-element-name@^1.0.0:
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397"
integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c=
-is-regex@^1.1.1:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251"
- integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==
+is-regex@^1.1.1, is-regex@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
+ integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==
dependencies:
call-bind "^1.0.2"
- has-symbols "^1.0.1"
+ has-symbols "^1.0.2"
is-regexp@^2.0.0:
version "2.1.0"
@@ -7866,6 +7875,11 @@ lodash.values@^4.3.0:
resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347"
integrity sha1-o6bCsOvsxcLLocF+bmIP6BtT00c=
+lodash@4.17.20:
+ version "4.17.20"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
+ integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
+
lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
@@ -8592,7 +8606,7 @@ node-int64@^0.4.0:
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=
-"node-libs-browser@^1.0.0 || ^2.0.0", node-libs-browser@^2.2.1:
+node-libs-browser@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"
integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==
@@ -10170,11 +10184,12 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
-resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0:
- version "1.17.0"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
- integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
+resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.20.0:
+ version "1.20.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
+ integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
dependencies:
+ is-core-module "^2.2.0"
path-parse "^1.0.6"
responselike@^1.0.2: