diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-12 21:09:26 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-12 21:09:26 +0300 |
commit | f4182abcb628e20978f011376811bbf8e644eff5 (patch) | |
tree | bb7886935855da9f69571b4970cfc5519dd40f2a | |
parent | 6cf30e964d54d536b0ff861916745f0a4bb31ebb (diff) |
Add latest changes from gitlab-org/gitlab@master
78 files changed, 1587 insertions, 362 deletions
@@ -159,7 +159,7 @@ gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 2.0.10' gem 'asciidoctor-include-ext', '~> 0.3.1', require: false gem 'asciidoctor-plantuml', '~> 0.0.12' -gem 'rouge', '~> 3.24.0' +gem 'rouge', '~> 3.25.0' gem 'truncato', '~> 0.7.11' gem 'bootstrap_form', '~> 4.2.0' gem 'nokogiri', '~> 1.10.9' diff --git a/Gemfile.lock b/Gemfile.lock index 764172e62f9..58f5f7753ec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -977,7 +977,7 @@ GEM rexml (3.2.4) rinku (2.0.0) rotp (2.1.2) - rouge (3.24.0) + rouge (3.25.0) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) @@ -1468,7 +1468,7 @@ DEPENDENCIES request_store (~> 1.5) responders (~> 3.0) retriable (~> 3.1.2) - rouge (~> 3.24.0) + rouge (~> 3.25.0) rqrcode-rails3 (~> 0.1.7) rspec-parameterized rspec-rails (~> 4.0.0) diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index 2d426ee663a..f84e39baa53 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -8,11 +8,40 @@ import initPopover from '~/blob/suggest_gitlab_ci_yml'; import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils'; import Tracking from '~/tracking'; +const initPopovers = () => { + const suggestEl = document.querySelector('.js-suggest-gitlab-ci-yml'); + + if (suggestEl) { + const commitButton = document.querySelector('#commit-changes'); + + initPopover(suggestEl); + + if (commitButton) { + const { dismissKey, humanAccess } = suggestEl.dataset; + const urlParams = new URLSearchParams(window.location.search); + const mergeRequestPath = urlParams.get('mr_path') || true; + + const commitCookieName = `suggest_gitlab_ci_yml_commit_${dismissKey}`; + const commitTrackLabel = 'suggest_gitlab_ci_yml_commit_changes'; + const commitTrackValue = '20'; + + commitButton.addEventListener('click', () => { + setCookie(commitCookieName, mergeRequestPath); + + Tracking.event(undefined, 'click_button', { + label: commitTrackLabel, + property: humanAccess, + value: commitTrackValue, + }); + }); + } + } +}; + export default () => { const editBlobForm = $('.js-edit-blob-form'); const uploadBlobForm = $('.js-upload-blob-form'); const deleteBlobForm = $('.js-delete-blob-form'); - const suggestEl = document.querySelector('.js-suggest-gitlab-ci-yml'); if (editBlobForm.length) { const urlRoot = editBlobForm.data('relativeUrlRoot'); @@ -33,6 +62,7 @@ export default () => { projectId, isMarkdown, }); + initPopovers(); }) .catch(e => createFlash(e)); @@ -62,30 +92,4 @@ export default () => { if (deleteBlobForm.length) { new NewCommitForm(deleteBlobForm); } - - if (suggestEl) { - const commitButton = document.querySelector('#commit-changes'); - - initPopover(suggestEl); - - if (commitButton) { - const { dismissKey, humanAccess } = suggestEl.dataset; - const urlParams = new URLSearchParams(window.location.search); - const mergeRequestPath = urlParams.get('mr_path') || true; - - const commitCookieName = `suggest_gitlab_ci_yml_commit_${dismissKey}`; - const commitTrackLabel = 'suggest_gitlab_ci_yml_commit_changes'; - const commitTrackValue = '20'; - - commitButton.addEventListener('click', () => { - setCookie(commitCookieName, mergeRequestPath); - - Tracking.event(undefined, 'click_button', { - label: commitTrackLabel, - property: humanAccess, - value: commitTrackValue, - }); - }); - } - } }; diff --git a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue index 2245c0d0f44..c81f171af2b 100644 --- a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue +++ b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue @@ -1,13 +1,22 @@ <script> import { mapActions, mapGetters } from 'vuex'; -import { GlDropdownItem, GlDropdownDivider, GlAvatarLabeled, GlAvatarLink } from '@gitlab/ui'; +import { + GlDropdownItem, + GlDropdownDivider, + GlAvatarLabeled, + GlAvatarLink, + GlSearchBoxByType, +} from '@gitlab/ui'; import { __, n__ } from '~/locale'; import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue'; import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue'; import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql'; +import searchUsers from '~/boards/queries/users_search.query.graphql'; export default { + noSearchDelay: 0, + searchDelay: 250, i18n: { unassigned: __('Unassigned'), assignee: __('Assignee'), @@ -22,23 +31,42 @@ export default { GlDropdownDivider, GlAvatarLabeled, GlAvatarLink, + GlSearchBoxByType, }, data() { return { + search: '', participants: [], selected: this.$store.getters.activeIssue.assignees, }; }, apollo: { participants: { - query: getIssueParticipants, + query() { + return this.isSearchEmpty ? getIssueParticipants : searchUsers; + }, variables() { + if (this.isSearchEmpty) { + return { + id: `gid://gitlab/Issue/${this.activeIssue.iid}`, + }; + } + return { - id: `gid://gitlab/Issue/${this.activeIssue.iid}`, + search: this.search, }; }, update(data) { - return data.issue?.participants?.nodes || []; + if (this.isSearchEmpty) { + return data.issue?.participants?.nodes || []; + } + + return data.users?.nodes || []; + }, + debounce() { + const { noSearchDelay, searchDelay } = this.$options; + + return this.isSearchEmpty ? noSearchDelay : searchDelay; }, }, }, @@ -58,6 +86,9 @@ export default { selectedUserNames() { return this.selected.map(({ username }) => username); }, + isSearchEmpty() { + return this.search === ''; + }, }, methods: { ...mapActions(['setAssignees']), @@ -97,6 +128,9 @@ export default { :text="$options.i18n.assignees" :header-text="$options.i18n.assignTo" > + <template #search> + <gl-search-box-by-type v-model.trim="search" /> + </template> <template #items> <gl-dropdown-item :is-checked="selectedIsEmpty" diff --git a/app/assets/javascripts/boards/queries/users_search.query.graphql b/app/assets/javascripts/boards/queries/users_search.query.graphql new file mode 100644 index 00000000000..ca016495d79 --- /dev/null +++ b/app/assets/javascripts/boards/queries/users_search.query.graphql @@ -0,0 +1,11 @@ +query usersSearch($search: String!) { + users(search: $search) { + nodes { + username + name + webUrl + avatarUrl + id + } + } +} diff --git a/app/assets/javascripts/pages/projects/pipelines/show/index.js b/app/assets/javascripts/pages/projects/pipelines/show/index.js index 7a57e417b41..d3f46b7e025 100644 --- a/app/assets/javascripts/pages/projects/pipelines/show/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/show/index.js @@ -1,7 +1,5 @@ import initPipelineDetails from '~/pipelines/pipeline_details_bundle'; import initPipelines from '../init_pipelines'; -document.addEventListener('DOMContentLoaded', () => { - initPipelines(); - initPipelineDetails(); -}); +initPipelines(); +initPipelineDetails(); diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 67aec12655a..51d8bbeaef0 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -6,12 +6,10 @@ import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; import pipelineGraph from './components/graph/graph_component.vue'; import createDagApp from './pipeline_details_dag'; import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin'; -import PipelinesMediator from './pipeline_details_mediator'; import legacyPipelineHeader from './components/legacy_header_component.vue'; import eventHub from './event_hub'; import TestReports from './components/test_reports/test_reports.vue'; import createTestReportsStore from './stores/test_reports'; -import { createPipelineHeaderApp } from './pipeline_details_header'; Vue.use(Translate); @@ -22,7 +20,7 @@ const SELECTORS = { PIPELINE_TESTS: '#js-pipeline-tests-detail', }; -const createPipelinesDetailApp = mediator => { +const createLegacyPipelinesDetailApp = mediator => { if (!document.querySelector(SELECTORS.PIPELINE_GRAPH)) { return; } @@ -127,18 +125,48 @@ const createTestDetails = () => { }); }; -export default () => { +export default async function() { + createTestDetails(); + createDagApp(); + const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS); - const mediator = new PipelinesMediator({ endpoint: dataset.endpoint }); - mediator.fetchPipeline(); + let mediator; + + if (!gon.features.graphqlPipelineHeader || !gon.features.graphqlPipelineDetails) { + try { + const { default: PipelinesMediator } = await import( + /* webpackChunkName: 'PipelinesMediator' */ './pipeline_details_mediator' + ); + mediator = new PipelinesMediator({ endpoint: dataset.endpoint }); + mediator.fetchPipeline(); + } catch { + Flash(__('An error occurred while loading the pipeline.')); + } + } - createPipelinesDetailApp(mediator); + if (gon.features.graphqlPipelineDetails) { + try { + const { createPipelinesDetailApp } = await import( + /* webpackChunkName: 'createPipelinesDetailApp' */ './pipeline_details_graph' + ); + createPipelinesDetailApp(); + } catch { + Flash(__('An error occurred while loading the pipeline.')); + } + } else { + createLegacyPipelinesDetailApp(mediator); + } if (gon.features.graphqlPipelineHeader) { - createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER); + try { + const { createPipelineHeaderApp } = await import( + /* webpackChunkName: 'createPipelineHeaderApp' */ './pipeline_details_header' + ); + createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER); + } catch { + Flash(__('An error occurred while loading a section of this page.')); + } } else { createLegacyPipelineHeaderApp(mediator); } - createTestDetails(); - createDagApp(); -}; +} diff --git a/app/assets/javascripts/pipelines/pipeline_details_dag.js b/app/assets/javascripts/pipelines/pipeline_details_dag.js index dc03b457265..3e9bdae96dd 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_dag.js +++ b/app/assets/javascripts/pipelines/pipeline_details_dag.js @@ -10,11 +10,12 @@ const apolloProvider = new VueApollo({ }); const createDagApp = () => { - if (!window.gon?.features?.dagPipelineTab) { + const el = document.querySelector('#js-pipeline-dag-vue'); + + if (!window.gon?.features?.dagPipelineTab || !el) { return; } - const el = document.querySelector('#js-pipeline-dag-vue'); const { pipelineProjectPath, pipelineIid, emptySvgPath, dagDocPath } = el?.dataset; // eslint-disable-next-line no-new diff --git a/app/assets/javascripts/pipelines/pipeline_details_graph.js b/app/assets/javascripts/pipelines/pipeline_details_graph.js new file mode 100644 index 00000000000..880855cf21d --- /dev/null +++ b/app/assets/javascripts/pipelines/pipeline_details_graph.js @@ -0,0 +1,7 @@ +const createPipelinesDetailApp = () => { + // Placeholder. See: https://gitlab.com/gitlab-org/gitlab/-/issues/223262 + // eslint-disable-next-line no-useless-return + return; +}; + +export { createPipelinesDetailApp }; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue index 68c97ed13d7..c5bbe1b33fb 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue @@ -21,6 +21,7 @@ export default { <template> <gl-dropdown class="show" :text="text" :header-text="headerText"> + <slot name="search"></slot> <gl-dropdown-form> <slot name="items"></slot> </gl-dropdown-form> diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js new file mode 100644 index 00000000000..2f87c4e7878 --- /dev/null +++ b/app/assets/javascripts/vue_shared/security_reports/constants.js @@ -0,0 +1,3 @@ +export const FEEDBACK_TYPE_DISMISSAL = 'dismissal'; +export const FEEDBACK_TYPE_ISSUE = 'issue'; +export const FEEDBACK_TYPE_MERGE_REQUEST = 'merge_request'; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js new file mode 100644 index 00000000000..c9da824613d --- /dev/null +++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js @@ -0,0 +1,24 @@ +import { fetchDiffData } from '../../utils'; +import * as types from './mutation_types'; + +export const setDiffEndpoint = ({ commit }, path) => commit(types.SET_DIFF_ENDPOINT, path); + +export const requestDiff = ({ commit }) => commit(types.REQUEST_DIFF); + +export const receiveDiffSuccess = ({ commit }, response) => + commit(types.RECEIVE_DIFF_SUCCESS, response); + +export const receiveDiffError = ({ commit }, response) => + commit(types.RECEIVE_DIFF_ERROR, response); + +export const fetchDiff = ({ state, rootState, dispatch }) => { + dispatch('requestDiff'); + + return fetchDiffData(rootState, state.paths.diffEndpoint, 'secret_detection') + .then(data => { + dispatch('receiveDiffSuccess', data); + }) + .catch(() => { + dispatch('receiveDiffError'); + }); +}; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js new file mode 100644 index 00000000000..68c81bb4509 --- /dev/null +++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js @@ -0,0 +1,10 @@ +import state from './state'; +import mutations from './mutations'; +import * as actions from './actions'; + +export default { + namespaced: true, + state, + mutations, + actions, +}; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutation_types.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutation_types.js new file mode 100644 index 00000000000..aacec0fb679 --- /dev/null +++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutation_types.js @@ -0,0 +1,4 @@ +export const RECEIVE_DIFF_SUCCESS = 'RECEIVE_DIFF_SUCCESS'; +export const RECEIVE_DIFF_ERROR = 'RECEIVE_DIFF_ERROR'; +export const REQUEST_DIFF = 'REQUEST_DIFF'; +export const SET_DIFF_ENDPOINT = 'SET_DIFF_ENDPOINT'; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutations.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutations.js new file mode 100644 index 00000000000..ee943b0621c --- /dev/null +++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/mutations.js @@ -0,0 +1,30 @@ +import { parseDiff } from '~/vue_shared/security_reports/store/utils'; +import * as types from './mutation_types'; + +export default { + [types.SET_DIFF_ENDPOINT](state, path) { + state.paths.diffEndpoint = path; + }, + + [types.REQUEST_DIFF](state) { + state.isLoading = true; + }, + + [types.RECEIVE_DIFF_SUCCESS](state, { diff, enrichData }) { + const { added, fixed, existing } = parseDiff(diff, enrichData); + const baseReportOutofDate = diff.base_report_out_of_date || false; + const hasBaseReport = Boolean(diff.base_report_created_at); + + state.isLoading = false; + state.newIssues = added; + state.resolvedIssues = fixed; + state.allIssues = existing; + state.baseReportOutofDate = baseReportOutofDate; + state.hasBaseReport = hasBaseReport; + }, + + [types.RECEIVE_DIFF_ERROR](state) { + state.isLoading = false; + state.hasError = true; + }, +}; diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/state.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/state.js new file mode 100644 index 00000000000..e860e3af924 --- /dev/null +++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/state.js @@ -0,0 +1,16 @@ +export default () => ({ + paths: { + head: null, + base: null, + diffEndpoint: null, + }, + + isLoading: false, + hasError: false, + + newIssues: [], + resolvedIssues: [], + allIssues: [], + baseReportOutofDate: false, + hasBaseReport: false, +}); diff --git a/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/app/assets/javascripts/vue_shared/security_reports/store/utils.js index 9a3581e276e..6e50efae741 100644 --- a/app/assets/javascripts/vue_shared/security_reports/store/utils.js +++ b/app/assets/javascripts/vue_shared/security_reports/store/utils.js @@ -1,5 +1,10 @@ import pollUntilComplete from '~/lib/utils/poll_until_complete'; import axios from '~/lib/utils/axios_utils'; +import { + FEEDBACK_TYPE_DISMISSAL, + FEEDBACK_TYPE_ISSUE, + FEEDBACK_TYPE_MERGE_REQUEST, +} from '../constants'; export const fetchDiffData = (state, endpoint, category) => { const requests = [pollUntilComplete(endpoint)]; @@ -24,21 +29,21 @@ export const enrichVulnerabilityWithFeedback = (vulnerability, feedback = []) => feedback .filter(fb => fb.project_fingerprint === vulnerability.project_fingerprint) .reduce((vuln, fb) => { - if (fb.feedback_type === 'dismissal') { + if (fb.feedback_type === FEEDBACK_TYPE_DISMISSAL) { return { ...vuln, isDismissed: true, dismissalFeedback: fb, }; } - if (fb.feedback_type === 'issue' && fb.issue_iid) { + if (fb.feedback_type === FEEDBACK_TYPE_ISSUE && fb.issue_iid) { return { ...vuln, hasIssue: true, issue_feedback: fb, }; } - if (fb.feedback_type === 'merge_request' && fb.merge_request_iid) { + if (fb.feedback_type === FEEDBACK_TYPE_MERGE_REQUEST && fb.merge_request_iid) { return { ...vuln, hasMergeRequest: true, diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 0944219a6e6..81e6d5f2b97 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -16,6 +16,7 @@ class Projects::PipelinesController < Projects::ApplicationController push_frontend_feature_flag(:pipelines_security_report_summary, project) push_frontend_feature_flag(:new_pipeline_form, project) push_frontend_feature_flag(:graphql_pipeline_header, project, type: :development, default_enabled: false) + push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: false) push_frontend_feature_flag(:new_pipeline_form_prefilled_vars, project, type: :development) end before_action :ensure_pipeline, only: [:show] diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 7c254e069f6..a9361c55958 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -64,15 +64,17 @@ module DiffHelper else # `sub` and substring-ing would destroy HTML-safeness of `line` if line.start_with?('+', '-', ' ') - line.dup.tap do |line| - line[0] = '' - end + line[1, line.length] else line end end end + def diff_link_number(line_type, match, text) + line_type == match ? " " : text + end + def parallel_diff_discussions(left, right, diff_file) return unless @grouped_diff_discussions diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index a0125ff79de..77ced17bc22 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -210,7 +210,7 @@ module IssuablesHelper output << content_tag(:span, (sprite_icon('first-contribution', css_class: 'gl-icon gl-vertical-align-middle') if issuable.first_contribution?), class: 'has-tooltip gl-ml-2', title: _('1st contribution!')) - output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block gl-ml-3") + output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-md-inline-block gl-ml-3") output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none") output.join.html_safe diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb index f6e52def270..78a2be7a9f8 100644 --- a/app/policies/group_member_policy.rb +++ b/app/policies/group_member_policy.rb @@ -11,7 +11,10 @@ class GroupMemberPolicy < BasePolicy condition(:is_target_user) { @user && @subject.user_id == @user.id } rule { anonymous }.prevent_all - rule { last_owner }.prevent_all + rule { last_owner }.policy do + prevent :update_group_member + prevent :destroy_group_member + end rule { can?(:admin_group_member) }.policy do enable :update_group_member diff --git a/app/policies/prometheus_service_policy.rb b/app/policies/prometheus_service_policy.rb deleted file mode 100644 index 5bd2f5e66f8..00000000000 --- a/app/policies/prometheus_service_policy.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -class PrometheusServicePolicy < ::BasePolicy - delegate { @subject.project } -end diff --git a/app/policies/service_policy.rb b/app/policies/service_policy.rb new file mode 100644 index 00000000000..61aff444620 --- /dev/null +++ b/app/policies/service_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ServicePolicy < BasePolicy + delegate(:project) +end diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml index d8facbb780a..a0eff3f1d0b 100644 --- a/app/views/admin/identities/_identity.html.haml +++ b/app/views/admin/identities/_identity.html.haml @@ -1,6 +1,6 @@ %tr %td - #{Gitlab::Auth::OAuth::Provider.label_for(identity.provider)} (#{identity.provider}) + #{Gitlab::Auth::OAuth::Provider.label_for(identity.provider)} (#{identity.provider}) #{identity.saml_provider_id.present? ? "ID: #{identity.saml_provider_id}" : ""} %td = identity.extern_uid %td diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 23f11c072ea..46cce59f67a 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -24,7 +24,7 @@ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.light = branches_sort_options_hash[@sort] - = icon('chevron-down') + = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3") %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable %li.dropdown-header = s_('Branches|Sort by') diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index 5129641058c..24dfb59dc85 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -22,7 +22,7 @@ = hidden_field_tag :ref, default_ref = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide js-branch-select monospace', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do .text-left.dropdown-toggle-text= default_ref - = icon('chevron-down') + = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3") = render 'shared/ref_dropdown', dropdown_class: 'wide' .form-text.text-muted Existing branch name, tag, or commit SHA .form-actions diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index b5a3e957c17..dbe0bf35b98 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -2,13 +2,13 @@ - if current_user && can?(current_user, :fork_project, @project) .count-badge.d-inline-flex.align-item-stretch.gl-mr-3 - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: s_('ProjectOverview|Go to your fork'), class: 'gl-button btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn' do + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: s_('ProjectOverview|Go to your fork'), class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn' do = sprite_icon('fork', css_class: 'icon') %span= s_('ProjectOverview|Fork') - else - can_create_fork = current_user.can?(:create_fork) = link_to new_project_fork_path(@project), - class: "gl-button btn btn-default btn-xs has-tooltip count-badge-button d-flex align-items-center fork-btn #{'has-tooltip disabled' unless can_create_fork}", + class: "btn btn-default btn-xs has-tooltip count-badge-button d-flex align-items-center fork-btn #{'has-tooltip disabled' unless can_create_fork}", title: (s_('ProjectOverview|You have reached your project limit') unless can_create_fork) do = sprite_icon('fork', css_class: 'icon') %span= s_('ProjectOverview|Fork') diff --git a/app/views/projects/commit/diff_files.html.haml b/app/views/projects/commit/diff_files.html.haml index 3a473be3840..0c52c1a15a4 100644 --- a/app/views/projects/commit/diff_files.html.haml +++ b/app/views/projects/commit/diff_files.html.haml @@ -1,3 +1 @@ -- diff_files = diffs.diff_files - -= render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, diff_page_context: 'is-commit' } += render partial: 'projects/diffs/file', collection: diffs.diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, diff_page_context: 'is-commit' } diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index a6a8b947625..18da238d445 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -14,16 +14,15 @@ = submodule_diff_compare_link(diff_file) - unless diff_file.submodule? - - blob = diff_file.blob .file-actions.d-none.d-sm-block - - if blob&.readable_text? + - if diff_file.blob&.readable_text? = link_to '#', class: 'js-toggle-diff-comments gl-button btn active has-tooltip', title: _("Toggle comments for this file"), disabled: @diff_notes_disabled do = sprite_icon('comment') \ - if editable_diff?(diff_file) - link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {} = edit_blob_button(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, - blob: blob, link_opts: link_opts) + blob: diff_file.blob, link_opts: link_opts) - if image_diff && image_replaced = view_file_button(diff_file.old_content_sha, diff_file.old_path, project, replaced: true) diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index f732503a23f..cb43527def1 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -1,17 +1,14 @@ -- show_toggle = local_assigns.fetch(:show_toggle, true) - -- if show_toggle +- if local_assigns.fetch(:show_toggle, true) %i.fa.diff-toggle-caret.fa-fw - if diff_file.submodule? - - blob = diff_file.blob %span = sprite_icon('archive') %strong.file-title-name - = submodule_link(blob, diff_file.content_sha, diff_file.repository) + = submodule_link(diff_file.blob, diff_file.content_sha, diff_file.repository) - = copy_file_path_button(blob.path) + = copy_file_path_button(diff_file.blob.path) - else = conditional_link_to url.present?, url do = blob_icon diff_file.b_mode, diff_file.file_path diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 4d40071e07c..de7f9eba158 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -1,12 +1,11 @@ -- email = local_assigns.fetch(:email, false) - plain = local_assigns.fetch(:plain, false) - discussions = local_assigns.fetch(:discussions, nil) -- type = line.type - line_code = diff_file.line_code(line) - if discussions && line.discussable? - line_discussions = discussions[line_code] -%tr.line_holder{ class: type, id: (line_code unless plain) } - - case type + +%tr.line_holder{ class: line.type, id: (line_code unless plain) } + - case line.type - when 'match' = diff_match_line line.old_pos, line.new_pos, text: line.text - when 'old-nonewline', 'new-nonewline' @@ -14,21 +13,21 @@ %td.new_line.diff-line-num %td.line_content.match= line.text - else - %td.old_line.diff-line-num{ class: [type, ("js-avatar-container" if !plain)], data: { linenumber: line.old_pos } } - - link_text = type == "new" ? " " : line.old_pos + %td.old_line.diff-line-num{ class: [line.type, ("js-avatar-container" if !plain)], data: { linenumber: line.old_pos } } - if plain - = link_text + = diff_link_number(line.type, "new", line.old_pos) - else - = add_diff_note_button(line_code, diff_file.position(line), type) - %a{ href: "##{line_code}", data: { linenumber: link_text } } - %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } - - link_text = type == "old" ? " " : line.new_pos + = add_diff_note_button(line_code, diff_file.position(line), line.type) + %a{ href: "##{line_code}", data: { linenumber: diff_link_number(line.type, "new", line.old_pos) } } + + %td.new_line.diff-line-num{ class: line.type, data: { linenumber: line.new_pos } } - if plain - = link_text + = diff_link_number(line.type, "old", line.new_pos) - else - %a{ href: "##{line_code}", data: { linenumber: link_text } } - %td.line_content{ class: type }< - - if email + %a{ href: "##{line_code}", data: { linenumber: diff_link_number(line.type, "old", line.new_pos) } } + + %td.line_content{ class: line.type }< + - if local_assigns.fetch(:email, false) %pre= line.rich_text - else = diff_line_content(line.rich_text) diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index cee479aab0a..6429cf31bc3 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -10,7 +10,7 @@ %strong.cgreen= pluralize(sum_added_lines, 'addition') and %strong.cred= pluralize(sum_removed_lines, 'deletion') - .diff-stats-additions-deletions-collapsed.float-right.d-none.d-sm-none{ "aria-hidden": "true", "aria-describedby": "diff-stats" } + .diff-stats-additions-deletions-collapsed.float-right.d-none{ "aria-hidden": "true", "aria-describedby": "diff-stats" } %strong.cgreen< +#{sum_added_lines} %strong.cred< diff --git a/app/views/shared/issue_type/_details_header.html.haml b/app/views/shared/issue_type/_details_header.html.haml index 4ad790ab840..e484156f263 100644 --- a/app/views/shared/issue_type/_details_header.html.haml +++ b/app/views/shared/issue_type/_details_header.html.haml @@ -28,10 +28,10 @@ - else .detail-page-header-actions.js-issuable-actions.js-issuable-buttons{ data: { "action": "close-reopen" } } .clearfix.issue-btn-group.dropdown - %button.btn.gl-button.btn-default.float-left.d-md-none.d-lg-none.d-xl-none{ type: "button", data: { toggle: "dropdown" } } + %button.btn.gl-button.btn-default.float-left.gl-display-md-none{ type: "button", data: { toggle: "dropdown" } } = _('Options') = icon('caret-down') - .dropdown-menu.dropdown-menu-right.d-lg-none.d-xl-none + .dropdown-menu.dropdown-menu-right %ul - unless current_user == issuable.author %li= link_to _('Report abuse'), new_abuse_report_path(user_id: issuable.author.id, ref_url: issue_url(issuable)) @@ -49,7 +49,7 @@ = render 'shared/issuable/close_reopen_button', issuable: issuable, can_update: can_update_issue, can_reopen: can_reopen_issue, warn_before_close: defined?(issuable.blocked?) && issuable.blocked? - if can_report_spam - = link_to _('Submit as spam'), mark_as_spam_project_issue_path(@project, issuable), method: :post, class: 'gl-display-none gl-display-sm-none gl-display-md-block gl-button btn btn-grouped btn-spam', title: 'Submit as spam' + = link_to _('Submit as spam'), mark_as_spam_project_issue_path(@project, issuable), method: :post, class: 'gl-display-none gl-display-md-block gl-button btn btn-grouped btn-spam', title: 'Submit as spam' - if can_create_issue - = link_to new_project_issue_path(@project, new_issuable_params), class: 'gl-display-none gl-display-sm-none gl-display-md-block gl-button btn btn-grouped btn-success btn-inverted', title: _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, id: 'new_%{display_issuable_type}_link' % { display_issuable_type: display_issuable_type } do + = link_to new_project_issue_path(@project, new_issuable_params), class: 'gl-display-none gl-display-md-block gl-button btn btn-grouped btn-success btn-inverted', title: _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, id: 'new_%{display_issuable_type}_link' % { display_issuable_type: display_issuable_type } do = _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type } diff --git a/changelogs/unreleased/224509-chevron-down-svg-project-branch.yml b/changelogs/unreleased/224509-chevron-down-svg-project-branch.yml new file mode 100644 index 00000000000..32e9ff0bf53 --- /dev/null +++ b/changelogs/unreleased/224509-chevron-down-svg-project-branch.yml @@ -0,0 +1,5 @@ +--- +title: Update chevron-down icon on project branch page +merge_request: 47460 +author: +type: other diff --git a/changelogs/unreleased/233965-suggest-pipeline-flow-second-step-should-open-on-page-load.yml b/changelogs/unreleased/233965-suggest-pipeline-flow-second-step-should-open-on-page-load.yml new file mode 100644 index 00000000000..e00875bf91e --- /dev/null +++ b/changelogs/unreleased/233965-suggest-pipeline-flow-second-step-should-open-on-page-load.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Suggest Pipeline flow second step not loading +merge_request: 47419 +author: +type: fixed diff --git a/changelogs/unreleased/270065-bs4-optimization-diffs.yml b/changelogs/unreleased/270065-bs4-optimization-diffs.yml new file mode 100644 index 00000000000..8d8e2a011b6 --- /dev/null +++ b/changelogs/unreleased/270065-bs4-optimization-diffs.yml @@ -0,0 +1,5 @@ +--- +title: Remove duplicated BS display properties from various Diffs +merge_request: 47125 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/276911-fix-ruby-lexer-rouge-bug.yml b/changelogs/unreleased/276911-fix-ruby-lexer-rouge-bug.yml new file mode 100644 index 00000000000..075eae9a2b3 --- /dev/null +++ b/changelogs/unreleased/276911-fix-ruby-lexer-rouge-bug.yml @@ -0,0 +1,5 @@ +--- +title: Fix syntax highlight issue with regular expressions +merge_request: 47469 +author: +type: fixed diff --git a/changelogs/unreleased/31528-fix-pipelines-charts-timeout.yml b/changelogs/unreleased/31528-fix-pipelines-charts-timeout.yml new file mode 100644 index 00000000000..11e01b79155 --- /dev/null +++ b/changelogs/unreleased/31528-fix-pipelines-charts-timeout.yml @@ -0,0 +1,5 @@ +--- +title: Fix pipelines chart query timeout +merge_request: 47069 +author: +type: performance diff --git a/changelogs/unreleased/ajk-group-member-policy.yml b/changelogs/unreleased/ajk-group-member-policy.yml new file mode 100644 index 00000000000..5824a8c6db0 --- /dev/null +++ b/changelogs/unreleased/ajk-group-member-policy.yml @@ -0,0 +1,5 @@ +--- +title: Fix overly aggressive prevent call +merge_request: 47455 +author: +type: fixed diff --git a/changelogs/unreleased/arty-admin-idpid.yml b/changelogs/unreleased/arty-admin-idpid.yml new file mode 100644 index 00000000000..13bf180dfce --- /dev/null +++ b/changelogs/unreleased/arty-admin-idpid.yml @@ -0,0 +1,5 @@ +--- +title: Display Group SAML provider ID in admin +merge_request: 47034 +author: +type: added diff --git a/changelogs/unreleased/ss-assignee-dropdown-search.yml b/changelogs/unreleased/ss-assignee-dropdown-search.yml new file mode 100644 index 00000000000..9cc9fc78c1f --- /dev/null +++ b/changelogs/unreleased/ss-assignee-dropdown-search.yml @@ -0,0 +1,5 @@ +--- +title: Add search assignees to group issue boards +merge_request: 47241 +author: +type: added diff --git a/config/feature_flags/development/ci_variable_expansion_in_rules_changes.yml b/config/feature_flags/development/ci_variable_expansion_in_rules_changes.yml new file mode 100644 index 00000000000..3d21fb74447 --- /dev/null +++ b/config/feature_flags/development/ci_variable_expansion_in_rules_changes.yml @@ -0,0 +1,7 @@ +--- +name: ci_variable_expansion_in_rules_changes +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45037 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267192 +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/config/feature_flags/development/graphql_pipeline_details.yml b/config/feature_flags/development/graphql_pipeline_details.yml new file mode 100644 index 00000000000..c6d03850f0e --- /dev/null +++ b/config/feature_flags/development/graphql_pipeline_details.yml @@ -0,0 +1,7 @@ +--- +name: graphql_pipeline_details +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46380 +rollout_issue_url: +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/db/migrate/20201106134139_add_pipelines_created_index.rb b/db/migrate/20201106134139_add_pipelines_created_index.rb new file mode 100644 index 00000000000..aaf6643cf0b --- /dev/null +++ b/db/migrate/20201106134139_add_pipelines_created_index.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddPipelinesCreatedIndex < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + INDEX_NAME = :index_ci_pipelines_on_project_id_and_status_and_created_at + + disable_ddl_transaction! + + def up + add_concurrent_index :ci_pipelines, [:project_id, :status, :created_at], name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :ci_pipelines, INDEX_NAME + end +end diff --git a/db/schema_migrations/20201106134139 b/db/schema_migrations/20201106134139 new file mode 100644 index 00000000000..245cbbf712e --- /dev/null +++ b/db/schema_migrations/20201106134139 @@ -0,0 +1 @@ +9b1008df64741ad313ddf51969c18d609cd01a7255563fe0395d2bbf4d288e30
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index a7334cd10f2..ff62fdf84c1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -20401,6 +20401,8 @@ CREATE INDEX index_ci_pipelines_on_project_id_and_source ON ci_pipelines USING b CREATE INDEX index_ci_pipelines_on_project_id_and_status_and_config_source ON ci_pipelines USING btree (project_id, status, config_source); +CREATE INDEX index_ci_pipelines_on_project_id_and_status_and_created_at ON ci_pipelines USING btree (project_id, status, created_at); + CREATE INDEX index_ci_pipelines_on_project_id_and_status_and_updated_at ON ci_pipelines USING btree (project_id, status, updated_at); CREATE INDEX index_ci_pipelines_on_project_id_and_user_id_and_status_and_ref ON ci_pipelines USING btree (project_id, user_id, status, ref) WHERE (source <> 12); diff --git a/doc/administration/logs.md b/doc/administration/logs.md index 307d2ac3baa..ee7c7247ad3 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -966,6 +966,11 @@ For Omnibus GitLab installations, GitLab Monitor logs reside in `/var/log/gitlab For Omnibus GitLab installations, GitLab Exporter logs reside in `/var/log/gitlab/gitlab-exporter/`. +## GitLab Kubernetes Agent Server + +For Omnibus GitLab installations, GitLab Kubernetes Agent Server logs reside +in `/var/log/gitlab/gitlab-kas/`. + ## Gathering logs When [troubleshooting](troubleshooting/index.md) issues that aren't localized to one of the diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index e13412fabb8..8162505270c 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -1803,6 +1803,16 @@ type BoardEpic implements CurrentUserTodos & Noteable { upvotes: Int! """ + Number of user discussions in the epic + """ + userDiscussionsCount: Int! + + """ + Number of user notes of the epic + """ + userNotesCount: Int! + + """ Permissions for the current user on the resource """ userPermissions: EpicPermissions! @@ -7251,6 +7261,16 @@ type Epic implements CurrentUserTodos & Noteable { upvotes: Int! """ + Number of user discussions in the epic + """ + userDiscussionsCount: Int! + + """ + Number of user notes of the epic + """ + userNotesCount: Int! + + """ Permissions for the current user on the resource """ userPermissions: EpicPermissions! diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 841224447d6..523d29202f1 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -4754,6 +4754,42 @@ "deprecationReason": null }, { + "name": "userDiscussionsCount", + "description": "Number of user discussions in the epic", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userNotesCount", + "description": "Number of user notes of the epic", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "userPermissions", "description": "Permissions for the current user on the resource", "args": [ @@ -20056,6 +20092,42 @@ "deprecationReason": null }, { + "name": "userDiscussionsCount", + "description": "Number of user discussions in the epic", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userNotesCount", + "description": "Number of user notes of the epic", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "userPermissions", "description": "Permissions for the current user on the resource", "args": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 3224e16ea49..b50da3a15b0 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -290,6 +290,8 @@ Represents an epic on an issue board. | `title` | String | Title of the epic | | `updatedAt` | Time | Timestamp of when the epic was updated | | `upvotes` | Int! | Number of upvotes the epic has received | +| `userDiscussionsCount` | Int! | Number of user discussions in the epic | +| `userNotesCount` | Int! | Number of user notes of the epic | | `userPermissions` | EpicPermissions! | Permissions for the current user on the resource | | `userPreferences` | BoardEpicUserPreferences | User preferences for the epic on the issue board | | `webPath` | String! | Web path of the epic | @@ -1189,6 +1191,8 @@ Represents an epic. | `title` | String | Title of the epic | | `updatedAt` | Time | Timestamp of when the epic was updated | | `upvotes` | Int! | Number of upvotes the epic has received | +| `userDiscussionsCount` | Int! | Number of user discussions in the epic | +| `userNotesCount` | Int! | Number of user notes of the epic | | `userPermissions` | EpicPermissions! | Permissions for the current user on the resource | | `webPath` | String! | Web path of the epic | | `webUrl` | String! | Web URL of the epic | diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 3ccc10907b3..50ae85572b1 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -244,7 +244,7 @@ a preconfigured `workflow: rules` entry. `workflow: rules` accepts these keywords: - [`if`](#rulesif): Check this rule to determine when to run a pipeline. -- [`when`](#when): Specify what to do when the `if` rule evaluates to true. +- [`when`](#when): Specify what to do when the `if` rule evaluates to true. - To run a pipeline, set to `always`. - To prevent pipelines from running, set to `never`. @@ -1347,6 +1347,53 @@ Tag pipelines, scheduled pipelines, and so on do **not** have a Git `push` event associated with them. A `rules: changes` job is **always** added to those pipeline if there is no `if:` statement that limits the job to branch or merge request pipelines. +##### Variables in `rules:changes` + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34272) in GitLab 13.6. +> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default. +> - It's disabled on GitLab.com. +> - It's not recommended for production use. +> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-variables-support-in-ruleschanges). **(CORE ONLY)** + +CAUTION: **Warning:** +This feature might not be available to you. Check the **version history** note above for details. + +Environment variables can be used in `rules:changes` expressions to determine when +to add jobs to a pipeline: + +```yaml +docker build: + variables: + DOCKERFILES_DIR: 'path/to/files/' + script: docker build -t my-image:$CI_COMMIT_REF_SLUG . + rules: + - changes: + - $DOCKERFILES_DIR/* +``` + +The `$` character can be used for both variables and paths. For example, if the +`$DOCKERFILES_DIR` variable exists, its value is used. If it does not exist, the +`$` is interpreted as being part of a path. + +###### Enable or disable variables support in `rules:changes` **(CORE ONLY)** + +Variables support in `rules:changes` is under development and not ready for production use. It is +deployed behind a feature flag that is **disabled by default**. +[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) +can enable it. + +To enable it: + +```ruby +Feature.enable(:ci_variable_expansion_in_rules_changes) +``` + +To disable it: + +```ruby +Feature.disable(:ci_variable_expansion_in_rules_changes) +``` + #### `rules:exists` > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24021) in GitLab 12.4. diff --git a/doc/development/product_analytics/usage_ping.md b/doc/development/product_analytics/usage_ping.md index d384ce09338..d371224471f 100644 --- a/doc/development/product_analytics/usage_ping.md +++ b/doc/development/product_analytics/usage_ping.md @@ -429,13 +429,22 @@ w - `values`: One value or array of values we count. For example: user_id, visitor_id, user_ids. - `event_name`: event name. -1. Get event data using `Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names:, start_date:, end_date)`. +1. Track event on context level using base module `Gitlab::UsageDataCounters::HLLRedisCounter.track_event_in_context(entity_id, event_name, context)`. + + Arguments: + + - `entity_id`: value we count. For example: user_id, visitor_id. + - `event_name`: event name. + - `context`: context value. Allowed values are `default`, `free`, `bronze`, `silver`, `gold`, `starter`, `premium`, `ultimate` + +1. Get event data using `Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names:, start_date:, end_date:, context: '')`. Arguments: - `event_names`: the list of event names. - `start_date`: start date of the period for which we want to get event data. - `end_date`: end date of the period for which we want to get event data. + - `context`: context of the event. Allowed values are `default`, `free`, `bronze`, `silver`, `gold`, `starter`, `premium`, `ultimate`. Recommendations: diff --git a/doc/development/uploads.md b/doc/development/uploads.md index 67979ebaba3..0307ab76cd1 100644 --- a/doc/development/uploads.md +++ b/doc/development/uploads.md @@ -278,8 +278,10 @@ Uploads routes belong to one of these categories: 1. Rails controllers: uploads handled by Rails controllers. 1. Grape API: uploads handled by a Grape API endpoint. -1. GraphQL API: uploads handled by a GraphQL resolve function. In these cases, there is nothing else - to do apart from implementing the actual upload. +1. GraphQL API: uploads handled by a GraphQL resolve function. + +CAUTION: **Warning:** +GraphQL uploads do not support [direct upload](#direct-upload) yet. Depending on the use case, the feature may not work on installations without NFS (like GitLab.com or Kubernetes installations). Uploading to object storage inside the GraphQL resolve function may result in timeout errors. For more details please follow [issue #280819](https://gitlab.com/gitlab-org/gitlab/-/issues/280819). ### Update Workhorse for the new route diff --git a/doc/integration/kerberos.md b/doc/integration/kerberos.md index 316db57c7cc..50468443769 100644 --- a/doc/integration/kerberos.md +++ b/doc/integration/kerberos.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Source Code +stage: Manage +group: Access info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers" type: reference, how-to --- @@ -47,7 +47,7 @@ sudo chmod 0600 /etc/http.keytab ### Configure GitLab -**Installations from source** +#### Installations from source NOTE: **Note:** For source installations, make sure the `kerberos` gem group @@ -74,7 +74,7 @@ For source installations, make sure the `kerberos` gem group 1. [Restart GitLab](../administration/restart_gitlab.md#installations-from-source) for the changes to take effect. -**Omnibus package installations** +#### Omnibus package installations 1. Edit `/etc/gitlab/gitlab.rb`: @@ -91,18 +91,71 @@ GitLab will now offer the `negotiate` authentication method for signing in and HTTP Git access, enabling Git clients that support this authentication protocol to authenticate with Kerberos tokens. -## Creating and linking Kerberos accounts +#### Enable single sign-on -The Administrative user can navigate to **Admin > Users > Example User > Identities** -and attach a Kerberos account. Existing GitLab users can go to **Profile > Account** -and attach a Kerberos account. If you want to allow users without a GitLab -account to sign in, enable the `allow_single_sign_on` option, as described in the -[Configure GitLab](#configure-gitlab) section. The first time a user signs in -with Kerberos credentials, GitLab will create a new GitLab user associated with -the email, which is built from the Kerberos username and realm. User accounts are -created after successful authentications. +See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) +for initial settings to enable single sign-on and add Kerberos servers +as an identity provider. -## Linking Kerberos and LDAP accounts together +## Create and link Kerberos accounts + +You can either link a Kerberos account to an existing GitLab account, or +set up GitLab to create a new account when a Kerberos user tries to sign in. + +### Link a Kerberos account to an existing GitLab account + +If you're an administrator, you can link a Kerberos account to an +existing GitLab account. To do so: + +1. Navigate to **Admin Area > Overview > Users > Example User**. +1. Select the Identities tab. +1. Select 'Kerberos Spnego' in the 'Provider' dropdown box. +1. Make sure the **Identifier** corresponds to the Kerberos username. +1. Select **Save changes**. + +If you're not an administrator: + +1. Select your avatar in the upper-right corner, and select **Settings**. +1. Select Account. In the **Social sign-in** section, select + **Connect Kerberos Spnego**. + If you don't see a **Social sign-in** Kerberos option, follow the + requirements in [Enable single sign-on](#enable-single-sign-on). + +In either case, you should now be able to sign in to your GitLab account +with your Kerberos credentials. + +### Create accounts on first sign-in + +The first time users sign in to GitLab with their Kerberos accounts, +GitLab creates a matching account. +Before you continue, review the [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) options in Omnibus and GitLab source. You must also include `kerberos`. + +With that information at hand: + +1. Include `'kerberos'` with the `allow_single_sign_on` setting. +1. For now, accept the default `block_auto_created_users` option, true. +1. When a user tries to sign in with Kerberos credentials, GitLab + creates a new account. + 1. If `block_auto_created_users` is true, the Kerberos user may see + a message like: + + ```shell + Your account has been blocked. Please contact your GitLab + administrator if you think this is an error. + ``` + + 1. As an administrator, you can confirm the new, blocked account. + Select **Admin Area > Overview > Users** and review the Blocked tab. + 1. You can enable the user. + 1. If `block_auto_created_users` is false, the Kerberos user is + authenticated and is signed in to GitLab. + +CAUTION: **Warning** +We recommend that you retain the default for `block_auto_created_users`. +Kerberos users who create accounts on GitLab without administrator +knowledge can be a security risk. + +## Link Kerberos and LDAP accounts together If your users sign in with Kerberos, but you also have [LDAP integration](../administration/auth/ldap/index.md) enabled, your users will be linked to their LDAP accounts on their first sign-in. diff --git a/doc/operations/incident_management/alert_integrations.md b/doc/operations/incident_management/alert_integrations.md index 6d76f519e62..7850841d380 100644 --- a/doc/operations/incident_management/alert_integrations.md +++ b/doc/operations/incident_management/alert_integrations.md @@ -9,7 +9,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13203) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.4. > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/42640) to [GitLab Core](https://about.gitlab.com/pricing/) in 12.8. -GitLab can accept alerts from any source via a webhook receiver. This can be configured generically or, in GitLab versions 13.1 and greater, you can configure +GitLab can accept alerts from any source via a webhook receiver. This can be configured +generically or, in GitLab versions 13.1 and greater, you can configure [External Prometheus instances](../metrics/alerts.md#external-prometheus-instances) to use this endpoint. @@ -26,16 +27,14 @@ The list displays the integration name, type, and status (enabled or disabled): ## Configuration -You can either configure alerts to integrate with an [external Prometheus server](#external-prometheus-integration), -or provide a [generic HTTP endpoint](#generic-http-endpoint) to receive alerts -from other services. +GitLab can receive alerts via a [HTTP endpoint](#generic-http-endpoint) that you configure, +or the [Prometheus integration](#external-prometheus-integration). -### Generic HTTP Endpoint +### Generic HTTP Endpoint **CORE** -Enabling the Generic HTTP Endpoint creates a unique HTTP endpoint that can receive alert payloads in JSON format. You can always -[customize the payload](#customizing-the-payload) to your liking. - -You will need to activate the endpoint and obtain credentials to set up this integration: +Enabling the Generic HTTP Endpoint activates a unique HTTP endpoint that can +receive alert payloads in JSON format. You can always +[customize the payload](#customize-the-alert-payload-outside-of-gitlab) to your liking. 1. Sign in to GitLab as a user with maintainer [permissions](../../user/permissions.md) for a project. @@ -44,20 +43,49 @@ You will need to activate the endpoint and obtain credentials to set up this int 1. Toggle the **Active** alert setting to display the **URL** and **Authorization Key** for the webhook configuration. +### HTTP Endpoints **PREMIUM** + +> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4442) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.6. + +In [GitLab Premium](https://about.gitlab.com/pricing/), you can create multiple +unique HTTP endpoints to receive alerts from any external source in JSON format, +and you can [customize the payload](#customize-the-alert-payload-outside-of-gitlab). + +1. Sign in to GitLab as a user with maintainer [permissions](../../user/permissions.md) + for a project. +1. Navigate to **Settings > Operations** in your project. +1. Expand the **Alerts** section. +1. For each endpoint you want to create: + + 1. In the **Integration** dropdown menu, select **HTTP Endpoint**. + 1. Name the integration. + 1. Toggle the **Active** alert setting to display the **URL** and **Authorization Key** + for the webhook configuration. You will input the URL and Authorization Key + in your external service. + 1. _(Optional)_ To generate a test alert to test the new integration, enter a + sample payload, then click **Save and test alert payload**.Valid JSON is required. + 1. Click **Save Integration**. + +The new HTTP Endpoint displays in the [integrations list](#integrations-list). +You can edit the integration by selecting the **{pencil}** pencil icon on the right +side of the integrations list. + ### External Prometheus integration -For GitLab versions 13.1 and greater, please see [External Prometheus Instances](../metrics/alerts.md#external-prometheus-instances) to configure alerts for this integration. +For GitLab versions 13.1 and greater, please read +[External Prometheus Instances](../metrics/alerts.md#external-prometheus-instances) +to configure alerts for this integration. -## Customizing the payload +## Customize the alert payload outside of GitLab -You can customize the payload by sending the following parameters. This applies to all types of integrations. All fields -other than `title` are optional: +For all integration types, you can customize the payload by sending the following +parameters. All fields other than `title` are optional: | Property | Type | Description | | ------------------------- | --------------- | ----------- | | `title` | String | The title of the incident. Required. | | `description` | String | A high-level summary of the problem. | -| `start_time` | DateTime | The time of the incident. If none is provided, a timestamp of the issue will be used. | +| `start_time` | DateTime | The time of the incident. If none is provided, a timestamp of the issue is used. | | `end_time` | DateTime | For existing alerts only. When provided, the alert is resolved and the associated incident is closed. | | `service` | String | The affected service. | | `monitoring_tool` | String | The name of the associated monitoring tool. | @@ -74,8 +102,9 @@ can be a nested JSON object. For example: { "foo": { "bar": { "baz": 42 } } } ``` -TIP: **Payload size:** -Ensure your requests are smaller than the [payload application limits](../../administration/instance_limits.md#generic-alert-json-payloads). +TIP: **Tip:** +Ensure your requests are smaller than the +[payload application limits](../../administration/instance_limits.md#generic-alert-json-payloads). Example request: diff --git a/doc/user/clusters/agent/index.md b/doc/user/clusters/agent/index.md index cffa8016acc..8fa85b91d73 100644 --- a/doc/user/clusters/agent/index.md +++ b/doc/user/clusters/agent/index.md @@ -63,7 +63,7 @@ For more details, please refer to our [full architecture documentation](https:// The setup process involves a few steps to enable GitOps deployments: 1. [Install the Agent server](#install-the-kubernetes-agent-server). -1. [Define a configuration directory](#define-a-configuration-repository). +1. [Define a configuration repository](#define-a-configuration-repository). 1. [Create an Agent record in GitLab](#create-an-agent-record-in-gitlab). 1. [Generate and copy a Secret token used to connect to the Agent](#create-the-kubernetes-secret). 1. [Install the Agent into the cluster](#install-the-agent-into-the-cluster). @@ -108,6 +108,10 @@ When using the [Omnibus GitLab](https://docs.gitlab.com/omnibus/) package: 1. [Reconfigure GitLab](../../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure). +To configure any additional options related to GitLab Kubernetes Agent Server, +refer to the **Enable GitLab KAS** section of the +[`gitlab.rb.template`](https://gitlab.com/gitlab-org/omnibus-gitlab/-/blob/master/files/gitlab-config-template/gitlab.rb.template). + #### Install with the Helm chart When installing or upgrading the GitLab Helm chart, consider the following Helm v3 example. diff --git a/lib/expand_variables.rb b/lib/expand_variables.rb index 3a50925d628..dc8f9d0c970 100644 --- a/lib/expand_variables.rb +++ b/lib/expand_variables.rb @@ -1,17 +1,39 @@ # frozen_string_literal: true module ExpandVariables + VARIABLES_REGEXP = /\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/.freeze + class << self def expand(value, variables) + replace_with(value, variables) do |vars_hash, last_match| + match_or_blank_value(vars_hash, last_match) + end + end + + def expand_existing(value, variables) + replace_with(value, variables) do |vars_hash, last_match| + match_or_original_value(vars_hash, last_match) + end + end + + private + + def replace_with(value, variables) variables_hash = nil - value.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do + value.gsub(VARIABLES_REGEXP) do variables_hash ||= transform_variables(variables) - variables_hash[Regexp.last_match(1) || Regexp.last_match(2)] + yield(variables_hash, Regexp.last_match) end end - private + def match_or_blank_value(variables, last_match) + variables[last_match[1] || last_match[2]] + end + + def match_or_original_value(variables, last_match) + match_or_blank_value(variables, last_match) || last_match[0] + end def transform_variables(variables) # Lazily initialise variables diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb index 728a66ca87f..ee267a33aa7 100644 --- a/lib/gitlab/ci/build/rules/rule/clause/changes.rb +++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb @@ -11,12 +11,22 @@ module Gitlab def satisfied_by?(pipeline, context) return true if pipeline.modified_paths.nil? + expanded_globs = expand_globs(pipeline, context) pipeline.modified_paths.any? do |path| - @globs.any? do |glob| + expanded_globs.any? do |glob| File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB) end end end + + def expand_globs(pipeline, context) + return @globs unless ::Feature.enabled?(:ci_variable_expansion_in_rules_changes, pipeline.project) + return @globs unless context + + @globs.map do |glob| + ExpandVariables.expand_existing(glob, context.variables) + end + end end end end diff --git a/lib/gitlab/ci/charts.rb b/lib/gitlab/ci/charts.rb index 3fbfdffe277..25fb9c0ca97 100644 --- a/lib/gitlab/ci/charts.rb +++ b/lib/gitlab/ci/charts.rb @@ -3,38 +3,8 @@ module Gitlab module Ci module Charts - module DailyInterval - # rubocop: disable CodeReuse/ActiveRecord - def grouped_count(query) - query - .group("DATE(#{::Ci::Pipeline.table_name}.created_at)") - .count(:created_at) - .transform_keys { |date| date.strftime(@format) } # rubocop:disable Gitlab/ModuleWithInstanceVariables - end - # rubocop: enable CodeReuse/ActiveRecord - - def interval_step - @interval_step ||= 1.day - end - end - - module MonthlyInterval - # rubocop: disable CodeReuse/ActiveRecord - def grouped_count(query) - query - .group("to_char(#{::Ci::Pipeline.table_name}.created_at, '01 Month YYYY')") - .count(:created_at) - .transform_keys(&:squish) - end - # rubocop: enable CodeReuse/ActiveRecord - - def interval_step - @interval_step ||= 1.month - end - end - class Chart - attr_reader :labels, :total, :success, :project, :pipeline_times + attr_reader :from, :to, :labels, :total, :success, :project, :pipeline_times def initialize(project) @labels = [] @@ -46,48 +16,59 @@ module Gitlab collect end + private + + attr_reader :interval + # rubocop: disable CodeReuse/ActiveRecord def collect query = project.all_pipelines - .where("? > #{::Ci::Pipeline.table_name}.created_at AND #{::Ci::Pipeline.table_name}.created_at > ?", @to, @from) # rubocop:disable GitlabSecurity/SqlInjection + .where(::Ci::Pipeline.arel_table['created_at'].gteq(@from)) + .where(::Ci::Pipeline.arel_table['created_at'].lteq(@to)) totals_count = grouped_count(query) success_count = grouped_count(query.success) current = @from - while current < @to - label = current.strftime(@format) - - @labels << label - @total << (totals_count[label] || 0) - @success << (success_count[label] || 0) + while current <= @to + @labels << current.strftime(@format) + @total << (totals_count[current] || 0) + @success << (success_count[current] || 0) current += interval_step end end # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord + def grouped_count(query) + query + .group("date_trunc('#{interval}', #{::Ci::Pipeline.table_name}.created_at)") + .count(:created_at) + end + # rubocop: enable CodeReuse/ActiveRecord + + def interval_step + @interval_step ||= 1.public_send(interval) # rubocop: disable GitlabSecurity/PublicSend + end end class YearChart < Chart - include MonthlyInterval - attr_reader :to, :from - def initialize(*) @to = Date.today.end_of_month.end_of_day - @from = @to.years_ago(1).beginning_of_month.beginning_of_day - @format = '%d %B %Y' + @from = (@to - 1.year).beginning_of_month.beginning_of_day + @interval = :month + @format = '%B %Y' super end end class MonthChart < Chart - include DailyInterval - attr_reader :to, :from - def initialize(*) @to = Date.today.end_of_day - @from = 1.month.ago.beginning_of_day + @from = (@to - 1.month).beginning_of_day + @interval = :day @format = '%d %B' super @@ -95,12 +76,10 @@ module Gitlab end class WeekChart < Chart - include DailyInterval - attr_reader :to, :from - def initialize(*) @to = Date.today.end_of_day - @from = 1.week.ago.beginning_of_day + @from = (@to - 1.week).beginning_of_day + @interval = :day @format = '%d %B' super diff --git a/lib/gitlab/usage_data_counters/hll_redis_counter.rb b/lib/gitlab/usage_data_counters/hll_redis_counter.rb index fffba7746a5..54aa00a2dbd 100644 --- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb +++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb @@ -14,6 +14,7 @@ module Gitlab SlotMismatch = Class.new(EventError) CategoryMismatch = Class.new(EventError) UnknownAggregationOperator = Class.new(EventError) + InvalidContext = Class.new(EventError) KNOWN_EVENTS_PATH = File.expand_path('known_events/*.yml', __dir__) ALLOWED_AGGREGATIONS = %i(daily weekly).freeze @@ -42,21 +43,23 @@ module Gitlab class << self include Gitlab::Utils::UsageData - def track_event(entity_id, event_name, time = Time.zone.now) - return unless Gitlab::CurrentSettings.usage_ping_enabled? - - event = event_for(event_name) + def track_event(value, event_name, time = Time.zone.now) + track(value, event_name, time: time) + end - raise UnknownEvent, "Unknown event #{event_name}" unless event.present? + def track_event_in_context(value, event_name, context, time = Time.zone.now) + return if context.blank? + return unless context.in?(valid_context_list) - Gitlab::Redis::HLL.add(key: redis_key(event, time), value: entity_id, expiry: expiry(event)) + track(value, event_name, context: context, time: time) end - def unique_events(event_names:, start_date:, end_date:) - count_unique_events(event_names: event_names, start_date: start_date, end_date: end_date) do |events| + def unique_events(event_names:, start_date:, end_date:, context: '') + count_unique_events(event_names: event_names, start_date: start_date, end_date: end_date, context: context) do |events| raise SlotMismatch, events unless events_in_same_slot?(events) raise CategoryMismatch, events unless events_in_same_category?(events) raise AggregationMismatch, events unless events_same_aggregation?(events) + raise InvalidContext if context.present? && !context.in?(valid_context_list) end end @@ -114,6 +117,20 @@ module Gitlab private + def track(value, event_name, context: '', time: Time.zone.now) + return unless Gitlab::CurrentSettings.usage_ping_enabled? + + event = event_for(event_name) + raise UnknownEvent, "Unknown event #{event_name}" unless event.present? + + Gitlab::Redis::HLL.add(key: redis_key(event, time, context), value: value, expiry: expiry(event)) + end + + # The aray of valid context on which we allow tracking + def valid_context_list + Plan.all_plans + end + def calculate_count_for_aggregation(aggregation, start_date:, end_date:) case aggregation[:operator] when UNION_OF_AGGREGATED_METRICS @@ -193,14 +210,14 @@ module Gitlab end end - def count_unique_events(event_names:, start_date:, end_date:) + def count_unique_events(event_names:, start_date:, end_date:, context: '') events = events_for(Array(event_names).map(&:to_s)) yield events if block_given? aggregation = events.first[:aggregation] - keys = keys_for_aggregation(aggregation, events: events, start_date: start_date, end_date: end_date) + keys = keys_for_aggregation(aggregation, events: events, start_date: start_date, end_date: end_date, context: context) redis_usage_data { Gitlab::Redis::HLL.count(keys: keys) } end @@ -213,11 +230,11 @@ module Gitlab events_in_same_slot?(events) && events_in_same_category?(events) && events_same_aggregation?(events) end - def keys_for_aggregation(aggregation, events:, start_date:, end_date:) + def keys_for_aggregation(aggregation, events:, start_date:, end_date:, context: '') if aggregation.to_sym == :daily - daily_redis_keys(events: events, start_date: start_date, end_date: end_date) + daily_redis_keys(events: events, start_date: start_date, end_date: end_date, context: context) else - weekly_redis_keys(events: events, start_date: start_date, end_date: end_date) + weekly_redis_keys(events: events, start_date: start_date, end_date: end_date, context: context) end end @@ -272,17 +289,26 @@ module Gitlab end # Compose the key in order to store events daily or weekly - def redis_key(event, time) + def redis_key(event, time, context = '') raise UnknownEvent.new("Unknown event #{event[:name]}") unless known_events_names.include?(event[:name].to_s) raise UnknownAggregation.new("Use :daily or :weekly aggregation") unless ALLOWED_AGGREGATIONS.include?(event[:aggregation].to_sym) + key = apply_slot(event) + key = apply_time_aggregation(key, time, event) + key = "#{context}_#{key}" if context.present? + key + end + + def apply_slot(event) slot = redis_slot(event) - key = if slot.present? - event[:name].to_s.gsub(slot, "{#{slot}}") - else - "{#{event[:name]}}" - end + if slot.present? + event[:name].to_s.gsub(slot, "{#{slot}}") + else + "{#{event[:name]}}" + end + end + def apply_time_aggregation(key, time, event) if event[:aggregation].to_sym == :daily year_day = time.strftime('%G-%j') "#{year_day}-#{key}" @@ -292,21 +318,29 @@ module Gitlab end end - def daily_redis_keys(events:, start_date:, end_date:) + def daily_redis_keys(events:, start_date:, end_date:, context: '') (start_date.to_date..end_date.to_date).map do |date| - events.map { |event| redis_key(event, date) } + events.map { |event| redis_key(event, date, context) } end.flatten end - def weekly_redis_keys(events:, start_date:, end_date:) + def validate_aggregation_operator!(operator) + return true if ALLOWED_METRICS_AGGREGATIONS.include?(operator) + + raise UnknownAggregationOperator.new("Events should be aggregated with one of operators #{ALLOWED_METRICS_AGGREGATIONS}") + end + + def weekly_redis_keys(events:, start_date:, end_date:, context: '') weeks = end_date.to_date.cweek - start_date.to_date.cweek weeks = 1 if weeks == 0 (0..(weeks - 1)).map do |week_increment| - events.map { |event| redis_key(event, start_date + week_increment * 7.days) } + events.map { |event| redis_key(event, start_date + week_increment * 7.days, context) } end.flatten end end end end end + +Gitlab::UsageDataCounters::HLLRedisCounter.prepend_if_ee('EE::Gitlab::UsageDataCounters::HLLRedisCounter') diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 82fdd0fe1cc..dd39815ab04 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3087,6 +3087,9 @@ msgstr "" msgid "An error occurred while initializing path locks" msgstr "" +msgid "An error occurred while loading a section of this page." +msgstr "" + msgid "An error occurred while loading all the files." msgstr "" @@ -3141,6 +3144,9 @@ msgstr "" msgid "An error occurred while loading the merge request." msgstr "" +msgid "An error occurred while loading the pipeline." +msgstr "" + msgid "An error occurred while loading the pipelines jobs." msgstr "" @@ -3198,6 +3204,9 @@ msgstr "" msgid "An error occurred while triggering the job." msgstr "" +msgid "An error occurred while trying to generate the report. Please try again later." +msgstr "" + msgid "An error occurred while trying to run a new pipeline for this Merge Request." msgstr "" diff --git a/spec/features/merge_request/user_sees_suggest_pipeline_spec.rb b/spec/features/merge_request/user_sees_suggest_pipeline_spec.rb index bed3e1e681a..4bb6c3265a4 100644 --- a/spec/features/merge_request/user_sees_suggest_pipeline_spec.rb +++ b/spec/features/merge_request/user_sees_suggest_pipeline_spec.rb @@ -30,4 +30,40 @@ RSpec.describe 'Merge request > User sees suggest pipeline', :js do expect(page).not_to have_content('Are you adding technical debt or code vulnerabilities?') end + + it 'runs tour from start to finish ensuring all nudges are executed' do + # nudge 1 + expect(page).to have_content('Are you adding technical debt or code vulnerabilities?') + + page.within '.mr-pipeline-suggest' do + find('[data-testid="ok"]').click + end + + wait_for_requests + + # nudge 2 + expect(page).to have_content('Choose Code Quality to add a pipeline that tests the quality of your code.') + + find('.js-gitlab-ci-yml-selector').click + + wait_for_requests + + within '.gitlab-ci-yml-selector' do + find('.dropdown-input-field').set('Jekyll') + find('.dropdown-content li', text: 'Jekyll').click + end + + wait_for_requests + + expect(page).not_to have_content('Choose Code Quality to add a pipeline that tests the quality of your code.') + # nudge 3 + expect(page).to have_content('The template is ready!') + + find('#commit-changes').click + + wait_for_requests + + # nudge 4 + expect(page).to have_content("That's it, well done!") + end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 51826d867cd..99e4084ef12 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -14,6 +14,7 @@ RSpec.describe 'Pipeline', :js do before do sign_in(user) project.add_role(user, role) + stub_feature_flags(graphql_pipeline_details: false) end shared_context 'pipeline builds' do diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 3e78dfc3bc7..450524b8d70 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -12,6 +12,7 @@ RSpec.describe 'Pipelines', :js do before do sign_in(user) + stub_feature_flags(graphql_pipeline_details: false) project.add_developer(user) project.update!(auto_devops_attributes: { enabled: false }) end diff --git a/spec/frontend/boards/components/board_assignee_dropdown_spec.js b/spec/frontend/boards/components/board_assignee_dropdown_spec.js index 846f8acfa5d..e185a6d5419 100644 --- a/spec/frontend/boards/components/board_assignee_dropdown_spec.js +++ b/spec/frontend/boards/components/board_assignee_dropdown_spec.js @@ -1,23 +1,59 @@ -import { mount } from '@vue/test-utils'; -import { GlDropdownItem, GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui'; +import { mount, createLocalVue } from '@vue/test-utils'; +import { GlDropdownItem, GlAvatarLink, GlAvatarLabeled, GlSearchBoxByType } from '@gitlab/ui'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; +import VueApollo from 'vue-apollo'; import BoardAssigneeDropdown from '~/boards/components/board_assignee_dropdown.vue'; import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue'; import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue'; import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; import store from '~/boards/stores'; import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql'; +import searchUsers from '~/boards/queries/users_search.query.graphql'; import { participants } from '../mock_data'; +const localVue = createLocalVue(); + +localVue.use(VueApollo); + describe('BoardCardAssigneeDropdown', () => { let wrapper; + let fakeApollo; + let getIssueParticipantsSpy; + let getSearchUsersSpy; + const iid = '111'; const activeIssueName = 'test'; const anotherIssueName = 'hello'; - const createComponent = () => { + const createComponent = (search = '') => { + wrapper = mount(BoardAssigneeDropdown, { + data() { + return { + search, + selected: store.getters.activeIssue.assignees, + participants, + }; + }, + store, + provide: { + canUpdate: true, + rootPath: '', + }, + }); + }; + + const createComponentWithApollo = (search = '') => { + fakeApollo = createMockApollo([ + [getIssueParticipants, getIssueParticipantsSpy], + [searchUsers, getSearchUsersSpy], + ]); + wrapper = mount(BoardAssigneeDropdown, { + localVue, + apolloProvider: fakeApollo, data() { return { + search, selected: store.getters.activeIssue.assignees, participants, }; @@ -43,7 +79,7 @@ describe('BoardCardAssigneeDropdown', () => { }; const findByText = text => { - return wrapper.findAll(GlDropdownItem).wrappers.find(x => x.text().indexOf(text) === 0); + return wrapper.findAll(GlDropdownItem).wrappers.find(node => node.text().indexOf(text) === 0); }; beforeEach(() => { @@ -59,6 +95,10 @@ describe('BoardCardAssigneeDropdown', () => { }); afterEach(() => { + jest.restoreAllMocks(); + }); + + afterEach(() => { wrapper.destroy(); wrapper = null; }); @@ -203,27 +243,66 @@ describe('BoardCardAssigneeDropdown', () => { }, ); - describe('Apollo Schema', () => { + describe('Apollo', () => { beforeEach(() => { - createComponent(); + getIssueParticipantsSpy = jest.fn().mockResolvedValue({ + data: { + issue: { + participants: { + nodes: [ + { + username: 'participant', + name: 'participant', + webUrl: '', + avatarUrl: '', + id: '', + }, + ], + }, + }, + }, + }); + getSearchUsersSpy = jest.fn().mockResolvedValue({ + data: { + users: { + nodes: [{ username: 'root', name: 'root', webUrl: '', avatarUrl: '', id: '' }], + }, + }, + }); }); - it('returns the correct query', () => { - expect(wrapper.vm.$options.apollo.participants.query).toEqual(getIssueParticipants); - }); + describe('when search is empty', () => { + beforeEach(() => { + createComponentWithApollo(); + }); - it('contains the correct variables', () => { - const { variables } = wrapper.vm.$options.apollo.participants; - const boundVariable = variables.bind(wrapper.vm); + it('calls getIssueParticipants', async () => { + jest.runOnlyPendingTimers(); + await wrapper.vm.$nextTick(); - expect(boundVariable()).toEqual({ id: 'gid://gitlab/Issue/111' }); + expect(getIssueParticipantsSpy).toHaveBeenCalledWith({ id: 'gid://gitlab/Issue/111' }); + }); }); - it('returns the correct data from update', () => { - const node = { test: 1 }; - const { update } = wrapper.vm.$options.apollo.participants; + describe('when search is not empty', () => { + beforeEach(() => { + createComponentWithApollo('search term'); + }); + + it('calls searchUsers', async () => { + jest.runOnlyPendingTimers(); + await wrapper.vm.$nextTick(); - expect(update({ issue: { participants: { nodes: [node] } } })).toEqual([node]); + expect(getSearchUsersSpy).toHaveBeenCalledWith({ search: 'search term' }); + }); }); }); + + it('finds GlSearchBoxByType', async () => { + createComponent(); + + await openDropdown(); + + expect(wrapper.find(GlSearchBoxByType).exists()).toBe(true); + }); }); diff --git a/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js b/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js index 9c5c97c57e8..233c488b60b 100644 --- a/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js @@ -15,4 +15,17 @@ describe('MultiSelectDropdown Component', () => { }); expect(getByText(wrapper.element, 'Test')).toBeDefined(); }); + + it('renders search slot', () => { + const wrapper = shallowMount(MultiSelectDropdown, { + propsData: { + text: '', + headerText: '', + }, + slots: { + search: '<p>Search</p>', + }, + }); + expect(getByText(wrapper.element, 'Search')).toBeDefined(); + }); }); diff --git a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js new file mode 100644 index 00000000000..bbcdfb5cd99 --- /dev/null +++ b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js @@ -0,0 +1,203 @@ +import MockAdapter from 'axios-mock-adapter'; +import testAction from 'helpers/vuex_action_helper'; + +import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state'; +import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types'; +import * as actions from '~/vue_shared/security_reports/store/modules/secret_detection/actions'; +import axios from '~/lib/utils/axios_utils'; + +const diffEndpoint = 'diff-endpoint.json'; +const blobPath = 'blob-path.json'; +const reports = { + base: 'base', + head: 'head', + enrichData: 'enrichData', + diff: 'diff', +}; +const error = 'Something went wrong'; +const vulnerabilityFeedbackPath = 'vulnerability-feedback-path'; +const rootState = { vulnerabilityFeedbackPath, blobPath }; + +let state; + +describe('secret detection report actions', () => { + beforeEach(() => { + state = createState(); + }); + + describe('setDiffEndpoint', () => { + it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, done => { + testAction( + actions.setDiffEndpoint, + diffEndpoint, + state, + [ + { + type: types.SET_DIFF_ENDPOINT, + payload: diffEndpoint, + }, + ], + [], + done, + ); + }); + }); + + describe('requestDiff', () => { + it(`should commit ${types.REQUEST_DIFF}`, done => { + testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], [], done); + }); + }); + + describe('receiveDiffSuccess', () => { + it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, done => { + testAction( + actions.receiveDiffSuccess, + reports, + state, + [ + { + type: types.RECEIVE_DIFF_SUCCESS, + payload: reports, + }, + ], + [], + done, + ); + }); + }); + + describe('receiveDiffError', () => { + it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, done => { + testAction( + actions.receiveDiffError, + error, + state, + [ + { + type: types.RECEIVE_DIFF_ERROR, + payload: error, + }, + ], + [], + done, + ); + }); + }); + + describe('fetchDiff', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + state.paths.diffEndpoint = diffEndpoint; + rootState.canReadVulnerabilityFeedback = true; + }); + + afterEach(() => { + mock.restore(); + }); + + describe('when diff and vulnerability feedback endpoints respond successfully', () => { + beforeEach(() => { + mock + .onGet(diffEndpoint) + .replyOnce(200, reports.diff) + .onGet(vulnerabilityFeedbackPath) + .replyOnce(200, reports.enrichData); + }); + + it('should dispatch the `receiveDiffSuccess` action', done => { + const { diff, enrichData } = reports; + testAction( + actions.fetchDiff, + {}, + { ...rootState, ...state }, + [], + [ + { type: 'requestDiff' }, + { + type: 'receiveDiffSuccess', + payload: { + diff, + enrichData, + }, + }, + ], + done, + ); + }); + }); + + describe('when diff endpoint responds successfully and fetching vulnerability feedback is not authorized', () => { + beforeEach(() => { + rootState.canReadVulnerabilityFeedback = false; + mock.onGet(diffEndpoint).replyOnce(200, reports.diff); + }); + + it('should dispatch the `receiveDiffSuccess` action with empty enrich data', done => { + const { diff } = reports; + const enrichData = []; + testAction( + actions.fetchDiff, + {}, + { ...rootState, ...state }, + [], + [ + { type: 'requestDiff' }, + { + type: 'receiveDiffSuccess', + payload: { + diff, + enrichData, + }, + }, + ], + done, + ); + }); + }); + + describe('when the vulnerability feedback endpoint fails', () => { + beforeEach(() => { + mock + .onGet(diffEndpoint) + .replyOnce(200, reports.diff) + .onGet(vulnerabilityFeedbackPath) + .replyOnce(404); + }); + + it('should dispatch the `receiveDiffError` action', done => { + testAction( + actions.fetchDiff, + {}, + { ...rootState, ...state }, + [], + [{ type: 'requestDiff' }, { type: 'receiveDiffError' }], + done, + ); + }); + }); + + describe('when the diff endpoint fails', () => { + beforeEach(() => { + mock + .onGet(diffEndpoint) + .replyOnce(404) + .onGet(vulnerabilityFeedbackPath) + .replyOnce(200, reports.enrichData); + }); + + it('should dispatch the `receiveDiffError` action', done => { + testAction( + actions.fetchDiff, + {}, + { ...rootState, ...state }, + [], + [{ type: 'requestDiff' }, { type: 'receiveDiffError' }], + done, + ); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js new file mode 100644 index 00000000000..13fcc0f47a3 --- /dev/null +++ b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js @@ -0,0 +1,84 @@ +import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types'; +import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state'; +import mutations from '~/vue_shared/security_reports/store/modules/secret_detection/mutations'; + +const createIssue = ({ ...config }) => ({ changed: false, ...config }); + +describe('secret detection module mutations', () => { + const path = 'path'; + let state; + + beforeEach(() => { + state = createState(); + }); + + describe(types.SET_DIFF_ENDPOINT, () => { + it('should set the secret detection diff endpoint', () => { + mutations[types.SET_DIFF_ENDPOINT](state, path); + + expect(state.paths.diffEndpoint).toBe(path); + }); + }); + + describe(types.REQUEST_DIFF, () => { + it('should set the `isLoading` status to `true`', () => { + mutations[types.REQUEST_DIFF](state); + + expect(state.isLoading).toBe(true); + }); + }); + + describe(types.RECEIVE_DIFF_SUCCESS, () => { + beforeEach(() => { + const reports = { + diff: { + added: [ + createIssue({ cve: 'CVE-1' }), + createIssue({ cve: 'CVE-2' }), + createIssue({ cve: 'CVE-3' }), + ], + fixed: [createIssue({ cve: 'CVE-4' }), createIssue({ cve: 'CVE-5' })], + existing: [createIssue({ cve: 'CVE-6' })], + base_report_out_of_date: true, + }, + }; + state.isLoading = true; + mutations[types.RECEIVE_DIFF_SUCCESS](state, reports); + }); + + it('should set the `isLoading` status to `false`', () => { + expect(state.isLoading).toBe(false); + }); + + it('should set the `baseReportOutofDate` status to `true`', () => { + expect(state.baseReportOutofDate).toBe(true); + }); + + it('should have the relevant `new` issues', () => { + expect(state.newIssues).toHaveLength(3); + }); + + it('should have the relevant `resolved` issues', () => { + expect(state.resolvedIssues).toHaveLength(2); + }); + + it('should have the relevant `all` issues', () => { + expect(state.allIssues).toHaveLength(1); + }); + }); + + describe(types.RECEIVE_DIFF_ERROR, () => { + beforeEach(() => { + state.isLoading = true; + mutations[types.RECEIVE_DIFF_ERROR](state); + }); + + it('should set the `isLoading` status to `false`', () => { + expect(state.isLoading).toBe(false); + }); + + it('should set the `hasError` status to `true`', () => { + expect(state.hasError).toBe(true); + }); + }); +}); diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index ef1f0940074..3580959fde0 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -130,6 +130,38 @@ RSpec.describe DiffHelper do end end + describe "#diff_link_number" do + using RSpec::Parameterized::TableSyntax + + let(:line) do + double(:line, type: line_type) + end + + # This helper is used to generate the line numbers on the + # diff lines. It essentially just returns a blank string + # on the old/new lines. The following table tests all the + # possible permutations for clarity. + + where(:line_type, :match, :line_number, :expected_return_value) do + "new" | "new" | 1 | " " + "new" | "old" | 2 | 2 + "old" | "new" | 3 | 3 + "old" | "old" | 4 | " " + "new-nonewline" | "new" | 5 | 5 + "new-nonewline" | "old" | 6 | 6 + "old-nonewline" | "new" | 7 | 7 + "old-nonewline" | "old" | 8 | 8 + "match" | "new" | 9 | 9 + "match" | "old" | 10 | 10 + end + + with_them do + it "returns the expected value" do + expect(helper.diff_link_number(line.type, match, line_number)).to eq(expected_return_value) + end + end + end + describe "#mark_inline_diffs" do let(:old_line) { %{abc 'def'} } let(:new_line) { %{abc "def"} } diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb index 4a5b70ff248..a994b4b92a6 100644 --- a/spec/lib/expand_variables_spec.rb +++ b/spec/lib/expand_variables_spec.rb @@ -3,106 +3,132 @@ require 'spec_helper' RSpec.describe ExpandVariables do + shared_examples 'common variable expansion' do |expander| + using RSpec::Parameterized::TableSyntax + + where do + { + "no expansion": { + value: 'key', + result: 'key', + variables: [] + }, + "simple expansion": { + value: 'key$variable', + result: 'keyvalue', + variables: [ + { key: 'variable', value: 'value' } + ] + }, + "simple with hash of variables": { + value: 'key$variable', + result: 'keyvalue', + variables: { + 'variable' => 'value' + } + }, + "complex expansion": { + value: 'key${variable}', + result: 'keyvalue', + variables: [ + { key: 'variable', value: 'value' } + ] + }, + "simple expansions": { + value: 'key$variable$variable2', + result: 'keyvalueresult', + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ] + }, + "complex expansions": { + value: 'key${variable}${variable2}', + result: 'keyvalueresult', + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ] + }, + "out-of-order expansion": { + value: 'key$variable2$variable', + result: 'keyresultvalue', + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ] + }, + "out-of-order complex expansion": { + value: 'key${variable2}${variable}', + result: 'keyresultvalue', + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ] + }, + "review-apps expansion": { + value: 'review/$CI_COMMIT_REF_NAME', + result: 'review/feature/add-review-apps', + variables: [ + { key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' } + ] + }, + "do not lazily access variables when no expansion": { + value: 'key', + result: 'key', + variables: -> { raise NotImplementedError } + }, + "lazily access variables": { + value: 'key$variable', + result: 'keyvalue', + variables: -> { [{ key: 'variable', value: 'value' }] } + } + } + end + + with_them do + subject { expander.call(value, variables) } + + it { is_expected.to eq(result) } + end + end + describe '#expand' do context 'table tests' do - using RSpec::Parameterized::TableSyntax - - where do - { - "no expansion": { - value: 'key', - result: 'key', - variables: [] - }, - "missing variable": { - value: 'key$variable', - result: 'key', - variables: [] - }, - "simple expansion": { - value: 'key$variable', - result: 'keyvalue', - variables: [ - { key: 'variable', value: 'value' } - ] - }, - "simple with hash of variables": { - value: 'key$variable', - result: 'keyvalue', - variables: { - 'variable' => 'value' + it_behaves_like 'common variable expansion', described_class.method(:expand) + + context 'with missing variables' do + using RSpec::Parameterized::TableSyntax + + where do + { + "missing variable": { + value: 'key$variable', + result: 'key', + variables: [] + }, + "complex expansions with missing variable": { + value: 'key${variable}${variable2}', + result: 'keyvalue', + variables: [ + { key: 'variable', value: 'value' } + ] + }, + "complex expansions with missing variable for Windows": { + value: 'key%variable%%variable2%', + result: 'keyvalue', + variables: [ + { key: 'variable', value: 'value' } + ] } - }, - "complex expansion": { - value: 'key${variable}', - result: 'keyvalue', - variables: [ - { key: 'variable', value: 'value' } - ] - }, - "simple expansions": { - value: 'key$variable$variable2', - result: 'keyvalueresult', - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ] - }, - "complex expansions": { - value: 'key${variable}${variable2}', - result: 'keyvalueresult', - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ] - }, - "complex expansions with missing variable": { - value: 'key${variable}${variable2}', - result: 'keyvalue', - variables: [ - { key: 'variable', value: 'value' } - ] - }, - "out-of-order expansion": { - value: 'key$variable2$variable', - result: 'keyresultvalue', - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ] - }, - "out-of-order complex expansion": { - value: 'key${variable2}${variable}', - result: 'keyresultvalue', - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ] - }, - "review-apps expansion": { - value: 'review/$CI_COMMIT_REF_NAME', - result: 'review/feature/add-review-apps', - variables: [ - { key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' } - ] - }, - "do not lazily access variables when no expansion": { - value: 'key', - result: 'key', - variables: -> { raise NotImplementedError } - }, - "lazily access variables": { - value: 'key$variable', - result: 'keyvalue', - variables: -> { [{ key: 'variable', value: 'value' }] } } - } - end + end - with_them do - subject { ExpandVariables.expand(value, variables) } + with_them do + subject { ExpandVariables.expand(value, variables) } - it { is_expected.to eq(result) } + it { is_expected.to eq(result) } + end end end @@ -132,4 +158,70 @@ RSpec.describe ExpandVariables do end end end + + describe '#expand_existing' do + context 'table tests' do + it_behaves_like 'common variable expansion', described_class.method(:expand_existing) + + context 'with missing variables' do + using RSpec::Parameterized::TableSyntax + + where do + { + "missing variable": { + value: 'key$variable', + result: 'key$variable', + variables: [] + }, + "complex expansions with missing variable": { + value: 'key${variable}${variable2}', + result: 'keyvalue${variable2}', + variables: [ + { key: 'variable', value: 'value' } + ] + }, + "complex expansions with missing variable for Windows": { + value: 'key%variable%%variable2%', + result: 'keyvalue%variable2%', + variables: [ + { key: 'variable', value: 'value' } + ] + } + } + end + + with_them do + subject { ExpandVariables.expand_existing(value, variables) } + + it { is_expected.to eq(result) } + end + end + end + + context 'lazily inits variables' do + let(:variables) { -> { [{ key: 'variable', value: 'result' }] } } + + subject { described_class.expand_existing(value, variables) } + + context 'when expanding variable' do + let(:value) { 'key$variable$variable2' } + + it 'calls block at most once' do + expect(variables).to receive(:call).once.and_call_original + + is_expected.to eq('keyresult$variable2') + end + end + + context 'when no expansion is needed' do + let(:value) { 'key' } + + it 'does not call block' do + expect(variables).not_to receive(:call) + + is_expected.to eq('key') + end + end + end + end end diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb index cf52f601006..fbe139b20db 100644 --- a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb +++ b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb @@ -13,5 +13,41 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do subject { described_class.new(globs).satisfied_by?(pipeline, nil) } end + + context 'when using variable expansion' do + let(:pipeline) { build(:ci_pipeline) } + let(:modified_paths) { ['helm/test.txt'] } + let(:globs) { ['$HELM_DIR/**/*'] } + let(:context) { double('context') } + let(:variables) { [] } + + subject { described_class.new(globs).satisfied_by?(pipeline, context) } + + before do + allow(pipeline).to receive(:modified_paths).and_return(modified_paths) + allow(context).to receive(:variables).and_return(variables) + end + + context 'when context is nil' do + let(:context) {} + + it { is_expected.to be_falsey } + end + + context 'when context has the specified variables' do + let(:variables) do + [{ key: "HELM_DIR", value: "helm", public: true }] + end + + it { is_expected.to be_truthy } + end + + context 'when variable expansion does not match' do + let(:globs) { ['path/with/$in/it/*'] } + let(:modified_paths) { ['path/with/$in/it/file.txt'] } + + it { is_expected.to be_truthy } + end + end end end diff --git a/spec/lib/gitlab/ci/charts_spec.rb b/spec/lib/gitlab/ci/charts_spec.rb index e00e5ed3920..cfc2019a89b 100644 --- a/spec/lib/gitlab/ci/charts_spec.rb +++ b/spec/lib/gitlab/ci/charts_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Charts do - context "yearchart" do + context 'yearchart' do let(:project) { create(:project) } let(:chart) { Gitlab::Ci::Charts::YearChart.new(project) } @@ -16,9 +16,13 @@ RSpec.describe Gitlab::Ci::Charts do it 'starts at the beginning of the current year' do expect(chart.from).to eq(chart.to.years_ago(1).beginning_of_month.beginning_of_day) end + + it 'uses %B %Y as labels format' do + expect(chart.labels).to include(chart.from.strftime('%B %Y')) + end end - context "monthchart" do + context 'monthchart' do let(:project) { create(:project) } let(:chart) { Gitlab::Ci::Charts::MonthChart.new(project) } @@ -31,9 +35,13 @@ RSpec.describe Gitlab::Ci::Charts do it 'starts one month ago' do expect(chart.from).to eq(1.month.ago.beginning_of_day) end + + it 'uses %d %B as labels format' do + expect(chart.labels).to include(chart.from.strftime('%d %B')) + end end - context "weekchart" do + context 'weekchart' do let(:project) { create(:project) } let(:chart) { Gitlab::Ci::Charts::WeekChart.new(project) } @@ -46,9 +54,13 @@ RSpec.describe Gitlab::Ci::Charts do it 'starts one week ago' do expect(chart.from).to eq(1.week.ago.beginning_of_day) end + + it 'uses %d %B as labels format' do + expect(chart.labels).to include(chart.from.strftime('%d %B')) + end end - context "pipeline_times" do + context 'pipeline_times' do let(:project) { create(:project) } let(:chart) { Gitlab::Ci::Charts::PipelineTime.new(project) } diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 980a52bb61e..d4174a34433 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -301,19 +301,19 @@ EOT describe '#too_large?' do it 'returns true for a diff that is too large' do - diff = described_class.new(diff: 'a' * 204800) + diff = described_class.new({ diff: 'a' * 204800 }) expect(diff.too_large?).to eq(true) end it 'returns false for a diff that is small enough' do - diff = described_class.new(diff: 'a') + diff = described_class.new({ diff: 'a' }) expect(diff.too_large?).to eq(false) end it 'returns true for a diff that was explicitly marked as being too large' do - diff = described_class.new(diff: 'a') + diff = described_class.new({ diff: 'a' }) diff.too_large! @@ -323,19 +323,19 @@ EOT describe '#collapsed?' do it 'returns false by default even on quite big diff' do - diff = described_class.new(diff: 'a' * 20480) + diff = described_class.new({ diff: 'a' * 20480 }) expect(diff).not_to be_collapsed end it 'returns false by default for a diff that is small enough' do - diff = described_class.new(diff: 'a') + diff = described_class.new({ diff: 'a' }) expect(diff).not_to be_collapsed end it 'returns true for a diff that was explicitly marked as being collapsed' do - diff = described_class.new(diff: 'a') + diff = described_class.new({ diff: 'a' }) diff.collapse! @@ -359,7 +359,7 @@ EOT describe '#collapse!' do it 'prunes the diff' do - diff = described_class.new(diff: "foo\nbar") + diff = described_class.new({ diff: "foo\nbar" }) diff.collapse! diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb index 4e6a1e9b8fb..7eb1a6fd67d 100644 --- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb @@ -8,6 +8,9 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s let(:entity3) { '34rfjuuy-ce56-sa35-ds34-dfer567dfrf2' } let(:entity4) { '8b9a2671-2abf-4bec-a682-22f6a8f7bf31' } + let(:default_context) { 'default' } + let(:invalid_context) { 'invalid' } + around do |example| # We need to freeze to a reference time # because visits are grouped by the week number in the year @@ -55,11 +58,13 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s let(:no_slot) { 'no_slot' } let(:different_aggregation) { 'different_aggregation' } let(:custom_daily_event) { 'g_analytics_custom' } + let(:context_event) { 'context_event' } let(:global_category) { 'global' } let(:compliance_category) { 'compliance' } let(:productivity_category) { 'productivity' } let(:analytics_category) { 'analytics' } + let(:other_category) { 'other' } let(:known_events) do [ @@ -68,7 +73,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s { name: category_productivity_event, redis_slot: "analytics", category: productivity_category, aggregation: "weekly" }, { name: compliance_slot_event, redis_slot: "compliance", category: compliance_category, aggregation: "weekly" }, { name: no_slot, category: global_category, aggregation: "daily" }, - { name: different_aggregation, category: global_category, aggregation: "monthly" } + { name: different_aggregation, category: global_category, aggregation: "monthly" }, + { name: context_event, category: other_category, expiry: 6, aggregation: 'weekly' } ].map(&:with_indifferent_access) end @@ -170,6 +176,34 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s end end + describe '.track_event_in_context' do + context 'with valid contex' do + it 'increments conext event counte' do + expect(Gitlab::Redis::HLL).to receive(:add) do |kwargs| + expect(kwargs[:key]).to match(/^#{default_context}\_.*/) + end + + described_class.track_event_in_context(entity1, context_event, default_context) + end + end + + context 'with empty context' do + it 'does not increment a counter' do + expect(Gitlab::Redis::HLL).not_to receive(:add) + + described_class.track_event_in_context(entity1, context_event, '') + end + end + + context 'when sending invalid context' do + it 'does not increment a counter' do + expect(Gitlab::Redis::HLL).not_to receive(:add) + + described_class.track_event_in_context(entity1, context_event, invalid_context) + end + end + end + describe '.unique_events' do before do # events in current week, should not be counted as week is not complete @@ -250,6 +284,48 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s end end + describe 'context level tracking' do + using RSpec::Parameterized::TableSyntax + + let(:known_events) do + [ + { name: 'event_name_1', redis_slot: 'event', category: 'category1', aggregation: "weekly" }, + { name: 'event_name_2', redis_slot: 'event', category: 'category1', aggregation: "weekly" }, + { name: 'event_name_3', redis_slot: 'event', category: 'category1', aggregation: "weekly" } + ].map(&:with_indifferent_access) + end + + before do + allow(described_class).to receive(:known_events).and_return(known_events) + allow(described_class).to receive(:categories).and_return(%w(category1 category2)) + + described_class.track_event_in_context([entity1, entity3], 'event_name_1', default_context, 2.days.ago) + described_class.track_event_in_context(entity3, 'event_name_1', default_context, 2.days.ago) + described_class.track_event_in_context(entity3, 'event_name_1', invalid_context, 2.days.ago) + described_class.track_event_in_context([entity1, entity2], 'event_name_2', '', 2.weeks.ago) + end + + subject(:unique_events) { described_class.unique_events(event_names: event_names, start_date: 4.weeks.ago, end_date: Date.current, context: context) } + + context 'with correct arguments' do + where(:event_names, :context, :value) do + ['event_name_1'] | 'default' | 2 + ['event_name_1'] | '' | 0 + ['event_name_2'] | '' | 0 + end + + with_them do + it { is_expected.to eq value } + end + end + + context 'with invalid context' do + it 'raise error' do + expect { described_class.unique_events(event_names: 'event_name_1', start_date: 4.weeks.ago, end_date: Date.current, context: invalid_context) }.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::InvalidContext) + end + end + end + describe 'unique_events_data' do let(:known_events) do [ diff --git a/spec/policies/group_member_policy_spec.rb b/spec/policies/group_member_policy_spec.rb index 4215fa09301..9e58ea81ef3 100644 --- a/spec/policies/group_member_policy_spec.rb +++ b/spec/policies/group_member_policy_spec.rb @@ -42,6 +42,7 @@ RSpec.describe GroupMemberPolicy do it do expect_disallowed(:destroy_group_member) expect_disallowed(:update_group_member) + expect_allowed(:read_group) end end diff --git a/spec/policies/prometheus_service_policy_spec.rb b/spec/policies/prometheus_service_policy_spec.rb deleted file mode 100644 index 71fa2b9d630..00000000000 --- a/spec/policies/prometheus_service_policy_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe PrometheusServicePolicy, :models do - let(:integration) { create(:prometheus_service) } - let(:project) { integration.project } - let(:user) { create(:user) } - - subject(:policy) { described_class.new(user, integration) } - - describe 'rules' do - it { is_expected.to be_disallowed :admin_project } - - context 'when maintainer' do - before do - project.add_maintainer(user) - end - - it { is_expected.to be_allowed :admin_project } - end - end -end diff --git a/spec/policies/service_policy_spec.rb b/spec/policies/service_policy_spec.rb new file mode 100644 index 00000000000..5d2c9c1f6c3 --- /dev/null +++ b/spec/policies/service_policy_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ServicePolicy, :models do + let_it_be(:user) { create(:user) } + let(:project) { integration.project } + + subject(:policy) { Ability.policy_for(user, integration) } + + context 'when the integration is a prometheus_service' do + let(:integration) { create(:prometheus_service) } + + describe 'rules' do + it { is_expected.to be_disallowed :admin_project } + + context 'when maintainer' do + before do + project.add_maintainer(user) + end + + it { is_expected.to be_allowed :admin_project } + end + end + end +end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 223b8d325cc..f9015752644 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1870,6 +1870,12 @@ RSpec.describe Ci::CreatePipelineService do - changes: - README.md allow_failure: true + + README: + script: "I use variables for changes!" + rules: + - changes: + - $CI_JOB_NAME* EOY end @@ -1879,10 +1885,10 @@ RSpec.describe Ci::CreatePipelineService do .to receive(:modified_paths).and_return(%w[README.md]) end - it 'creates two jobs' do + it 'creates five jobs' do expect(pipeline).to be_persisted expect(build_names) - .to contain_exactly('regular-job', 'rules-job', 'delayed-job', 'negligible-job') + .to contain_exactly('regular-job', 'rules-job', 'delayed-job', 'negligible-job', 'README') end it 'sets when: for all jobs' do |