Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
fae19e0b68
commit
eb004dc626
|
@ -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'
|
||||
|
|
|
@ -1 +1 @@
|
|||
b011445d9ffa82c36c771946e035852888df0730
|
||||
b85367529ca34c8c423b94b7486c44e109ed553f
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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__(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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],
|
||||
}),
|
||||
{},
|
||||
),
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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
|
||||
// },
|
||||
}));
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
query getAllIssues($projectPath: ID!) {
|
||||
project(fullPath: $projectPath) {
|
||||
issues {
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
webPath
|
||||
webUrl
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
query getIssues($projectPath: ID!) {
|
||||
project(fullPath: $projectPath) {
|
||||
issues {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Adds feature flag to disable package events
|
||||
merge_request: 45802
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Migrate DeprecatedModal to GitLab UI Modal for promoted labels
|
||||
merge_request: 46047
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix wide content overflow on Notebook output
|
||||
merge_request: 45971
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix single file editor patch branch name
|
||||
merge_request: 46044
|
||||
author:
|
||||
type: fixed
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
```
|
|
@ -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
|
||||
|
|
|
@ -36,7 +36,7 @@ module Gitlab
|
|||
}
|
||||
end
|
||||
|
||||
@client = Client.new(credentials[:user], opts)
|
||||
@client = Client.new(credentials[:user], **opts)
|
||||
end
|
||||
|
||||
def execute
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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>"
|
||||
`;
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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', () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 }
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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({}))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue