diff options
75 files changed, 1113 insertions, 359 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 0f0844a7987..6b91fc8c428 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -842,7 +842,6 @@ Rails/SaveBang: - 'spec/controllers/projects/imports_controller_spec.rb' - 'spec/controllers/projects/issues_controller_spec.rb' - 'spec/controllers/projects/labels_controller_spec.rb' - - 'spec/controllers/projects/merge_requests_controller_spec.rb' - 'spec/controllers/projects/milestones_controller_spec.rb' - 'spec/controllers/projects/notes_controller_spec.rb' - 'spec/controllers/projects/pipelines_controller_spec.rb' @@ -1264,8 +1263,6 @@ FactoryBot/InlineAssociation: - 'ee/spec/factories/geo/event_log.rb' - 'ee/spec/factories/groups.rb' - 'ee/spec/factories/merge_request_blocks.rb' - - 'ee/spec/factories/resource_iteration_event.rb' - - 'ee/spec/factories/resource_weight_events.rb' - 'ee/spec/factories/vulnerabilities/feedback.rb' - 'spec/factories/atlassian_identities.rb' - 'spec/factories/design_management/design_at_version.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 03630f1d18e..2e96a9ff62e 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -b011445d9ffa82c36c771946e035852888df0730 +b85367529ca34c8c423b94b7486c44e109ed553f @@ -317,7 +317,7 @@ gem 'premailer-rails', '~> 1.10.3' gem 'gitlab-labkit', '0.13.1' # I18n -gem 'ruby_parser', '~> 3.8', require: false +gem 'ruby_parser', '~> 3.15', require: false gem 'rails-i18n', '~> 6.0' gem 'gettext_i18n_rails', '~> 1.8.0' gem 'gettext_i18n_rails_js', '~> 1.3' diff --git a/Gemfile.lock b/Gemfile.lock index 21c36eb4c23..7b7223af8b6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1042,7 +1042,7 @@ GEM nokogiri (>= 1.5.10) ruby-statistics (2.1.2) ruby2_keywords (0.0.2) - ruby_parser (3.13.1) + ruby_parser (3.15.0) sexp_processor (~> 4.9) rubyntlm (0.6.2) rubypants (0.2.0) @@ -1085,7 +1085,7 @@ GEM sentry-raven (3.0.4) faraday (>= 1.0) settingslogic (2.0.9) - sexp_processor (4.12.0) + sexp_processor (4.15.1) shellany (0.0.1) shoulda-matchers (4.0.1) activesupport (>= 4.2.0) @@ -1474,7 +1474,7 @@ DEPENDENCIES ruby-fogbugz (~> 0.2.1) ruby-prof (~> 1.3.0) ruby-progressbar - ruby_parser (~> 3.8) + ruby_parser (~> 3.15) rubyzip (~> 2.0.0) rugged (~> 0.28) sanitize (~> 5.2.1) diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue index f8fb58cdca2..3e06e8264d3 100644 --- a/app/assets/javascripts/clusters_list/components/clusters.vue +++ b/app/assets/javascripts/clusters_list/components/clusters.vue @@ -8,10 +8,10 @@ import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf, GlTable, + GlTooltipDirective, } from '@gitlab/ui'; import AncestorNotice from './ancestor_notice.vue'; import NodeErrorHelpText from './node_error_help_text.vue'; -import tooltip from '~/vue_shared/directives/tooltip'; import { CLUSTER_TYPES, STATUSES } from '../constants'; import { __, sprintf } from '~/locale'; @@ -30,7 +30,7 @@ export default { NodeErrorHelpText, }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, computed: { ...mapState([ @@ -227,7 +227,7 @@ export default { <gl-loading-icon v-if="item.status === 'deleting' || item.status === 'creating'" - v-tooltip + v-gl-tooltip :title="statusTitle(item.status)" size="sm" /> diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js index 37f887bcf0a..416ca88d6c9 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -14,6 +14,8 @@ const createTranslatedTextForFiles = (files, text) => { export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading; +// Note: If changing the structure of the placeholder branch name, please also +// update #patch_branch_name in app/helpers/tree_helper.rb export const placeholderBranchName = (state, _, rootState) => `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr( -BRANCH_SUFFIX_COUNT, diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue index 4d527baf730..a3d7ddd5bad 100644 --- a/app/assets/javascripts/notebook/cells/output/html.vue +++ b/app/assets/javascripts/notebook/cells/output/html.vue @@ -37,6 +37,6 @@ export default { <template> <div class="output"> <prompt type="Out" :count="count" :show-output="showOutput" /> - <div v-html="sanitizedOutput"></div> + <div class="gl-overflow-auto" v-html="sanitizedOutput"></div> </div> </template> diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue index f58e4909a08..7b5e0f70b7b 100644 --- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue +++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue @@ -1,15 +1,21 @@ <script> -import { GlSprintf } from '@gitlab/ui'; +import { GlSprintf, GlModal } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; import { deprecatedCreateFlash as createFlash } from '~/flash'; -import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; -import { s__, sprintf } from '~/locale'; +import { s__, __, sprintf } from '~/locale'; import { visitUrl } from '~/lib/utils/url_utility'; import eventHub from '../event_hub'; export default { + primaryProps: { + text: s__('Labels|Promote Label'), + attributes: [{ variant: 'warning' }, { category: 'primary' }], + }, + cancelProps: { + text: __('Cancel'), + }, components: { - GlModal: DeprecatedModal2, + GlModal, GlSprintf, }, props: { @@ -72,12 +78,12 @@ export default { </script> <template> <gl-modal - id="promote-label-modal" - :footer-primary-button-text="s__('Labels|Promote Label')" - footer-primary-button-variant="warning" - @submit="onSubmit" + modal-id="promote-label-modal" + :action-primary="$options.primaryProps" + :action-cancel="$options.cancelProps" + @primary="onSubmit" > - <div slot="title" class="modal-title-with-label"> + <div slot="modal-title" class="modal-title-with-label"> <gl-sprintf :message=" s__( diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js index 36cf485f33d..ee129011f9a 100644 --- a/app/assets/javascripts/pages/projects/labels/index/index.js +++ b/app/assets/javascripts/pages/projects/labels/index/index.js @@ -27,71 +27,55 @@ const initLabelIndex = () => { eventHub.$once('promoteLabelModal.requestFinished', onRequestFinished); }; - const onDeleteButtonClick = event => { - const button = event.currentTarget; - const modalProps = { - labelTitle: button.dataset.labelTitle, - labelColor: button.dataset.labelColor, - labelTextColor: button.dataset.labelTextColor, - url: button.dataset.url, - groupName: button.dataset.groupName, - }; - eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted); - eventHub.$emit('promoteLabelModal.props', modalProps); - }; - const promoteLabelButtons = document.querySelectorAll('.js-promote-project-label-button'); - promoteLabelButtons.forEach(button => { - button.addEventListener('click', onDeleteButtonClick); - }); - eventHub.$once('promoteLabelModal.mounted', () => { - promoteLabelButtons.forEach(button => { - button.removeAttribute('disabled'); - }); - }); + return new Vue({ + el: '#js-promote-label-modal', + data() { + return { + modalProps: { + labelTitle: '', + labelColor: '', + labelTextColor: '', + url: '', + groupName: '', + }, + }; + }, + mounted() { + eventHub.$on('promoteLabelModal.props', this.setModalProps); + eventHub.$emit('promoteLabelModal.mounted'); - const promoteLabelModal = document.getElementById('promote-label-modal'); - let promoteLabelModalComponent; + promoteLabelButtons.forEach(button => { + button.removeAttribute('disabled'); + button.addEventListener('click', () => { + this.$root.$emit('bv::show::modal', 'promote-label-modal'); + eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted); - if (promoteLabelModal) { - promoteLabelModalComponent = new Vue({ - el: promoteLabelModal, - components: { - PromoteLabelModal, - }, - data() { - return { - modalProps: { - labelTitle: '', - labelColor: '', - labelTextColor: '', - url: '', - groupName: '', - }, - }; - }, - mounted() { - eventHub.$on('promoteLabelModal.props', this.setModalProps); - eventHub.$emit('promoteLabelModal.mounted'); - }, - beforeDestroy() { - eventHub.$off('promoteLabelModal.props', this.setModalProps); - }, - methods: { - setModalProps(modalProps) { - this.modalProps = modalProps; - }, - }, - render(createElement) { - return createElement('promote-label-modal', { - props: this.modalProps, + this.setModalProps({ + labelTitle: button.dataset.labelTitle, + labelColor: button.dataset.labelColor, + labelTextColor: button.dataset.labelTextColor, + url: button.dataset.url, + groupName: button.dataset.groupName, + }); }); + }); + }, + beforeDestroy() { + eventHub.$off('promoteLabelModal.props', this.setModalProps); + }, + methods: { + setModalProps(modalProps) { + this.modalProps = modalProps; }, - }); - } - - return promoteLabelModalComponent; + }, + render(createElement) { + return createElement(PromoteLabelModal, { + props: this.modalProps, + }); + }, + }); }; document.addEventListener('DOMContentLoaded', initLabelIndex); diff --git a/app/assets/javascripts/releases/components/issuable_stats.vue b/app/assets/javascripts/releases/components/issuable_stats.vue new file mode 100644 index 00000000000..5f28331c543 --- /dev/null +++ b/app/assets/javascripts/releases/components/issuable_stats.vue @@ -0,0 +1,99 @@ +<script> +import { GlLink, GlBadge, GlSprintf } from '@gitlab/ui'; + +export default { + name: 'IssuableStats', + components: { + GlLink, + GlBadge, + GlSprintf, + }, + props: { + label: { + type: String, + required: true, + }, + total: { + type: Number, + required: true, + }, + closed: { + type: Number, + required: true, + }, + merged: { + type: Number, + required: false, + default: null, + }, + openPath: { + type: String, + required: false, + default: '', + }, + closedPath: { + type: String, + required: false, + default: '', + }, + mergedPath: { + type: String, + required: false, + default: '', + }, + }, + computed: { + open() { + return this.total - (this.closed + (this.merged || 0)); + }, + showMerged() { + return this.merged != null; + }, + }, +}; +</script> + +<template> + <div + class="gl-display-flex gl-flex-direction-column gl-flex-shrink-0 gl-mr-6 gl-mb-5 js-issues-container" + > + <span class="gl-mb-2"> + {{ label }} + <gl-badge variant="muted" size="sm">{{ total }}</gl-badge> + </span> + <div class="gl-display-flex"> + <span class="gl-white-space-pre-wrap" data-testid="open-stat"> + <gl-sprintf :message="__('Open: %{open}')"> + <template #open> + <gl-link v-if="openPath" :href="openPath">{{ open }}</gl-link> + <template v-else>{{ open }}</template> + </template> + </gl-sprintf> + </span> + + <template v-if="showMerged"> + <span class="gl-mx-2">•</span> + + <span class="gl-white-space-pre-wrap" data-testid="merged-stat"> + <gl-sprintf :message="__('Merged: %{merged}')"> + <template #merged> + <gl-link v-if="mergedPath" :href="mergedPath">{{ merged }}</gl-link> + <template v-else>{{ merged }}</template> + </template> + </gl-sprintf> + </span> + </template> + + <span class="gl-mx-2">•</span> + + <span class="gl-white-space-pre-wrap" data-testid="closed-stat"> + <gl-sprintf :message="__('Closed: %{closed}')"> + <template #closed> + <gl-link v-if="closedPath" :href="closedPath">{{ closed }}</gl-link> + <template v-else>{{ closed }}</template> + </template> + </gl-sprintf> + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/releases/components/release_block_milestone_info.vue b/app/assets/javascripts/releases/components/release_block_milestone_info.vue index deff673cc17..30598a5eec1 100644 --- a/app/assets/javascripts/releases/components/release_block_milestone_info.vue +++ b/app/assets/javascripts/releases/components/release_block_milestone_info.vue @@ -1,24 +1,17 @@ <script> -import { - GlProgressBar, - GlLink, - GlBadge, - GlButton, - GlTooltipDirective, - GlSprintf, -} from '@gitlab/ui'; +import { GlProgressBar, GlLink, GlButton, GlTooltipDirective } from '@gitlab/ui'; import { sum } from 'lodash'; import { __, n__, sprintf } from '~/locale'; import { MAX_MILESTONES_TO_DISPLAY } from '../constants'; +import IssuableStats from './issuable_stats.vue'; export default { name: 'ReleaseBlockMilestoneInfo', components: { GlProgressBar, GlLink, - GlBadge, GlButton, - GlSprintf, + IssuableStats, }, directives: { GlTooltip: GlTooltipDirective, @@ -64,18 +57,9 @@ export default { closedIssuesCount() { return sum(this.allIssueStats.map(stats => stats.closed || 0)); }, - openIssuesCount() { - return this.totalIssuesCount - this.closedIssuesCount; - }, milestoneLabelText() { return n__('Milestone', 'Milestones', this.milestones.length); }, - issueCountsText() { - return sprintf(__('Open: %{open} • Closed: %{closed}'), { - open: this.openIssuesCount, - closed: this.closedIssuesCount, - }); - }, milestonesToDisplay() { return this.showAllMilestones ? this.milestones @@ -106,20 +90,22 @@ export default { }; </script> <template> - <div class="release-block-milestone-info d-flex align-items-start flex-wrap"> + <div class="release-block-milestone-info gl-display-flex gl-flex-wrap"> <div v-gl-tooltip - class="milestone-progress-bar-container js-milestone-progress-bar-container d-flex flex-column align-items-start flex-shrink-1 mr-4 mb-3" + class="milestone-progress-bar-container js-milestone-progress-bar-container gl-display-flex gl-flex-direction-column gl-mr-6 gl-mb-5" :title="__('Closed issues')" > - <span class="mb-2">{{ percentCompleteText }}</span> - <span class="w-100"> + <span class="gl-mb-3">{{ percentCompleteText }}</span> + <span class="gl-w-full"> <gl-progress-bar :value="closedIssuesCount" :max="totalIssuesCount" variant="success" /> </span> </div> - <div class="d-flex flex-column align-items-start mr-4 mb-3 js-milestone-list-container"> - <span class="mb-1">{{ milestoneLabelText }}</span> - <div class="d-flex flex-wrap align-items-end"> + <div + class="gl-display-flex gl-flex-direction-column gl-mr-6 gl-mb-5 js-milestone-list-container" + > + <span class="gl-mb-2">{{ milestoneLabelText }}</span> + <div class="gl-display-flex gl-flex-wrap gl-align-items-flex-end"> <template v-for="(milestone, index) in milestonesToDisplay"> <gl-link :key="milestone.id" @@ -141,32 +127,12 @@ export default { </template> </div> </div> - <div class="d-flex flex-column align-items-start flex-shrink-0 mr-4 mb-3 js-issues-container"> - <span class="mb-1"> - {{ __('Issues') }} - <gl-badge variant="muted" size="sm">{{ totalIssuesCount }}</gl-badge> - </span> - <div class="d-flex"> - <gl-link v-if="openIssuesPath" ref="openIssuesLink" :href="openIssuesPath"> - <gl-sprintf :message="__('Open: %{openIssuesCount}')"> - <template #openIssuesCount>{{ openIssuesCount }}</template> - </gl-sprintf> - </gl-link> - <span v-else ref="openIssuesText"> - {{ sprintf(__('Open: %{openIssuesCount}'), { openIssuesCount }) }} - </span> - - <span class="mx-1">•</span> - - <gl-link v-if="closedIssuesPath" ref="closedIssuesLink" :href="closedIssuesPath"> - <gl-sprintf :message="__('Closed: %{closedIssuesCount}')"> - <template #closedIssuesCount>{{ closedIssuesCount }}</template> - </gl-sprintf> - </gl-link> - <span v-else ref="closedIssuesText"> - {{ sprintf(__('Closed: %{closedIssuesCount}'), { closedIssuesCount }) }} - </span> - </div> - </div> + <issuable-stats + :label="__('Issues')" + :total="totalIssuesCount" + :closed="closedIssuesCount" + :open-path="openIssuesPath" + :closed-path="closedIssuesPath" + /> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue new file mode 100644 index 00000000000..eff26729fa7 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue @@ -0,0 +1,157 @@ +<script> +import { GlButton, GlLoadingIcon, GlIcon, GlLink, GlBadge, GlSafeHtmlDirective } from '@gitlab/ui'; +import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; +import StatusIcon from '../mr_widget_status_icon.vue'; + +export const LOADING_STATES = { + collapsedLoading: 'collapsedLoading', + collapsedError: 'collapsedError', + expandedLoading: 'expandedLoading', +}; + +export default { + components: { + GlButton, + GlLoadingIcon, + GlIcon, + GlLink, + GlBadge, + SmartVirtualList, + StatusIcon, + }, + directives: { + SafeHtml: GlSafeHtmlDirective, + }, + data() { + return { + loadingState: LOADING_STATES.collapsedLoading, + collapsedData: null, + fullData: null, + isCollapsed: true, + }; + }, + computed: { + isLoadingSummary() { + return this.loadingState === LOADING_STATES.collapsedLoading; + }, + isLoadingExpanded() { + return this.loadingState === LOADING_STATES.expandedLoading; + }, + isCollapsible() { + if (this.isLoadingSummary) { + return false; + } + + return true; + }, + statusIconName() { + if (this.isLoadingSummary) { + return 'loading'; + } + + if (this.loadingState === LOADING_STATES.collapsedError) { + return 'warning'; + } + + return this.statusIcon(this.collapsedData); + }, + }, + watch: { + isCollapsed(newVal) { + if (!newVal) { + this.loadAllData(); + } else { + this.loadingState = null; + } + }, + }, + mounted() { + this.fetchCollapsedData(this.$props) + .then(data => { + this.collapsedData = data; + this.loadingState = null; + }) + .catch(e => { + this.loadingState = LOADING_STATES.collapsedError; + throw e; + }); + }, + methods: { + toggleCollapsed() { + this.isCollapsed = !this.isCollapsed; + }, + loadAllData() { + if (this.fullData) return; + + this.loadingState = LOADING_STATES.expandedLoading; + + this.fetchFullData(this.$props) + .then(data => { + this.loadingState = null; + this.fullData = data; + }) + .catch(e => { + this.loadingState = null; + throw e; + }); + }, + }, +}; +</script> + +<template> + <section class="media-section mr-widget-border-top"> + <div class="media gl-p-5"> + <status-icon :status="statusIconName" class="align-self-center" /> + <div class="media-body d-flex flex-align-self-center align-items-center"> + <div class="code-text"> + <template v-if="isLoadingSummary"> + {{ __('Loading...') }} + </template> + <div v-else v-safe-html="summary(collapsedData)"></div> + </div> + <gl-button + v-if="isCollapsible" + size="small" + class="float-right align-self-center" + @click="toggleCollapsed" + > + {{ isCollapsed ? __('Expand') : __('Collapse') }} + </gl-button> + </div> + </div> + <div v-if="!isCollapsed" class="mr-widget-grouped-section"> + <div v-if="isLoadingExpanded" class="report-block-container"> + <gl-loading-icon inline /> {{ __('Loading...') }} + </div> + <smart-virtual-list + v-else-if="fullData" + :length="fullData.length" + :remain="20" + :size="32" + wtag="ul" + wclass="report-block-list" + class="report-block-container" + > + <li v-for="data in fullData" :key="data.id" class="d-flex align-items-center"> + <div v-if="data.icon" :class="data.icon.class" class="d-flex"> + <gl-icon :name="data.icon.name" :size="24" /> + </div> + <div + class="gl-mt-2 gl-mb-2 align-content-around align-items-start flex-wrap align-self-center d-flex" + > + <div class="gl-mr-4"> + {{ data.text }} + </div> + <div v-if="data.link"> + <gl-link :href="data.link.href">{{ data.link.text }}</gl-link> + </div> + <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'"> + {{ data.badge.text }} + </gl-badge> + </div> + </li> + </smart-virtual-list> + </div> + </section> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js new file mode 100644 index 00000000000..5014c12dc30 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js @@ -0,0 +1,27 @@ +import { extensions } from './index'; + +export default { + props: { + mr: { + type: Object, + required: true, + }, + }, + render(h) { + return h( + 'div', + {}, + extensions.map(extension => + h(extension, { + props: extensions[0].props.reduce( + (acc, key) => ({ + ...acc, + [key]: this.mr[key], + }), + {}, + ), + }), + ), + ); + }, +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js new file mode 100644 index 00000000000..2bfaec8a1c9 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js @@ -0,0 +1,30 @@ +import ExtensionBase from './base.vue'; + +// Holds all the currently registered extensions +export const extensions = []; + +export const registerExtension = extension => { + // Pushes into the extenions array a dynamically created Vue component + // that gets exteneded from `base.vue` + extensions.push({ + extends: ExtensionBase, + name: extension.name, + props: extension.props, + computed: { + ...Object.keys(extension.computed).reduce( + (acc, computedKey) => ({ + ...acc, + // Making the computed property a method allows us to pass in arguments + // this allows for each computed property to recieve some data + [computedKey]() { + return extension.computed[computedKey]; + }, + }), + {}, + ), + }, + methods: { + ...extension.methods, + }, + }); +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js new file mode 100644 index 00000000000..2d21ced1b28 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js @@ -0,0 +1,66 @@ +/* eslint-disable */ +import issuesCollapsedQuery from './issues_collapsed.query.graphql'; +import issuesQuery from './issues.query.graphql'; + +export default { + // Give the extension a name + // Make it easier to track in Vue dev tools + name: 'WidgetIssues', + // Add an array of props + // These then get mapped to values stored in the MR Widget store + props: ['targetProjectFullPath'], + // Add any extra computed props in here + computed: { + // Small summary text to be displayed in the collapsed state + // Receives the collapsed data as an argument + summary(count) { + return `<strong>${count}</strong> open issue`; + }, + // Status icon to be used next to the summary text + // Receives the collapsed data as an argument + statusIcon(count) { + return count > 0 ? 'warning' : 'success'; + }, + }, + methods: { + // Fetches the collapsed data + // Ideally, this request should return the smallest amount of data possible + // Receives an object of all the props passed in to the extension + fetchCollapsedData({ targetProjectFullPath }) { + return this.$apollo + .query({ query: issuesCollapsedQuery, variables: { projectPath: targetProjectFullPath } }) + .then(({ data }) => data.project.issues.count); + }, + // Fetches the full data when the extension is expanded + // Receives an object of all the props passed in to the extension + fetchFullData({ targetProjectFullPath }) { + return this.$apollo + .query({ query: issuesQuery, variables: { projectPath: targetProjectFullPath } }) + .then(({ data }) => { + // Return some transformed data to be rendered in the expanded state + return data.project.issues.nodes.map(issue => ({ + id: issue.id, // Required: The ID of the object + text: issue.title, // Required: The text to get used on each row + // Icon to get rendered on the side of each row + icon: { + // Required: Name maps to an icon in GitLabs SVG + name: + issue.state === 'closed' ? 'status_failed_borderless' : 'status_success_borderless', + // Optional: An extra class to be added to the icon for additional styling + class: issue.state === 'closed' ? 'text-danger' : 'text-success', + }, + // Badges get rendered next to the text on each row + badge: issue.state === 'closed' && { + text: 'Closed', // Required: Text to be used inside of the badge + // variant: 'info', // Optional: The variant of the badge, maps to GitLab UI variants + }, + // Each row can have its own link that will take the user elsewhere + // link: { + // href: 'https://google.com', // Required: href for the link + // text: 'Link text', // Required: Text to be used inside the link + // }, + })); + }); + }, + }, +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.query.graphql b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.query.graphql new file mode 100644 index 00000000000..690f571c083 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.query.graphql @@ -0,0 +1,13 @@ +query getAllIssues($projectPath: ID!) { + project(fullPath: $projectPath) { + issues { + nodes { + id + title + webPath + webUrl + state + } + } + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql b/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql new file mode 100644 index 00000000000..389a81e0a61 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql @@ -0,0 +1,7 @@ +query getIssues($projectPath: ID!) { + project(fullPath: $projectPath) { + issues { + count + } + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index 87e56dfcbdf..72d4e7063ad 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -3,6 +3,8 @@ import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_optio import VueApollo from 'vue-apollo'; import Translate from '../vue_shared/translate'; import createDefaultClient from '~/lib/graphql'; +import { registerExtension } from './components/extensions'; +import issueExtension from './extensions/issues'; Vue.use(Translate); Vue.use(VueApollo); @@ -17,6 +19,8 @@ export default () => { gl.mrWidgetData.gitlabLogo = gon.gitlab_logo; gl.mrWidgetData.defaultAvatarUrl = gon.default_avatar_url; + registerExtension(issueExtension); + const vm = new Vue({ ...MrWidgetOptions, apolloProvider }); window.gl.mrWidget = { diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index f1eeb331b9b..190d790f584 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -37,6 +37,7 @@ import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue'; import MrWidgetAutoMergeEnabled from './components/states/mr_widget_auto_merge_enabled.vue'; import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue'; import CheckingState from './components/states/mr_widget_checking.vue'; +// import ExtensionsContainer from './components/extensions/container'; import eventHub from './event_hub'; import notify from '~/lib/utils/notify'; import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue'; @@ -57,6 +58,7 @@ export default { }, components: { Loading, + // ExtensionsContainer, 'mr-widget-header': WidgetHeader, 'mr-widget-suggest-pipeline': WidgetSuggestPipeline, 'mr-widget-merge-help': WidgetMergeHelp, @@ -454,6 +456,7 @@ export default { :service="service" /> <div class="mr-section-container mr-widget-workflow"> + <!-- <extensions-container :mr="mr" /> --> <grouped-codequality-reports-app v-if="shouldRenderCodeQuality" :base-path="mr.codeclimate.base_path" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue index c2ebf78d541..973cc314ee3 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue @@ -1,11 +1,10 @@ <script> -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; -import tooltip from '~/vue_shared/directives/tooltip'; export default { directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, components: { GlIcon, @@ -45,12 +44,9 @@ export default { <template> <div - v-tooltip + v-gl-tooltip.left.viewport :title="labelsList" class="sidebar-collapsed-icon" - data-placement="left" - data-container="body" - data-boundary="viewport" @click="handleClick" > <gl-icon name="labels" /> diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index ee4f74882a1..1647bf9c7ce 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -43,12 +43,6 @@ } } -.ldap-group-links { - .form-actions { - margin-bottom: $gl-padding; - } -} - .save-group-loader { margin-top: $gl-padding-50; margin-bottom: $gl-padding-50; diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb index 719c351242c..ecedbfb2a4f 100644 --- a/app/helpers/time_helper.rb +++ b/app/helpers/time_helper.rb @@ -32,4 +32,8 @@ module TimeHelper "%02d:%02d:%02d" % [hours, minutes, seconds] end end + + def time_in_milliseconds + (Time.now.to_f * 1000).to_i + end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 563450159b5..692971f4627 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -75,11 +75,27 @@ module TreeHelper if user_access(project).can_push_to_branch?(ref) ref else - project = tree_edit_project(project) - project.repository.next_branch('patch') + patch_branch_name(ref) end end + # Generate a patch branch name that should look like: + # `username-branchname-patch-epoch` + # where `epoch` is the last 5 digits of the time since epoch (in + # milliseconds) + # + # Note: this correlates with how the WebIDE formats the branch name + # and if this implementation changes, so should the `placeholderBranchName` + # definition in app/assets/javascripts/ide/stores/modules/commit/getters.js + def patch_branch_name(ref) + return unless current_user + + username = current_user.username + epoch = time_in_milliseconds.to_s.last(5) + + "#{username}-#{ref}-patch-#{epoch}" + end + def tree_edit_project(project = @project) if can?(current_user, :push_code, project) project diff --git a/app/models/csv_issue_import.rb b/app/models/issues/csv_import.rb index 01f83c983d7..d141f126ec9 100644 --- a/app/models/csv_issue_import.rb +++ b/app/models/issues/csv_import.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true -class CsvIssueImport < ApplicationRecord +class Issues::CsvImport < ApplicationRecord + self.table_name = 'csv_issue_imports' + belongs_to :project, optional: false belongs_to :user, optional: false end diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index 1c93073025d..580a348b408 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -57,6 +57,8 @@ class BasePolicy < DeclarativePolicy::Base rule { default }.enable :read_cross_project condition(:is_gitlab_com) { ::Gitlab.dev_env_or_com? } + + rule { admin }.enable :change_repository_storage end BasePolicy.prepend_if_ee('EE::BasePolicy') diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 59e2d617bf7..da857aa2625 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -546,8 +546,6 @@ class ProjectPolicy < BasePolicy prevent :create_pipeline end - rule { admin }.enable :change_repository_storage - rule { can?(:read_issue) }.policy do enable :read_design enable :read_design_activity diff --git a/app/services/clusters/kubernetes/create_or_update_service_account_service.rb b/app/services/clusters/kubernetes/create_or_update_service_account_service.rb index 2725a3aeaa5..5791b8ffcae 100644 --- a/app/services/clusters/kubernetes/create_or_update_service_account_service.rb +++ b/app/services/clusters/kubernetes/create_or_update_service_account_service.rb @@ -69,7 +69,13 @@ module Clusters def create_role_or_cluster_role_binding if namespace_creator - kubeclient.create_or_update_role_binding(role_binding_resource) + begin + kubeclient.delete_role_binding(role_binding_name, service_account_namespace) + rescue Kubeclient::ResourceNotFoundError + # Do nothing as we will create new role binding below + end + + kubeclient.update_role_binding(role_binding_resource) else kubeclient.create_or_update_cluster_role_binding(cluster_role_binding_resource) end diff --git a/app/services/issues/import_csv_service.rb b/app/services/issues/import_csv_service.rb index e71d098950e..16af0c88279 100644 --- a/app/services/issues/import_csv_service.rb +++ b/app/services/issues/import_csv_service.rb @@ -20,7 +20,7 @@ module Issues private def record_import_attempt - CsvIssueImport.create!(user: @user, project: @project) + Issues::CsvImport.create!(user: @user, project: @project) end def process_csv diff --git a/app/services/packages/create_event_service.rb b/app/services/packages/create_event_service.rb index d009cba2812..56480724031 100644 --- a/app/services/packages/create_event_service.rb +++ b/app/services/packages/create_event_service.rb @@ -3,6 +3,8 @@ module Packages class CreateEventService < BaseService def execute + return unless Feature.enabled?(:collect_package_events, default_enabled: false) + event_scope = scope.is_a?(::Packages::Package) ? scope.package_type : scope ::Packages::Event.create!( diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index debbe95d2aa..804d2da2c4b 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -5,7 +5,7 @@ - labels_or_filters = @labels.exists? || search.present? || subscribed.present? - if labels_or_filters - #promote-label-modal + #js-promote-label-modal = render 'shared/labels/nav', labels_or_filters: labels_or_filters, can_admin_label: can_admin_label .labels-container.gl-mt-2 diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index ad485c08bac..357d4d193df 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -5,7 +5,7 @@ - labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present? - if labels_or_filters - #promote-label-modal + #js-promote-label-modal = render 'shared/labels/nav', labels_or_filters: labels_or_filters, can_admin_label: can_admin_label .labels-container.gl-mt-3 diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 1dadb4384b9..4b09e8de896 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -34,10 +34,7 @@ label_title: label.title, label_color: label.color, label_text_color: label.text_color, - group_name: label.project.group.name, - target: '#promote-label-modal', - container: 'body', - toggle: 'modal' } } + group_name: label.project.group.name } } = _('Promote to group label') - if can?(current_user, :admin_label, label) %li diff --git a/changelogs/unreleased/205578-feature-flag-pkg-events.yml b/changelogs/unreleased/205578-feature-flag-pkg-events.yml new file mode 100644 index 00000000000..ce42a88d2b5 --- /dev/null +++ b/changelogs/unreleased/205578-feature-flag-pkg-events.yml @@ -0,0 +1,5 @@ +--- +title: Adds feature flag to disable package events +merge_request: 45802 +author: +type: changed diff --git a/changelogs/unreleased/229703-aqualls-promote-label-modal.yml b/changelogs/unreleased/229703-aqualls-promote-label-modal.yml new file mode 100644 index 00000000000..8d3557fa553 --- /dev/null +++ b/changelogs/unreleased/229703-aqualls-promote-label-modal.yml @@ -0,0 +1,5 @@ +--- +title: Migrate DeprecatedModal to GitLab UI Modal for promoted labels +merge_request: 46047 +author: +type: changed diff --git a/changelogs/unreleased/tancnle-fix-jupyter-notebook-overflow.yml b/changelogs/unreleased/tancnle-fix-jupyter-notebook-overflow.yml new file mode 100644 index 00000000000..bdb2f45a9a5 --- /dev/null +++ b/changelogs/unreleased/tancnle-fix-jupyter-notebook-overflow.yml @@ -0,0 +1,5 @@ +--- +title: Fix wide content overflow on Notebook output +merge_request: 45971 +author: +type: fixed diff --git a/changelogs/unreleased/vij-fix-inconsistent-branch-names.yml b/changelogs/unreleased/vij-fix-inconsistent-branch-names.yml new file mode 100644 index 00000000000..b15a11d8c77 --- /dev/null +++ b/changelogs/unreleased/vij-fix-inconsistent-branch-names.yml @@ -0,0 +1,5 @@ +--- +title: Fix single file editor patch branch name +merge_request: 46044 +author: +type: fixed diff --git a/config/feature_flags/development/collect_package_events.yml b/config/feature_flags/development/collect_package_events.yml new file mode 100644 index 00000000000..65b88f84d86 --- /dev/null +++ b/config/feature_flags/development/collect_package_events.yml @@ -0,0 +1,7 @@ +--- +name: collect_package_events +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45802 +rollout_issue_url: +group: group::package +type: development +default_enabled: false diff --git a/config/feature_flags/development/saml_group_links.yml b/config/feature_flags/development/saml_group_links.yml new file mode 100644 index 00000000000..84c3c73882f --- /dev/null +++ b/config/feature_flags/development/saml_group_links.yml @@ -0,0 +1,7 @@ +--- +name: saml_group_links +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45080 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267020 +type: development +group: group::access +default_enabled: false diff --git a/doc/development/new_fe_guide/modules/index.md b/doc/development/new_fe_guide/modules/index.md index a7820442df0..1752be6edcd 100644 --- a/doc/development/new_fe_guide/modules/index.md +++ b/doc/development/new_fe_guide/modules/index.md @@ -3,3 +3,7 @@ - [DirtySubmit](dirty_submit.md) Disable form submits until there are unsaved changes. + +- [Merge Request widget extensions](widget_extensions.md) + + Easily add extensions into the merge request widget diff --git a/doc/development/new_fe_guide/modules/widget_extensions.md b/doc/development/new_fe_guide/modules/widget_extensions.md new file mode 100644 index 00000000000..70d15743207 --- /dev/null +++ b/doc/development/new_fe_guide/modules/widget_extensions.md @@ -0,0 +1,50 @@ +# Merge request widget extensions + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44616) in GitLab 13.6. + +## Summary + +Extensions in the merge request widget allow for others team to quickly and easily add new features +into the widget that will match the existing design and interaction as other extensions. + +## Usage + +To use extensions you need to first create a new extension object that will be used to fetch the +data that will be rendered in the extension. See the example file in +app/assets/javascripts/vue_merge_request_widget/extensions/issues.js for a working example. + +The basic object structure is as below: + +```javascript +export default { + name: '', + props: [], + computed: { + summary() {}, + statusIcon() {}, + }, + methods: { + fetchCollapsedData() {}, + fetchFullData() {}, + }, +}; +``` + +Following the same data structure allows each extension to follow the same registering structure +but allows for each extension to manage where it gets its own data from. + +After creating this structure you need to register it. Registering the extension can happen at any +point _after_ the widget has been created. + +To register a extension the following can be done: + +```javascript +// Import the register method +import { registerExtension } from '~/vue_merge_request_widget/components/extensions'; + +// Import the new extension +import issueExtension from '~/vue_merge_request_widget/extensions/issues'; + +// Register the imported extension +registerExtension(issueExtension); +``` diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb index 13cd6dcad3f..8ec61d62279 100644 --- a/lib/gitlab/kubernetes/kube_client.rb +++ b/lib/gitlab/kubernetes/kube_client.rb @@ -61,18 +61,11 @@ module Gitlab # RBAC methods delegates to the apis/rbac.authorization.k8s.io api # group client delegate :update_cluster_role_binding, - to: :rbac_client - - # RBAC methods delegates to the apis/rbac.authorization.k8s.io api - # group client - delegate :create_role, - :get_role, - :update_role, - to: :rbac_client - - # RBAC methods delegates to the apis/rbac.authorization.k8s.io api - # group client - delegate :update_role_binding, + :create_role, + :get_role, + :update_role, + :delete_role_binding, + :update_role_binding, to: :rbac_client # non-entity methods that can only work with the core client @@ -186,6 +179,7 @@ module Gitlab update_cluster_role_binding(resource) end + # Note that we cannot update roleRef as that is immutable def create_or_update_role_binding(resource) update_role_binding(resource) end diff --git a/lib/gitlab/legacy_github_import/importer.rb b/lib/gitlab/legacy_github_import/importer.rb index 3f9fd1b1a19..e60651300eb 100644 --- a/lib/gitlab/legacy_github_import/importer.rb +++ b/lib/gitlab/legacy_github_import/importer.rb @@ -36,7 +36,7 @@ module Gitlab } end - @client = Client.new(credentials[:user], opts) + @client = Client.new(credentials[:user], **opts) end def execute diff --git a/lib/gitlab/robots_txt/parser.rb b/lib/gitlab/robots_txt/parser.rb index b9a3837e468..604d2f9b35b 100644 --- a/lib/gitlab/robots_txt/parser.rb +++ b/lib/gitlab/robots_txt/parser.rb @@ -3,34 +3,68 @@ module Gitlab module RobotsTxt class Parser - attr_reader :disallow_rules + DISALLOW_REGEX = /^disallow: /i.freeze + ALLOW_REGEX = /^allow: /i.freeze + + attr_reader :disallow_rules, :allow_rules def initialize(content) @raw_content = content - @disallow_rules = parse_raw_content! + @disallow_rules, @allow_rules = parse_raw_content! end def disallowed?(path) + return false if allow_rules.any? { |rule| path =~ rule } + disallow_rules.any? { |rule| path =~ rule } end private - # This parser is very basic as it only knows about `Disallow:` lines, - # and simply ignores all other lines. + # This parser is very basic as it only knows about `Disallow:` + # and `Allow:` lines, and simply ignores all other lines. # - # Order of predecence, 'Allow:`, etc are ignored for now. + # Patterns ending in `$`, and `*` for 0 or more characters are recognized. + # + # It is case insensitive and `Allow` rules takes precedence + # over `Disallow`. def parse_raw_content! - @raw_content.each_line.map do |line| - if line.start_with?('Disallow:') - value = line.sub('Disallow:', '').strip - value = Regexp.escape(value).gsub('\*', '.*') - Regexp.new("^#{value}") - else - nil + disallowed = [] + allowed = [] + + @raw_content.each_line.each do |line| + if disallow_rule?(line) + disallowed << get_disallow_pattern(line) + elsif allow_rule?(line) + allowed << get_allow_pattern(line) end - end.compact + end + + [disallowed, allowed] + end + + def disallow_rule?(line) + line =~ DISALLOW_REGEX + end + + def get_disallow_pattern(line) + get_pattern(line, DISALLOW_REGEX) + end + + def allow_rule?(line) + line =~ ALLOW_REGEX + end + + def get_allow_pattern(line) + get_pattern(line, ALLOW_REGEX) + end + + def get_pattern(line, rule_regex) + value = line.sub(rule_regex, '').strip + value = Regexp.escape(value).gsub('\*', '.*') + value = value.sub(/\\\$$/, '$') + Regexp.new("^#{value}") end end end diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb index 4df6a50c8dd..259d3e300b6 100644 --- a/lib/gitlab/setup_helper.rb +++ b/lib/gitlab/setup_helper.rb @@ -99,6 +99,7 @@ module Gitlab config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path } config[:bin_dir] = Gitlab.config.gitaly.client_path config[:gitlab] = { url: Gitlab.config.gitlab.url } + config[:logging] = { dir: Rails.root.join('log').to_s } TomlRB.dump(config) end diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb index 9213b5ebab2..d3475a67711 100644 --- a/lib/gitlab/url_blocker.rb +++ b/lib/gitlab/url_blocker.rb @@ -65,8 +65,8 @@ module Gitlab protected_uri_with_hostname end - def blocked_url?(*args) - validate!(*args) + def blocked_url?(url, **kwargs) + validate!(url, **kwargs) false rescue BlockedUrlError diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 030d50815a6..75a661d4c81 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -602,7 +602,7 @@ module Gitlab jira: distinct_count(::JiraImportState.where(time_period), :user_id), fogbugz: projects_imported_count('fogbugz', time_period), phabricator: projects_imported_count('phabricator', time_period), - csv: distinct_count(CsvIssueImport.where(time_period), :user_id) + csv: distinct_count(Issues::CsvImport.where(time_period), :user_id) }, groups_imported: distinct_count(::GroupImportState.where(time_period), :user_id) } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 26455f73a33..a9027e56ff8 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5477,7 +5477,7 @@ msgstr "" msgid "Closed this %{quick_action_target}." msgstr "" -msgid "Closed: %{closedIssuesCount}" +msgid "Closed: %{closed}" msgstr "" msgid "Closes this %{quick_action_target}." @@ -12912,6 +12912,12 @@ msgstr "" msgid "GroupRoadmap|To widen your search, change or remove filters; from %{startDate} to %{endDate}." msgstr "" +msgid "GroupSAML|Active SAML Group Links (%{count})" +msgstr "" + +msgid "GroupSAML|Are you sure you want to remove the SAML group link?" +msgstr "" + msgid "GroupSAML|Certificate fingerprint" msgstr "" @@ -12921,6 +12927,9 @@ msgstr "" msgid "GroupSAML|Copy SAML Response XML" msgstr "" +msgid "GroupSAML|Could not create SAML group link: %{errors}." +msgstr "" + msgid "GroupSAML|Default membership role" msgstr "" @@ -12966,12 +12975,30 @@ msgstr "" msgid "GroupSAML|NameID Format" msgstr "" +msgid "GroupSAML|New SAML group link saved." +msgstr "" + +msgid "GroupSAML|No active SAML group links" +msgstr "" + msgid "GroupSAML|Prohibit outer forks" msgstr "" msgid "GroupSAML|Prohibit outer forks for this group." msgstr "" +msgid "GroupSAML|Role to assign members of this SAML group." +msgstr "" + +msgid "GroupSAML|SAML Group Links" +msgstr "" + +msgid "GroupSAML|SAML Group Name" +msgstr "" + +msgid "GroupSAML|SAML Group Name: %{saml_group_name}" +msgstr "" + msgid "GroupSAML|SAML Response Output" msgstr "" @@ -12984,6 +13011,9 @@ msgstr "" msgid "GroupSAML|SAML Single Sign On Settings" msgstr "" +msgid "GroupSAML|SAML group link was successfully removed." +msgstr "" + msgid "GroupSAML|SCIM API endpoint URL" msgstr "" @@ -12996,6 +13026,9 @@ msgstr "" msgid "GroupSAML|The SCIM token is now hidden. To see the value of the token again, you need to " msgstr "" +msgid "GroupSAML|The case-sensitive group name that will be sent by the SAML identity provider." +msgstr "" + msgid "GroupSAML|This will be set as the access level of users added to the group." msgstr "" @@ -13020,6 +13053,9 @@ msgstr "" msgid "GroupSAML|Your SCIM token" msgstr "" +msgid "GroupSAML|as %{access_level}" +msgstr "" + msgid "GroupSAML|must match stored NameID of \"%{extern_uid}\" as we use this to identify users. If the NameID changes users will be unable to sign in." msgstr "" @@ -16601,6 +16637,9 @@ msgstr "" msgid "Merged this merge request." msgstr "" +msgid "Merged: %{merged}" +msgstr "" + msgid "Merges this merge request immediately." msgstr "" @@ -18596,10 +18635,7 @@ msgstr "" msgid "Open sidebar" msgstr "" -msgid "Open: %{openIssuesCount}" -msgstr "" - -msgid "Open: %{open} • Closed: %{closed}" +msgid "Open: %{open}" msgstr "" msgid "Opened" diff --git a/public/robots.txt b/public/robots.txt index f3fe51a25b0..c89d26b0e40 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -21,6 +21,7 @@ Disallow: /dashboard Disallow: /users Disallow: /help Disallow: /s/ +Disallow: /-/profile # Only specifically allow the Sign In page to avoid very ugly search results Allow: /users/sign_in diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index fefb80a44cc..d4f66220f4d 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -6,14 +6,10 @@ RSpec.describe Projects::MergeRequestsController do include ProjectForksHelper include Gitlab::Routing - let(:project) { create(:project, :repository) } - let(:user) { project.owner } + let_it_be_with_refind(:project) { create(:project, :repository) } + let_it_be_with_reload(:project_public_with_private_builds) { create(:project, :repository, :public, :builds_private) } + let(:user) { project.owner } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } - let(:merge_request_with_conflicts) do - create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project, merge_status: :unchecked) do |mr| - mr.mark_as_unmergeable - end - end before do sign_in(user) @@ -107,7 +103,7 @@ RSpec.describe Projects::MergeRequestsController do render_views it 'renders merge request page' do - merge_request.merge_request_diff.destroy + merge_request.merge_request_diff.destroy! go(format: :html) @@ -147,7 +143,7 @@ RSpec.describe Projects::MergeRequestsController do let(:new_project) { create(:project) } before do - project.route.destroy + project.route.destroy! new_project.redirect_routes.create!(path: project.full_path) new_project.add_developer(user) end @@ -359,12 +355,11 @@ RSpec.describe Projects::MergeRequestsController do end context 'there is no source project' do - let(:project) { create(:project, :repository) } let(:forked_project) { fork_project_with_submodules(project) } let!(:merge_request) { create(:merge_request, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } before do - forked_project.destroy + forked_project.destroy! end it 'closes MR without errors' do @@ -435,7 +430,7 @@ RSpec.describe Projects::MergeRequestsController do context 'when the merge request is not mergeable' do before do - merge_request.update(title: "WIP: #{merge_request.title}") + merge_request.update!(title: "WIP: #{merge_request.title}") post :merge, params: base_params end @@ -475,7 +470,7 @@ RSpec.describe Projects::MergeRequestsController do context 'when squash is passed as 1' do it 'updates the squash attribute on the MR to true' do - merge_request.update(squash: false) + merge_request.update!(squash: false) merge_with_sha(squash: '1') expect(merge_request.reload.squash_on_merge?).to be_truthy @@ -484,7 +479,7 @@ RSpec.describe Projects::MergeRequestsController do context 'when squash is passed as 0' do it 'updates the squash attribute on the MR to false' do - merge_request.update(squash: true) + merge_request.update!(squash: true) merge_with_sha(squash: '0') expect(merge_request.reload.squash_on_merge?).to be_falsey @@ -547,7 +542,7 @@ RSpec.describe Projects::MergeRequestsController do context 'and head pipeline is not the current one' do before do - head_pipeline.update(sha: 'not_current_sha') + head_pipeline.update!(sha: 'not_current_sha') end it 'returns :failed' do @@ -667,9 +662,9 @@ RSpec.describe Projects::MergeRequestsController do end context "when the user is owner" do - let(:owner) { create(:user) } - let(:namespace) { create(:namespace, owner: owner) } - let(:project) { create(:project, :repository, namespace: namespace) } + let_it_be(:owner) { create(:user) } + let_it_be(:namespace) { create(:namespace, owner: owner) } + let_it_be(:project) { create(:project, :repository, namespace: namespace) } before do sign_in owner @@ -765,7 +760,7 @@ RSpec.describe Projects::MergeRequestsController do end context 'with private builds on a public project' do - let(:project) { create(:project, :repository, :public, :builds_private) } + let(:project) { project_public_with_private_builds } context 'for a project owner' do it 'responds with serialized pipelines' do @@ -813,7 +808,7 @@ RSpec.describe Projects::MergeRequestsController do context 'with public builds' do let(:forked_project) do fork_project(project, fork_user, repository: true).tap do |new_project| - new_project.project_feature.update(builds_access_level: ProjectFeature::ENABLED) + new_project.project_feature.update!(builds_access_level: ProjectFeature::ENABLED) end end @@ -855,7 +850,7 @@ RSpec.describe Projects::MergeRequestsController do end describe 'GET exposed_artifacts' do - let(:merge_request) do + let_it_be(:merge_request) do create(:merge_request, :with_merge_request_pipeline, target_project: project, @@ -993,7 +988,7 @@ RSpec.describe Projects::MergeRequestsController do end describe 'GET coverage_reports' do - let(:merge_request) do + let_it_be(:merge_request) do create(:merge_request, :with_merge_request_pipeline, target_project: project, @@ -1123,7 +1118,7 @@ RSpec.describe Projects::MergeRequestsController do end describe 'GET terraform_reports' do - let(:merge_request) do + let_it_be(:merge_request) do create(:merge_request, :with_merge_request_pipeline, target_project: project, @@ -1271,7 +1266,7 @@ RSpec.describe Projects::MergeRequestsController do end describe 'GET test_reports' do - let(:merge_request) do + let_it_be(:merge_request) do create(:merge_request, :with_diffs, :with_merge_request_pipeline, @@ -1382,7 +1377,7 @@ RSpec.describe Projects::MergeRequestsController do end describe 'GET accessibility_reports' do - let(:merge_request) do + let_it_be(:merge_request) do create(:merge_request, :with_diffs, :with_merge_request_pipeline, @@ -1419,7 +1414,7 @@ RSpec.describe Projects::MergeRequestsController do end context 'permissions on a public project with private CI/CD' do - let(:project) { create(:project, :repository, :public, :builds_private) } + let(:project) { project_public_with_private_builds } let(:accessibility_comparison) { { status: :parsed, data: { summary: 1 } } } context 'while signed out' do @@ -1505,7 +1500,7 @@ RSpec.describe Projects::MergeRequestsController do describe 'POST remove_wip' do before do merge_request.title = merge_request.wip_title - merge_request.save + merge_request.save! post :remove_wip, params: { @@ -1626,7 +1621,7 @@ RSpec.describe Projects::MergeRequestsController do it 'links to the environment on that project', :sidekiq_might_not_need_inline do get_ci_environments_status - expect(json_response.first['url']).to match /#{forked.full_path}/ + expect(json_response.first['url']).to match(/#{forked.full_path}/) end context "when environment_target is 'merge_commit'", :sidekiq_might_not_need_inline do @@ -1653,7 +1648,7 @@ RSpec.describe Projects::MergeRequestsController do get_ci_environments_status(environment_target: 'merge_commit') expect(response).to have_gitlab_http_status(:ok) - expect(json_response.first['url']).to match /#{project.full_path}/ + expect(json_response.first['url']).to match(/#{project.full_path}/) end end end @@ -1773,7 +1768,7 @@ RSpec.describe Projects::MergeRequestsController do context 'with project member visibility on a public project' do let(:user) { create(:user) } - let(:project) { create(:project, :repository, :public, :builds_private) } + let(:project) { project_public_with_private_builds } it 'returns pipeline data to project members' do project.add_developer(user) diff --git a/spec/factories/csv_issue_import.rb b/spec/factories/issues/csv_import.rb index 7e6497248cf..94688cf6232 100644 --- a/spec/factories/csv_issue_import.rb +++ b/spec/factories/issues/csv_import.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true FactoryBot.define do - factory :csv_issue_import do + factory :issue_csv_import, class: 'Issues::CsvImport' do project user end diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index c30c8dda852..3f1c10b3688 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -179,12 +179,14 @@ RSpec.describe 'Editing file blob', :js do end context 'with protected branch' do - before do - visit project_edit_blob_path(project, tree_join(protected_branch, file_path)) - end - it 'shows blob editor with patch branch' do - expect(find('.js-branch-name').value).to eq('patch-1') + freeze_time do + visit project_edit_blob_path(project, tree_join(protected_branch, file_path)) + + epoch = Time.now.strftime('%s%L').last(5) + + expect(find('.js-branch-name').value).to eq "#{user.username}-protected-branch-patch-#{epoch}" + end end end end diff --git a/spec/frontend/pages/labels/components/promote_label_modal_spec.js b/spec/frontend/pages/labels/components/promote_label_modal_spec.js index 1fa12cf1365..f969808d78b 100644 --- a/spec/frontend/pages/labels/components/promote_label_modal_spec.js +++ b/spec/frontend/pages/labels/components/promote_label_modal_spec.js @@ -32,10 +32,9 @@ describe('Promote label modal', () => { }); it('contains a label span with the color', () => { - const labelFromTitle = vm.$el.querySelector('.modal-header .label.color-label'); - - expect(labelFromTitle.style.backgroundColor).not.toBe(null); - expect(labelFromTitle.textContent).toContain(vm.labelTitle); + expect(vm.labelColor).not.toBe(null); + expect(vm.labelColor).toBe(labelMockData.labelColor); + expect(vm.labelTitle).toBe(labelMockData.labelTitle); }); }); diff --git a/spec/frontend/releases/components/__snapshots__/issuable_stats_spec.js.snap b/spec/frontend/releases/components/__snapshots__/issuable_stats_spec.js.snap new file mode 100644 index 00000000000..8cffa9c8d36 --- /dev/null +++ b/spec/frontend/releases/components/__snapshots__/issuable_stats_spec.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`~/releases/components/issuable_stats.vue matches snapshot 1`] = ` +"<div class=\\"gl-display-flex gl-flex-direction-column gl-flex-shrink-0 gl-mr-6 gl-mb-5 js-issues-container\\"><span class=\\"gl-mb-2\\"> + Items + <span class=\\"badge badge-muted badge-pill gl-badge sm\\"><!----> 10</span></span> + <div class=\\"gl-display-flex\\"><span data-testid=\\"open-stat\\" class=\\"gl-white-space-pre-wrap\\">Open: <a href=\\"path/to/open/items\\" class=\\"gl-link\\">1</a></span> <span class=\\"gl-mx-2\\">•</span> <span data-testid=\\"merged-stat\\" class=\\"gl-white-space-pre-wrap\\">Merged: <a href=\\"path/to/merged/items\\" class=\\"gl-link\\">7</a></span> <span class=\\"gl-mx-2\\">•</span> <span data-testid=\\"closed-stat\\" class=\\"gl-white-space-pre-wrap\\">Closed: <a href=\\"path/to/closed/items\\" class=\\"gl-link\\">2</a></span></div> +</div>" +`; diff --git a/spec/frontend/releases/components/issuable_stats_spec.js b/spec/frontend/releases/components/issuable_stats_spec.js new file mode 100644 index 00000000000..224ad1499af --- /dev/null +++ b/spec/frontend/releases/components/issuable_stats_spec.js @@ -0,0 +1,114 @@ +import { GlLink } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { trimText } from 'helpers/text_helper'; +import IssuableStats from '~/releases/components/issuable_stats.vue'; + +describe('~/releases/components/issuable_stats.vue', () => { + let wrapper; + let defaultProps; + + const createComponent = propUpdates => { + wrapper = mount(IssuableStats, { + propsData: { + ...defaultProps, + ...propUpdates, + }, + }); + }; + + const findOpenStatLink = () => wrapper.find('[data-testid="open-stat"]').find(GlLink); + const findMergedStatLink = () => wrapper.find('[data-testid="merged-stat"]').find(GlLink); + const findClosedStatLink = () => wrapper.find('[data-testid="closed-stat"]').find(GlLink); + + beforeEach(() => { + defaultProps = { + label: 'Items', + total: 10, + closed: 2, + merged: 7, + openPath: 'path/to/open/items', + closedPath: 'path/to/closed/items', + mergedPath: 'path/to/merged/items', + }; + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('matches snapshot', () => { + createComponent(); + + expect(wrapper.html()).toMatchSnapshot(); + }); + + describe('when only total and closed counts are provided', () => { + beforeEach(() => { + createComponent({ merged: undefined, mergedPath: undefined }); + }); + + it('renders a label with the total count; also, the opened count and the closed count', () => { + expect(trimText(wrapper.text())).toMatchInterpolatedText('Items 10 Open: 8 • Closed: 2'); + }); + }); + + describe('when only total, merged, and closed counts are provided', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders a label with the total count; also, the opened count, the merged count, and the closed count', () => { + expect(wrapper.text()).toMatchInterpolatedText('Items 10 Open: 1 • Merged: 7 • Closed: 2'); + }); + }); + + describe('when path parameters are provided', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders the "open" stat as a link', () => { + const link = findOpenStatLink(); + + expect(link.exists()).toBe(true); + expect(link.attributes('href')).toBe(defaultProps.openPath); + }); + + it('renders the "merged" stat as a link', () => { + const link = findMergedStatLink(); + + expect(link.exists()).toBe(true); + expect(link.attributes('href')).toBe(defaultProps.mergedPath); + }); + + it('renders the "closed" stat as a link', () => { + const link = findClosedStatLink(); + + expect(link.exists()).toBe(true); + expect(link.attributes('href')).toBe(defaultProps.closedPath); + }); + }); + + describe('when path parameters are not provided', () => { + beforeEach(() => { + createComponent({ + openPath: undefined, + closedPath: undefined, + mergedPath: undefined, + }); + }); + + it('does not render the "open" stat as a link', () => { + expect(findOpenStatLink().exists()).toBe(false); + }); + + it('does not render the "merged" stat as a link', () => { + expect(findMergedStatLink().exists()).toBe(false); + }); + + it('does not render the "closed" stat as a link', () => { + expect(findClosedStatLink().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/releases/components/release_block_milestone_info_spec.js b/spec/frontend/releases/components/release_block_milestone_info_spec.js index 45f4eaa01a9..a17a8b9059c 100644 --- a/spec/frontend/releases/components/release_block_milestone_info_spec.js +++ b/spec/frontend/releases/components/release_block_milestone_info_spec.js @@ -187,67 +187,4 @@ describe('Release block milestone info', () => { expectAllZeros(); }); - - describe('Issue links', () => { - const findOpenIssuesLink = () => wrapper.find({ ref: 'openIssuesLink' }); - const findOpenIssuesText = () => wrapper.find({ ref: 'openIssuesText' }); - const findClosedIssuesLink = () => wrapper.find({ ref: 'closedIssuesLink' }); - const findClosedIssuesText = () => wrapper.find({ ref: 'closedIssuesText' }); - - describe('when openIssuePath is provided', () => { - const openIssuesPath = '/path/to/open/issues'; - - beforeEach(() => { - return factory({ milestones, openIssuesPath }); - }); - - it('renders the open issues as a link', () => { - expect(findOpenIssuesLink().exists()).toBe(true); - expect(findOpenIssuesText().exists()).toBe(false); - }); - - it('renders the open issues link with the correct href', () => { - expect(findOpenIssuesLink().attributes().href).toBe(openIssuesPath); - }); - }); - - describe('when openIssuePath is not provided', () => { - beforeEach(() => { - return factory({ milestones }); - }); - - it('renders the open issues as plain text', () => { - expect(findOpenIssuesLink().exists()).toBe(false); - expect(findOpenIssuesText().exists()).toBe(true); - }); - }); - - describe('when closedIssuePath is provided', () => { - const closedIssuesPath = '/path/to/closed/issues'; - - beforeEach(() => { - return factory({ milestones, closedIssuesPath }); - }); - - it('renders the closed issues as a link', () => { - expect(findClosedIssuesLink().exists()).toBe(true); - expect(findClosedIssuesText().exists()).toBe(false); - }); - - it('renders the closed issues link with the correct href', () => { - expect(findClosedIssuesLink().attributes().href).toBe(closedIssuesPath); - }); - }); - - describe('when closedIssuePath is not provided', () => { - beforeEach(() => { - return factory({ milestones }); - }); - - it('renders the closed issues as plain text', () => { - expect(findClosedIssuesLink().exists()).toBe(false); - expect(findClosedIssuesText().exists()).toBe(true); - }); - }); - }); }); diff --git a/spec/frontend/vue_mr_widget/components/extensions/index_spec.js b/spec/frontend/vue_mr_widget/components/extensions/index_spec.js new file mode 100644 index 00000000000..8f6fe3cd37a --- /dev/null +++ b/spec/frontend/vue_mr_widget/components/extensions/index_spec.js @@ -0,0 +1,31 @@ +import { registerExtension, extensions } from '~/vue_merge_request_widget/components/extensions'; +import ExtensionBase from '~/vue_merge_request_widget/components/extensions/base.vue'; + +describe('MR widget extension registering', () => { + it('registers a extension', () => { + registerExtension({ + name: 'Test', + props: ['helloWorld'], + computed: { + test() {}, + }, + methods: { + test() {}, + }, + }); + + expect(extensions[0]).toEqual( + expect.objectContaining({ + extends: ExtensionBase, + name: 'Test', + props: ['helloWorld'], + computed: { + test: expect.any(Function), + }, + methods: { + test: expect.any(Function), + }, + }), + ); + }); +}); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js index 7847e0ee71d..71c040c6633 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js @@ -81,9 +81,7 @@ describe('DropdownValueCollapsedComponent', () => { describe('template', () => { it('renders component container element with tooltip`', () => { - expect(vm.$el.dataset.placement).toBe('left'); - expect(vm.$el.dataset.container).toBe('body'); - expect(vm.$el.dataset.originalTitle).toBe(vm.labelsList); + expect(vm.$el.title).toBe(vm.labelsList); }); it('renders tags icon element', () => { diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb index 6663a5c81c8..3e406f5e74e 100644 --- a/spec/helpers/time_helper_spec.rb +++ b/spec/helpers/time_helper_spec.rb @@ -37,4 +37,14 @@ RSpec.describe TimeHelper do it { expect(duration_in_numbers(duration)).to eq formatted_string } end end + + describe "#time_in_milliseconds" do + it "returns the time in milliseconds" do + freeze_time do + time = (Time.now.to_f * 1000).to_i + + expect(time_in_milliseconds).to eq time + end + end + end end diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb index b5d356b985c..620bf248d7b 100644 --- a/spec/helpers/tree_helper_spec.rb +++ b/spec/helpers/tree_helper_spec.rb @@ -7,6 +7,8 @@ RSpec.describe TreeHelper do let(:repository) { project.repository } let(:sha) { 'c1c67abbaf91f624347bb3ae96eabe3a1b742478' } + let_it_be(:user) { create(:user) } + def create_file(filename) project.repository.create_file( project.creator, @@ -219,7 +221,6 @@ RSpec.describe TreeHelper do context 'user does not have write access but a personal fork exists' do include ProjectForksHelper - let_it_be(:user) { create(:user) } let(:forked_project) { create(:project, :repository, namespace: user.namespace) } before do @@ -277,8 +278,6 @@ RSpec.describe TreeHelper do end context 'user has write access' do - let_it_be(:user) { create(:user) } - before do project.add_developer(user) @@ -314,8 +313,6 @@ RSpec.describe TreeHelper do end context 'gitpod feature is enabled' do - let_it_be(:user) { create(:user) } - before do stub_feature_flags(gitpod: true) allow(Gitlab::CurrentSettings) @@ -358,4 +355,28 @@ RSpec.describe TreeHelper do end end end + + describe '.patch_branch_name' do + before do + allow(helper).to receive(:current_user).and_return(user) + end + + subject { helper.patch_branch_name('master') } + + it 'returns a patch branch name' do + freeze_time do + epoch = Time.now.strftime('%s%L').last(5) + + expect(subject).to eq "#{user.username}-master-patch-#{epoch}" + end + end + + context 'without a current_user' do + let(:user) { nil } + + it 'returns nil' do + expect(subject).to be nil + end + end + end end diff --git a/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb index add554992f1..188c56ae81f 100644 --- a/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectImporter do } end - let(:lfs_download_object) { LfsDownloadObject.new(lfs_attributes) } + let(:lfs_download_object) { LfsDownloadObject.new(**lfs_attributes) } let(:github_lfs_object) { Gitlab::GithubImport::Representation::LfsObject.new(lfs_attributes) } let(:importer) { described_class.new(github_lfs_object, project, nil) } diff --git a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb index 1f7b14661c2..6188ba8ec3f 100644 --- a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do } end - let(:lfs_download_object) { LfsDownloadObject.new(lfs_attributes) } + let(:lfs_download_object) { LfsDownloadObject.new(**lfs_attributes) } describe '#parallel?' do it 'returns true when running in parallel mode' do diff --git a/spec/lib/gitlab/jira_import_spec.rb b/spec/lib/gitlab/jira_import_spec.rb index c8cecb576da..2b602c80640 100644 --- a/spec/lib/gitlab/jira_import_spec.rb +++ b/spec/lib/gitlab/jira_import_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Gitlab::JiraImport do let_it_be(:project, reload: true) { create(:project) } let(:additional_params) { {} } - subject { described_class.validate_project_settings!(project, additional_params) } + subject { described_class.validate_project_settings!(project, **additional_params) } shared_examples 'raise Jira import error' do |message| it 'returns error' do diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb index 7b6d143dda9..7d11cf35e76 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -302,6 +302,8 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do :create_role, :get_role, :update_role, + :delete_role_binding, + :update_role_binding, :update_cluster_role_binding ].each do |method| describe "##{method}" do diff --git a/spec/lib/gitlab/legacy_github_import/importer_spec.rb b/spec/lib/gitlab/legacy_github_import/importer_spec.rb index 56d708a1e11..7baf826e71d 100644 --- a/spec/lib/gitlab/legacy_github_import/importer_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/importer_spec.rb @@ -274,7 +274,7 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do allow(project).to receive(:import_data).and_return(double(credentials: credentials)) expect(Gitlab::LegacyGithubImport::Client).to receive(:new).with( credentials[:user], - {} + **{} ) subject.client diff --git a/spec/lib/gitlab/robots_txt/parser_spec.rb b/spec/lib/gitlab/robots_txt/parser_spec.rb index bb88003ce20..f4e97e5e897 100644 --- a/spec/lib/gitlab/robots_txt/parser_spec.rb +++ b/spec/lib/gitlab/robots_txt/parser_spec.rb @@ -14,8 +14,13 @@ RSpec.describe Gitlab::RobotsTxt::Parser do <<~TXT User-Agent: * Disallow: /autocomplete/users - Disallow: /search + disallow: /search Disallow: /api + Allow: /users + Disallow: /help + allow: /help + Disallow: /test$ + Disallow: /ex$mple$ TXT end @@ -28,6 +33,12 @@ RSpec.describe Gitlab::RobotsTxt::Parser do '/api/grapql' | true '/api/index.html' | true '/projects' | false + '/users' | false + '/help' | false + '/test' | true + '/testfoo' | false + '/ex$mple' | true + '/ex$mplefoo' | false end with_them do @@ -47,6 +58,7 @@ RSpec.describe Gitlab::RobotsTxt::Parser do Disallow: /*/*.git Disallow: /*/archive/ Disallow: /*/repository/archive* + Allow: /*/repository/archive/foo TXT end @@ -61,6 +73,7 @@ RSpec.describe Gitlab::RobotsTxt::Parser do '/projects' | false '/git' | false '/projects/git' | false + '/project/repository/archive/foo' | false end with_them do diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index ada1feb2632..135d9757113 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -212,7 +212,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do jira_project = create(:project, creator_id: user.id) create(:jira_import_state, :finished, project: jira_project) - create(:csv_issue_import, user: user) + create(:issue_csv_import, user: user) end expect(described_class.usage_activity_by_stage_manage({})).to include( diff --git a/spec/models/csv_issue_import_spec.rb b/spec/models/issues/csv_import_spec.rb index 8109ad34d4a..2911a79e505 100644 --- a/spec/models/csv_issue_import_spec.rb +++ b/spec/models/issues/csv_import_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe CsvIssueImport, type: :model do +RSpec.describe Issues::CsvImport, type: :model do describe 'associations' do it { is_expected.to belong_to(:project).required } it { is_expected.to belong_to(:user).required } diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb index 103f2e9bc39..226660dc955 100644 --- a/spec/policies/base_policy_spec.rb +++ b/spec/policies/base_policy_spec.rb @@ -22,6 +22,34 @@ RSpec.describe BasePolicy do end end + shared_examples 'admin only access' do |policy| + let(:current_user) { build_stubbed(:user) } + + subject { described_class.new(current_user, nil) } + + it { is_expected.not_to be_allowed(policy) } + + context 'for admins' do + let(:current_user) { build_stubbed(:admin) } + + it 'allowed when in admin mode' do + enable_admin_mode!(current_user) + + is_expected.to be_allowed(policy) + end + + it 'prevented when not in admin mode' do + is_expected.not_to be_allowed(policy) + end + end + + context 'for anonymous' do + let(:current_user) { nil } + + it { is_expected.not_to be_allowed(policy) } + end + end + describe 'read cross project' do let(:current_user) { build_stubbed(:user) } let(:user) { build_stubbed(:user) } @@ -41,51 +69,15 @@ RSpec.describe BasePolicy do enable_external_authorization_service_check end - it { is_expected.not_to be_allowed(:read_cross_project) } - - context 'for admins' do - let(:current_user) { build_stubbed(:admin) } - - subject { described_class.new(current_user, nil) } - - it 'allowed when in admin mode' do - enable_admin_mode!(current_user) - - is_expected.to be_allowed(:read_cross_project) - end - - it 'prevented when not in admin mode' do - is_expected.not_to be_allowed(:read_cross_project) - end - end - - context 'for anonymous' do - let(:current_user) { nil } - - it { is_expected.not_to be_allowed(:read_cross_project) } - end + it_behaves_like 'admin only access', :read_cross_project end end describe 'full private access' do - let(:current_user) { build_stubbed(:user) } - - subject { described_class.new(current_user, nil) } - - it { is_expected.not_to be_allowed(:read_all_resources) } - - context 'for admins' do - let(:current_user) { build_stubbed(:admin) } - - it 'allowed when in admin mode' do - enable_admin_mode!(current_user) - - is_expected.to be_allowed(:read_all_resources) - end + it_behaves_like 'admin only access', :read_all_resources + end - it 'prevented when not in admin mode' do - is_expected.not_to be_allowed(:read_all_resources) - end - end + describe 'change_repository_storage' do + it_behaves_like 'admin only access', :change_repository_storage end end diff --git a/spec/requests/robots_txt_spec.rb b/spec/requests/robots_txt_spec.rb new file mode 100644 index 00000000000..524db5f442b --- /dev/null +++ b/spec/requests/robots_txt_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Robots.txt Requests', :aggregate_failures do + before do + Gitlab::Testing::RobotsBlockerMiddleware.block_requests! + end + + after do + Gitlab::Testing::RobotsBlockerMiddleware.allow_requests! + end + + it 'allows the requests' do + requests = [ + '/users/sign_in' + ] + + requests.each do |request| + get request + + expect(response).not_to have_gitlab_http_status(:service_unavailable), "#{request} must be allowed" + end + end + + it 'blocks the requests' do + requests = [ + '/autocomplete/users', + '/search', + '/admin', + '/profile', + '/dashboard', + '/users', + '/users/foo', + '/help', + '/s/', + '/-/profile', + '/foo/bar/new', + '/foo/bar/edit', + '/foo/bar/raw', + '/groups/foo/analytics', + '/groups/foo/contribution_analytics', + '/groups/foo/group_members', + '/foo/bar/project.git', + '/foo/bar/archive/foo', + '/foo/bar/repository/archive', + '/foo/bar/activity', + '/foo/bar/blame', + '/foo/bar/commits', + '/foo/bar/commit', + '/foo/bar/compare', + '/foo/bar/network', + '/foo/bar/graphs', + '/foo/bar/merge_requests/1.patch', + '/foo/bar/merge_requests/1.diff', + '/foo/bar/merge_requests/1/diffs', + '/foo/bar/deploy_keys', + '/foo/bar/hooks', + '/foo/bar/services', + '/foo/bar/protected_branches', + '/foo/bar/uploads/foo', + '/foo/bar/project_members', + '/foo/bar/settings' + ] + + requests.each do |request| + get request + + expect(response).to have_gitlab_http_status(:service_unavailable), "#{request} must be disallowed" + end + end +end diff --git a/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb index 7e3f1fdb379..90956e7b4ea 100644 --- a/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb +++ b/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb @@ -28,6 +28,7 @@ RSpec.describe Clusters::Kubernetes::CreateOrUpdateNamespaceService, '#execute' stub_kubeclient_get_secret_error(api_url, 'gitlab-token') stub_kubeclient_create_secret(api_url) + stub_kubeclient_delete_role_binding(api_url, "gitlab-#{namespace}", namespace: namespace) stub_kubeclient_put_role_binding(api_url, "gitlab-#{namespace}", namespace: namespace) stub_kubeclient_get_namespace(api_url, namespace: namespace) stub_kubeclient_get_service_account_error(api_url, "#{namespace}-service-account", namespace: namespace) diff --git a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb index 257e2e53733..3020ec2bf6f 100644 --- a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb +++ b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb @@ -141,6 +141,7 @@ RSpec.describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do before do cluster.platform_kubernetes.rbac! + stub_kubeclient_delete_role_binding(api_url, role_binding_name, namespace: namespace) stub_kubeclient_put_role_binding(api_url, role_binding_name, namespace: namespace) stub_kubeclient_put_role(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME, namespace: namespace) stub_kubeclient_put_role_binding(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME, namespace: namespace) diff --git a/spec/services/issues/import_csv_service_spec.rb b/spec/services/issues/import_csv_service_spec.rb index b34f77697a3..3e18c5ad64a 100644 --- a/spec/services/issues/import_csv_service_spec.rb +++ b/spec/services/issues/import_csv_service_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Issues::ImportCsvService do shared_examples_for 'an issue importer' do it 'records the import attempt' do expect { subject } - .to change { CsvIssueImport.where(project: project, user: user).count } + .to change { Issues::CsvImport.where(project: project, user: user).count } .by 1 end end diff --git a/spec/services/packages/create_event_service_spec.rb b/spec/services/packages/create_event_service_spec.rb index 7e66b430a8c..c2f1bd48bda 100644 --- a/spec/services/packages/create_event_service_spec.rb +++ b/spec/services/packages/create_event_service_spec.rb @@ -16,13 +16,29 @@ RSpec.describe Packages::CreateEventService do describe '#execute' do shared_examples 'package event creation' do |originator_type, expected_scope| - it 'creates the event' do - expect { subject }.to change { Packages::Event.count }.by(1) + context 'with feature flag disable' do + before do + stub_feature_flags(collect_package_events: false) + end - expect(subject.originator_type).to eq(originator_type) - expect(subject.originator).to eq(user&.id) - expect(subject.event_scope).to eq(expected_scope) - expect(subject.event_type).to eq(event_name) + it 'returns nil' do + expect(subject).to be nil + end + end + + context 'with feature flag enabled' do + before do + stub_feature_flags(collect_package_events: true) + end + + it 'creates the event' do + expect { subject }.to change { Packages::Event.count }.by(1) + + expect(subject.originator_type).to eq(originator_type) + expect(subject.originator).to eq(user&.id) + expect(subject.event_scope).to eq(expected_scope) + expect(subject.event_type).to eq(event_name) + end end end diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index 113bb31e4be..82692abd4cf 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -250,6 +250,11 @@ module KubernetesHelpers .to_return(kube_response({})) end + def stub_kubeclient_delete_role_binding(api_url, name, namespace: 'default') + WebMock.stub_request(:delete, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{name}") + .to_return(kube_response({})) + end + def stub_kubeclient_put_role_binding(api_url, name, namespace: 'default') WebMock.stub_request(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{name}") .to_return(kube_response({})) diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb index ba31e8e6056..39433cf0fd0 100644 --- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -128,6 +128,10 @@ RSpec.shared_examples 'job token for package uploads' do end RSpec.shared_examples 'a package tracking event' do |category, action| + before do + stub_feature_flags(collect_package_events: true) + end + it "creates a gitlab tracking event #{action}" do expect(Gitlab::Tracking).to receive(:event).with(category, action) |