Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-27 12:08:33 +00:00
parent fae19e0b68
commit eb004dc626
75 changed files with 1115 additions and 361 deletions

View File

@ -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'

View File

@ -1 +1 @@
b011445d9ffa82c36c771946e035852888df0730
b85367529ca34c8c423b94b7486c44e109ed553f

View File

@ -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'

View File

@ -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)

View File

@ -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"
/>

View File

@ -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,

View File

@ -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>

View File

@ -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__(

View File

@ -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');
});
});
const promoteLabelModal = document.getElementById('promote-label-modal');
let promoteLabelModalComponent;
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;
return new Vue({
el: '#js-promote-label-modal',
data() {
return {
modalProps: {
labelTitle: '',
labelColor: '',
labelTextColor: '',
url: '',
groupName: '',
},
},
render(createElement) {
return createElement('promote-label-modal', {
props: this.modalProps,
});
},
});
}
};
},
mounted() {
eventHub.$on('promoteLabelModal.props', this.setModalProps);
eventHub.$emit('promoteLabelModal.mounted');
return promoteLabelModalComponent;
promoteLabelButtons.forEach(button => {
button.removeAttribute('disabled');
button.addEventListener('click', () => {
this.$root.$emit('bv::show::modal', 'promote-label-modal');
eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
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;
},
},
render(createElement) {
return createElement(PromoteLabelModal, {
props: this.modalProps,
});
},
});
};
document.addEventListener('DOMContentLoaded', initLabelIndex);

View File

@ -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">&bull;</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">&bull;</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>

View File

@ -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">&bull;</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>

View File

@ -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>

View File

@ -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],
}),
{},
),
}),
),
);
},
};

View File

@ -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,
},
});
};

View File

@ -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
// },
}));
});
},
},
};

View File

@ -0,0 +1,13 @@
query getAllIssues($projectPath: ID!) {
project(fullPath: $projectPath) {
issues {
nodes {
id
title
webPath
webUrl
state
}
}
}
}

View File

@ -0,0 +1,7 @@
query getIssues($projectPath: ID!) {
project(fullPath: $projectPath) {
issues {
count
}
}
}

View File

@ -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 = {

View File

@ -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"

View File

@ -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" />

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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!(

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Adds feature flag to disable package events
merge_request: 45802
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Migrate DeprecatedModal to GitLab UI Modal for promoted labels
merge_request: 46047
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix wide content overflow on Notebook output
merge_request: 45971
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix single file editor patch branch name
merge_request: 46044
author:
type: fixed

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
```

View File

@ -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

View File

@ -36,7 +36,7 @@ module Gitlab
}
end
@client = Client.new(credentials[:user], opts)
@client = Client.new(credentials[:user], **opts)
end
def execute

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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);
});
});

View File

@ -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>"
`;

View File

@ -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);
});
});
});

View File

@ -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);
});
});
});
});

View File

@ -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),
},
}),
);
});
});

View File

@ -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', () => {

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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 }

View File

@ -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) }
it_behaves_like 'admin only access', :read_all_resources
end
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 '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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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({}))

View File

@ -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)