Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b1e352740b
commit
1d9f78b3a4
|
|
@ -0,0 +1,9 @@
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This issue is part of a bigger development effort described in detail by its epic. The scope of this issue is to ...
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
<!-- Likely in the form of checkboxed elements -->
|
||||||
|
|
||||||
|
- [ ] TODO
|
||||||
|
|
@ -2459,17 +2459,6 @@ Gitlab/FeatureAvailableUsage:
|
||||||
# WIP See https://gitlab.com/gitlab-org/gitlab/-/issues/327490
|
# WIP See https://gitlab.com/gitlab-org/gitlab/-/issues/327490
|
||||||
Style/RegexpLiteralMixedPreserve:
|
Style/RegexpLiteralMixedPreserve:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'ee/app/models/status_page/project_setting.rb'
|
|
||||||
- 'ee/app/presenters/vulnerability_presenter.rb'
|
|
||||||
- 'ee/lib/api/geo_nodes.rb'
|
|
||||||
- 'ee/lib/gitlab/vulnerabilities/standard_vulnerability.rb'
|
|
||||||
- 'lib/api/invitations.rb'
|
|
||||||
- 'lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb'
|
|
||||||
- 'lib/gitlab/metrics/requests_rack_middleware.rb'
|
|
||||||
- 'lib/gitlab/metrics/subscribers/active_record.rb'
|
|
||||||
- 'lib/gitlab/regex.rb'
|
|
||||||
- 'lib/gitlab/utils.rb'
|
|
||||||
- 'lib/product_analytics/tracker.rb'
|
|
||||||
- 'qa/qa/page/project/settings/advanced.rb'
|
- 'qa/qa/page/project/settings/advanced.rb'
|
||||||
- 'qa/spec/service/docker_run/gitlab_runner_spec.rb'
|
- 'qa/spec/service/docker_run/gitlab_runner_spec.rb'
|
||||||
- 'rubocop/cop/gitlab/duplicate_spec_location.rb'
|
- 'rubocop/cop/gitlab/duplicate_spec_location.rb'
|
||||||
|
|
|
||||||
|
|
@ -506,7 +506,10 @@ export default {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.gon?.features?.diffsVirtualScrolling) {
|
if (
|
||||||
|
window.gon?.features?.diffsVirtualScrolling ||
|
||||||
|
window.gon?.features?.diffSearchingUsageData
|
||||||
|
) {
|
||||||
let keydownTime;
|
let keydownTime;
|
||||||
Mousetrap.bind(['mod+f', 'mod+g'], () => {
|
Mousetrap.bind(['mod+f', 'mod+g'], () => {
|
||||||
keydownTime = new Date().getTime();
|
keydownTime = new Date().getTime();
|
||||||
|
|
@ -520,6 +523,11 @@ export default {
|
||||||
// and max 1000ms to be sure it the search box is filtered
|
// and max 1000ms to be sure it the search box is filtered
|
||||||
if (delta >= 0 && delta < 1000) {
|
if (delta >= 0 && delta < 1000) {
|
||||||
this.disableVirtualScroller = true;
|
this.disableVirtualScroller = true;
|
||||||
|
|
||||||
|
if (window.gon?.features?.diffSearchingUsageData) {
|
||||||
|
api.trackRedisHllUserEvent('i_code_review_user_searches_diff');
|
||||||
|
api.trackRedisCounterEvent('user_searches_diffs');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
import { mount2faAuthentication } from '~/authentication/mount_2fa';
|
import { mount2faAuthentication } from '~/authentication/mount_2fa';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', mount2faAuthentication);
|
mount2faAuthentication();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
import initEnvironmentsFolderBundle from '~/environments/folder/environments_folder_bundle';
|
import initEnvironmentsFolderBundle from '~/environments/folder/environments_folder_bundle';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initEnvironmentsFolderBundle);
|
initEnvironmentsFolderBundle();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
import monitoringApp from '~/monitoring/monitoring_app';
|
import monitoringApp from '~/monitoring/monitoring_app';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', monitoringApp);
|
monitoringApp();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
import initTerminal from '~/terminal/';
|
import initTerminal from '~/terminal/';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initTerminal);
|
initTerminal();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import $ from 'jquery';
|
||||||
import ShortcutsNetwork from '~/behaviors/shortcuts/shortcuts_network';
|
import ShortcutsNetwork from '~/behaviors/shortcuts/shortcuts_network';
|
||||||
import Network from '../network';
|
import Network from '../network';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
(() => {
|
||||||
if (!$('.network-graph').length) return;
|
if (!$('.network-graph').length) return;
|
||||||
|
|
||||||
const networkGraph = new Network({
|
const networkGraph = new Network({
|
||||||
|
|
@ -14,4 +14,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
// eslint-disable-next-line no-new
|
// eslint-disable-next-line no-new
|
||||||
new ShortcutsNetwork(networkGraph.branch_graph);
|
new ShortcutsNetwork(networkGraph.branch_graph);
|
||||||
});
|
})();
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui'
|
||||||
|
|
||||||
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
|
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
|
||||||
import { s__ } from '~/locale';
|
import { s__ } from '~/locale';
|
||||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
|
||||||
import {
|
import {
|
||||||
visibilityOptions,
|
visibilityOptions,
|
||||||
visibilityLevelDescriptions,
|
visibilityLevelDescriptions,
|
||||||
|
|
@ -48,7 +47,7 @@ export default {
|
||||||
GlFormCheckbox,
|
GlFormCheckbox,
|
||||||
GlToggle,
|
GlToggle,
|
||||||
},
|
},
|
||||||
mixins: [settingsMixin, glFeatureFlagsMixin()],
|
mixins: [settingsMixin],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
requestCveAvailable: {
|
requestCveAvailable: {
|
||||||
|
|
@ -737,22 +736,5 @@ export default {
|
||||||
}}</template>
|
}}</template>
|
||||||
</gl-form-checkbox>
|
</gl-form-checkbox>
|
||||||
</project-setting-row>
|
</project-setting-row>
|
||||||
<project-setting-row
|
|
||||||
v-if="glFeatures.allowEditingCommitMessages"
|
|
||||||
ref="allow-editing-commit-messages"
|
|
||||||
class="gl-mb-4"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
:value="allowEditingCommitMessages"
|
|
||||||
type="hidden"
|
|
||||||
name="project[project_setting_attributes][allow_editing_commit_messages]"
|
|
||||||
/>
|
|
||||||
<gl-form-checkbox v-model="allowEditingCommitMessages">
|
|
||||||
{{ s__('ProjectSettings|Allow editing commit messages') }}
|
|
||||||
<template #help>{{
|
|
||||||
s__('ProjectSettings|Commit authors can edit commit messages on unprotected branches.')
|
|
||||||
}}</template>
|
|
||||||
</gl-form-checkbox>
|
|
||||||
</project-setting-row>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import initStaticSiteEditor from '~/static_site_editor';
|
import initStaticSiteEditor from '~/static_site_editor';
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
initStaticSiteEditor(document.querySelector('#static-site-editor'));
|
||||||
initStaticSiteEditor(document.querySelector('#static-site-editor'));
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlModal, GlSprintf } from '@gitlab/ui';
|
import { GlModal, GlSprintf, GlFormInput } from '@gitlab/ui';
|
||||||
import { n__ } from '~/locale';
|
import { n__ } from '~/locale';
|
||||||
import {
|
import {
|
||||||
REMOVE_TAG_CONFIRMATION_TEXT,
|
REMOVE_TAG_CONFIRMATION_TEXT,
|
||||||
|
|
@ -12,6 +12,7 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
GlModal,
|
GlModal,
|
||||||
GlSprintf,
|
GlSprintf,
|
||||||
|
GlFormInput,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
itemsToBeDeleted: {
|
itemsToBeDeleted: {
|
||||||
|
|
@ -25,7 +26,15 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
projectPath: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
imageProjectPath() {
|
||||||
|
return this.itemsToBeDeleted[0]?.project?.path;
|
||||||
|
},
|
||||||
modalTitle() {
|
modalTitle() {
|
||||||
if (this.deleteImage) {
|
if (this.deleteImage) {
|
||||||
return DELETE_IMAGE_CONFIRMATION_TITLE;
|
return DELETE_IMAGE_CONFIRMATION_TITLE;
|
||||||
|
|
@ -40,6 +49,7 @@ export default {
|
||||||
if (this.deleteImage) {
|
if (this.deleteImage) {
|
||||||
return {
|
return {
|
||||||
message: DELETE_IMAGE_CONFIRMATION_TEXT,
|
message: DELETE_IMAGE_CONFIRMATION_TEXT,
|
||||||
|
item: this.imageProjectPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (this.itemsToBeDeleted.length > 1) {
|
if (this.itemsToBeDeleted.length > 1) {
|
||||||
|
|
@ -55,6 +65,9 @@ export default {
|
||||||
item: first?.path,
|
item: first?.path,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
disablePrimaryButton() {
|
||||||
|
return this.deleteImage && this.projectPath !== this.imageProjectPath;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
show() {
|
show() {
|
||||||
|
|
@ -69,10 +82,14 @@ export default {
|
||||||
ref="deleteModal"
|
ref="deleteModal"
|
||||||
modal-id="delete-tag-modal"
|
modal-id="delete-tag-modal"
|
||||||
ok-variant="danger"
|
ok-variant="danger"
|
||||||
:action-primary="{ text: __('Confirm'), attributes: { variant: 'danger' } }"
|
:action-primary="{
|
||||||
|
text: __('Delete'),
|
||||||
|
attributes: [{ variant: 'danger' }, { disabled: disablePrimaryButton }],
|
||||||
|
}"
|
||||||
:action-cancel="{ text: __('Cancel') }"
|
:action-cancel="{ text: __('Cancel') }"
|
||||||
@primary="$emit('confirmDelete')"
|
@primary="$emit('confirmDelete')"
|
||||||
@cancel="$emit('cancelDelete')"
|
@cancel="$emit('cancelDelete')"
|
||||||
|
@change="projectPath = ''"
|
||||||
>
|
>
|
||||||
<template #modal-title>{{ modalTitle }}</template>
|
<template #modal-title>{{ modalTitle }}</template>
|
||||||
<p v-if="modalDescription" data-testid="description">
|
<p v-if="modalDescription" data-testid="description">
|
||||||
|
|
@ -80,7 +97,13 @@ export default {
|
||||||
<template #item>
|
<template #item>
|
||||||
<b>{{ modalDescription.item }}</b>
|
<b>{{ modalDescription.item }}</b>
|
||||||
</template>
|
</template>
|
||||||
|
<template #code>
|
||||||
|
<code>{{ modalDescription.item }}</code>
|
||||||
|
</template>
|
||||||
</gl-sprintf>
|
</gl-sprintf>
|
||||||
</p>
|
</p>
|
||||||
|
<div v-if="deleteImage">
|
||||||
|
<gl-form-input v-model="projectPath" />
|
||||||
|
</div>
|
||||||
</gl-modal>
|
</gl-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
import { GlIcon, GlTooltipDirective, GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||||
import { sprintf, n__, s__ } from '~/locale';
|
import { sprintf, n__, s__ } from '~/locale';
|
||||||
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
|
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
|
||||||
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
||||||
|
|
@ -27,7 +27,7 @@ import getContainerRepositoryTagsCountQuery from '../../graphql/queries/get_cont
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DetailsHeader',
|
name: 'DetailsHeader',
|
||||||
components: { GlButton, GlIcon, TitleArea, MetadataItem },
|
components: { GlIcon, TitleArea, MetadataItem, GlDropdown, GlDropdownItem },
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
},
|
},
|
||||||
|
|
@ -143,9 +143,22 @@ export default {
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #right-actions>
|
<template #right-actions>
|
||||||
<gl-button variant="danger" :disabled="deleteButtonDisabled" @click="$emit('delete')">
|
<gl-dropdown
|
||||||
|
icon="ellipsis_v"
|
||||||
|
text="More actions"
|
||||||
|
:text-sr-only="true"
|
||||||
|
category="tertiary"
|
||||||
|
no-caret
|
||||||
|
right
|
||||||
|
>
|
||||||
|
<gl-dropdown-item
|
||||||
|
variant="danger"
|
||||||
|
:disabled="deleteButtonDisabled"
|
||||||
|
@click="$emit('delete')"
|
||||||
|
>
|
||||||
{{ __('Delete image repository') }}
|
{{ __('Delete image repository') }}
|
||||||
</gl-button>
|
</gl-dropdown-item>
|
||||||
|
</gl-dropdown>
|
||||||
</template>
|
</template>
|
||||||
</title-area>
|
</title-area>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ export const DETAILS_DELETE_IMAGE_ERROR_MESSAGE = s__(
|
||||||
|
|
||||||
export const DELETE_IMAGE_CONFIRMATION_TITLE = s__('ContainerRegistry|Delete image repository?');
|
export const DELETE_IMAGE_CONFIRMATION_TITLE = s__('ContainerRegistry|Delete image repository?');
|
||||||
export const DELETE_IMAGE_CONFIRMATION_TEXT = s__(
|
export const DELETE_IMAGE_CONFIRMATION_TEXT = s__(
|
||||||
'ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone.',
|
'ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone. Please type the following to confirm: %{code}',
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SCHEDULED_FOR_DELETION_STATUS_TITLE = s__(
|
export const SCHEDULED_FOR_DELETION_STATUS_TITLE = s__(
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ query getContainerRepositoryDetails($id: ID!) {
|
||||||
expirationPolicyCleanupStatus
|
expirationPolicyCleanupStatus
|
||||||
project {
|
project {
|
||||||
visibility
|
visibility
|
||||||
|
path
|
||||||
containerExpirationPolicy {
|
containerExpirationPolicy {
|
||||||
enabled
|
enabled
|
||||||
nextRunAt
|
nextRunAt
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ export default {
|
||||||
},
|
},
|
||||||
deleteImage() {
|
deleteImage() {
|
||||||
this.deleteImageAlert = true;
|
this.deleteImageAlert = true;
|
||||||
this.itemsToBeDeleted = [{ path: this.containerRepository.path }];
|
this.itemsToBeDeleted = [{ ...this.containerRepository }];
|
||||||
this.$refs.deleteModal.show();
|
this.$refs.deleteModal.show();
|
||||||
},
|
},
|
||||||
deleteImageError() {
|
deleteImageError() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
import initSourcegraph from './index';
|
import initSourcegraph from './index';
|
||||||
|
|
||||||
/**
|
initSourcegraph();
|
||||||
* Load sourcegraph in it's own listener so that it's isolated from failures.
|
|
||||||
*/
|
|
||||||
document.addEventListener('DOMContentLoaded', initSourcegraph);
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Analytics
|
||||||
|
module CycleAnalytics
|
||||||
|
module StageActions
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
include CycleAnalyticsParams
|
||||||
|
|
||||||
|
before_action :validate_params, only: %i[median]
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
result = list_service.execute
|
||||||
|
|
||||||
|
if result.success?
|
||||||
|
render json: cycle_analytics_configuration(result.payload[:stages])
|
||||||
|
else
|
||||||
|
render json: { message: result.message }, status: result.http_status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def median
|
||||||
|
render json: { value: data_collector.median.seconds }
|
||||||
|
end
|
||||||
|
|
||||||
|
def average
|
||||||
|
render json: { value: data_collector.average.seconds }
|
||||||
|
end
|
||||||
|
|
||||||
|
def records
|
||||||
|
serialized_records = data_collector.serialized_records do |relation|
|
||||||
|
add_pagination_headers(relation)
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: serialized_records
|
||||||
|
end
|
||||||
|
|
||||||
|
def count
|
||||||
|
render json: { count: data_collector.count }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def parent
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def value_stream_class
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_pagination_headers(relation)
|
||||||
|
Gitlab::Pagination::OffsetHeaderBuilder.new(
|
||||||
|
request_context: self,
|
||||||
|
per_page: relation.limit_value,
|
||||||
|
page: relation.current_page,
|
||||||
|
next_page: relation.next_page,
|
||||||
|
prev_page: relation.prev_page,
|
||||||
|
params: permitted_cycle_analytics_params
|
||||||
|
).execute(exclude_total_headers: true, data_without_counts: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stage
|
||||||
|
@stage ||= ::Analytics::CycleAnalytics::StageFinder.new(parent: parent, stage_id: params[:id]).execute
|
||||||
|
end
|
||||||
|
|
||||||
|
def data_collector
|
||||||
|
@data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(
|
||||||
|
stage: stage,
|
||||||
|
params: request_params.to_data_collector_params
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def value_stream
|
||||||
|
@value_stream ||= value_stream_class.build_default_value_stream(parent)
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_params
|
||||||
|
{ value_stream: value_stream }
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_service
|
||||||
|
Analytics::CycleAnalytics::Stages::ListService.new(parent: parent, current_user: current_user, params: list_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cycle_analytics_configuration(stages)
|
||||||
|
stage_presenters = stages.map { |s| ::Analytics::CycleAnalytics::StagePresenter.new(s) }
|
||||||
|
|
||||||
|
Analytics::CycleAnalytics::ConfigurationEntity.new(stages: stage_presenters)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -16,8 +16,19 @@ module CycleAnalyticsParams
|
||||||
end
|
end
|
||||||
|
|
||||||
def options(params)
|
def options(params)
|
||||||
@options ||= { from: start_date(params), current_user: current_user }.merge(date_range(params))
|
@options ||= {}.tap do |opts|
|
||||||
|
opts[:current_user] = current_user
|
||||||
|
opts[:projects] = params[:project_ids] if params[:project_ids]
|
||||||
|
opts[:group] = params[:group_id] if params[:group_id]
|
||||||
|
opts[:from] = params[:from] || start_date(params)
|
||||||
|
opts[:to] = params[:to] if params[:to]
|
||||||
|
opts[:end_event_filter] = params[:end_event_filter] if params[:end_event_filter]
|
||||||
|
opts.merge!(params.slice(*::Gitlab::Analytics::CycleAnalytics::RequestParams::FINDER_PARAM_NAMES))
|
||||||
|
opts.merge!(date_range(params))
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def start_date(params)
|
def start_date(params)
|
||||||
case params[:start_date]
|
case params[:start_date]
|
||||||
|
|
@ -41,6 +52,27 @@ module CycleAnalyticsParams
|
||||||
date = field.is_a?(Date) || field.is_a?(Time) ? field : Date.parse(field)
|
date = field.is_a?(Date) || field.is_a?(Time) ? field : Date.parse(field)
|
||||||
date.to_time.utc
|
date.to_time.utc
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def permitted_cycle_analytics_params
|
||||||
|
params.permit(*::Gitlab::Analytics::CycleAnalytics::RequestParams::STRONG_PARAMS_DEFINITION)
|
||||||
|
end
|
||||||
|
|
||||||
|
def all_cycle_analytics_params
|
||||||
|
permitted_cycle_analytics_params.merge(current_user: current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def request_params
|
||||||
|
@request_params ||= ::Gitlab::Analytics::CycleAnalytics::RequestParams.new(all_cycle_analytics_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_params
|
||||||
|
if request_params.invalid?
|
||||||
|
render(
|
||||||
|
json: { message: 'Invalid parameters', errors: request_params.errors },
|
||||||
|
status: :unprocessable_entity
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
CycleAnalyticsParams.prepend_mod_with('CycleAnalyticsParams')
|
CycleAnalyticsParams.prepend_mod_with('CycleAnalyticsParams')
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class Groups::EmailCampaignsController < Groups::ApplicationController
|
||||||
project_pipelines_url(group.projects.first)
|
project_pipelines_url(group.projects.first)
|
||||||
when :trial
|
when :trial
|
||||||
'https://about.gitlab.com/free-trial/'
|
'https://about.gitlab.com/free-trial/'
|
||||||
when :team
|
when :team, :team_short
|
||||||
group_group_members_url(group)
|
group_group_members_url(group)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Projects::Analytics::CycleAnalytics::StagesController < Projects::ApplicationController
|
class Projects::Analytics::CycleAnalytics::StagesController < Projects::ApplicationController
|
||||||
|
include ::Analytics::CycleAnalytics::StageActions
|
||||||
|
extend ::Gitlab::Utils::Override
|
||||||
|
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
feature_category :planning_analytics
|
feature_category :planning_analytics
|
||||||
|
|
@ -8,37 +11,19 @@ class Projects::Analytics::CycleAnalytics::StagesController < Projects::Applicat
|
||||||
before_action :authorize_read_cycle_analytics!
|
before_action :authorize_read_cycle_analytics!
|
||||||
before_action :only_default_value_stream_is_allowed!
|
before_action :only_default_value_stream_is_allowed!
|
||||||
|
|
||||||
def index
|
|
||||||
result = list_service.execute
|
|
||||||
|
|
||||||
if result.success?
|
|
||||||
render json: cycle_analytics_configuration(result.payload[:stages])
|
|
||||||
else
|
|
||||||
render json: { message: result.message }, status: result.http_status
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
override :parent
|
||||||
|
def parent
|
||||||
|
@project
|
||||||
|
end
|
||||||
|
|
||||||
|
override :value_stream_class
|
||||||
|
def value_stream_class
|
||||||
|
Analytics::CycleAnalytics::ProjectValueStream
|
||||||
|
end
|
||||||
|
|
||||||
def only_default_value_stream_is_allowed!
|
def only_default_value_stream_is_allowed!
|
||||||
render_404 if params[:value_stream_id] != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
|
render_404 if params[:value_stream_id] != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
|
||||||
end
|
end
|
||||||
|
|
||||||
def value_stream
|
|
||||||
Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project)
|
|
||||||
end
|
|
||||||
|
|
||||||
def list_params
|
|
||||||
{ value_stream: value_stream }
|
|
||||||
end
|
|
||||||
|
|
||||||
def list_service
|
|
||||||
Analytics::CycleAnalytics::Stages::ListService.new(parent: @project, current_user: current_user, params: list_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def cycle_analytics_configuration(stages)
|
|
||||||
stage_presenters = stages.map { |s| ::Analytics::CycleAnalytics::StagePresenter.new(s) }
|
|
||||||
|
|
||||||
Analytics::CycleAnalytics::ConfigurationEntity.new(stages: stage_presenters)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,14 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
|
||||||
def diffs_metadata
|
def diffs_metadata
|
||||||
diffs = @compare.diffs(diff_options)
|
diffs = @compare.diffs(diff_options)
|
||||||
|
|
||||||
|
options = additional_attributes.merge(
|
||||||
|
only_context_commits: show_only_context_commits?,
|
||||||
|
merge_ref_head_diff: render_merge_ref_head_diff?,
|
||||||
|
allow_tree_conflicts: display_merge_conflicts_in_diff?
|
||||||
|
)
|
||||||
|
|
||||||
render json: DiffsMetadataSerializer.new(project: @merge_request.project, current_user: current_user)
|
render json: DiffsMetadataSerializer.new(project: @merge_request.project, current_user: current_user)
|
||||||
.represent(diffs, additional_attributes.merge(only_context_commits: show_only_context_commits?))
|
.represent(diffs, options)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
||||||
# Usage data feature flags
|
# Usage data feature flags
|
||||||
push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml)
|
push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml)
|
||||||
push_frontend_feature_flag(:diff_settings_usage_data, default_enabled: :yaml)
|
push_frontend_feature_flag(:diff_settings_usage_data, default_enabled: :yaml)
|
||||||
|
push_frontend_feature_flag(:diff_searching_usage_data, @project, default_enabled: :yaml)
|
||||||
|
|
||||||
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
|
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
|
||||||
experiment_instance.exclude! unless helpers.can_import_members?
|
experiment_instance.exclude! unless helpers.can_import_members?
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,6 @@ class ProjectsController < Projects::ApplicationController
|
||||||
# Project Export Rate Limit
|
# Project Export Rate Limit
|
||||||
before_action :export_rate_limit, only: [:export, :download_export, :generate_new_export]
|
before_action :export_rate_limit, only: [:export, :download_export, :generate_new_export]
|
||||||
|
|
||||||
before_action only: [:edit] do
|
|
||||||
push_frontend_feature_flag(:allow_editing_commit_messages, @project)
|
|
||||||
end
|
|
||||||
|
|
||||||
before_action do
|
before_action do
|
||||||
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
|
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
|
||||||
push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml)
|
push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml)
|
||||||
|
|
@ -399,7 +395,6 @@ class ProjectsController < Projects::ApplicationController
|
||||||
%i[
|
%i[
|
||||||
show_default_award_emojis
|
show_default_award_emojis
|
||||||
squash_option
|
squash_option
|
||||||
allow_editing_commit_messages
|
|
||||||
mr_default_target_self
|
mr_default_target_self
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ class SearchController < ApplicationController
|
||||||
include SearchHelper
|
include SearchHelper
|
||||||
include RedisTracking
|
include RedisTracking
|
||||||
|
|
||||||
|
RESCUE_FROM_TIMEOUT_ACTIONS = [:count, :show].freeze
|
||||||
|
|
||||||
track_redis_hll_event :show, name: 'i_search_total'
|
track_redis_hll_event :show, name: 'i_search_total'
|
||||||
|
|
||||||
around_action :allow_gitaly_ref_name_caching
|
around_action :allow_gitaly_ref_name_caching
|
||||||
|
|
@ -154,13 +156,22 @@ class SearchController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_timeout(exception)
|
def render_timeout(exception)
|
||||||
raise exception unless action_name.to_sym == :show
|
raise exception unless action_name.to_sym.in?(RESCUE_FROM_TIMEOUT_ACTIONS)
|
||||||
|
|
||||||
log_exception(exception)
|
log_exception(exception)
|
||||||
|
|
||||||
@timeout = true
|
@timeout = true
|
||||||
|
|
||||||
|
if count_action_name?
|
||||||
|
render json: {}, status: :request_timeout
|
||||||
|
else
|
||||||
render status: :request_timeout
|
render status: :request_timeout
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def count_action_name?
|
||||||
|
action_name.to_sym == :count
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
SearchController.prepend_mod_with('SearchController')
|
SearchController.prepend_mod_with('SearchController')
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,12 @@ module Mutations
|
||||||
def ready?(**args)
|
def ready?(**args)
|
||||||
raise_resource_not_available_error! ERROR_MESSAGE if Gitlab::Database.read_only?
|
raise_resource_not_available_error! ERROR_MESSAGE if Gitlab::Database.read_only?
|
||||||
|
|
||||||
|
missing_args = self.class.arguments.values
|
||||||
|
.reject { |arg| arg.accepts?(args.fetch(arg.keyword, :not_given)) }
|
||||||
|
.map(&:graphql_name)
|
||||||
|
|
||||||
|
raise ArgumentError, "Arguments must be provided: #{missing_args.join(", ")}" if missing_args.any?
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,8 @@ module Mutations
|
||||||
|
|
||||||
argument :due_date,
|
argument :due_date,
|
||||||
Types::TimeType,
|
Types::TimeType,
|
||||||
required: false,
|
required: :nullable,
|
||||||
description: 'The desired due date for the issue, ' \
|
description: 'The desired due date for the issue. Due date is removed if null.'
|
||||||
'due date will be removed if absent or set to null'
|
|
||||||
|
|
||||||
def ready?(**args)
|
|
||||||
unless args.key?(:due_date)
|
|
||||||
raise Gitlab::Graphql::Errors::ArgumentError, 'Argument dueDate must be provided (`null` accepted)'
|
|
||||||
end
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def resolve(project_path:, iid:, due_date:)
|
def resolve(project_path:, iid:, due_date:)
|
||||||
issue = authorized_find!(project_path: project_path, iid: iid)
|
issue = authorized_find!(project_path: project_path, iid: iid)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,29 @@ module Types
|
||||||
@deprecation = gitlab_deprecation(kwargs)
|
@deprecation = gitlab_deprecation(kwargs)
|
||||||
@doc_reference = kwargs.delete(:see)
|
@doc_reference = kwargs.delete(:see)
|
||||||
|
|
||||||
|
# our custom addition `nullable` which allows us to declare
|
||||||
|
# an argument that must be provided, even if its value is null.
|
||||||
|
# When `required: true` then required arguments must not be null.
|
||||||
|
@gl_required = !!kwargs[:required]
|
||||||
|
@gl_nullable = kwargs[:required] == :nullable
|
||||||
|
|
||||||
|
# Only valid if an argument is also required.
|
||||||
|
if @gl_nullable
|
||||||
|
# Since the framework asserts that "required" means "cannot be null"
|
||||||
|
# we have to switch off "required" but still do the check in `ready?` behind the scenes
|
||||||
|
kwargs[:required] = false
|
||||||
|
end
|
||||||
|
|
||||||
super(*args, **kwargs, &block)
|
super(*args, **kwargs, &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def accepts?(value)
|
||||||
|
# if the argument is declared as required, it must be included
|
||||||
|
return false if @gl_required && value == :not_given
|
||||||
|
# if the argument is declared as required, the value can only be null IF it is also nullable.
|
||||||
|
return false if @gl_required && value.nil? && !@gl_nullable
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -491,7 +491,6 @@ module ProjectsHelper
|
||||||
|
|
||||||
def project_permissions_settings(project)
|
def project_permissions_settings(project)
|
||||||
feature = project.project_feature
|
feature = project.project_feature
|
||||||
|
|
||||||
{
|
{
|
||||||
packagesEnabled: !!project.packages_enabled,
|
packagesEnabled: !!project.packages_enabled,
|
||||||
visibilityLevel: project.visibility_level,
|
visibilityLevel: project.visibility_level,
|
||||||
|
|
@ -511,7 +510,6 @@ module ProjectsHelper
|
||||||
metricsDashboardAccessLevel: feature.metrics_dashboard_access_level,
|
metricsDashboardAccessLevel: feature.metrics_dashboard_access_level,
|
||||||
operationsAccessLevel: feature.operations_access_level,
|
operationsAccessLevel: feature.operations_access_level,
|
||||||
showDefaultAwardEmojis: project.show_default_award_emojis?,
|
showDefaultAwardEmojis: project.show_default_award_emojis?,
|
||||||
allowEditingCommitMessages: project.allow_editing_commit_messages?,
|
|
||||||
securityAndComplianceAccessLevel: project.security_and_compliance_access_level
|
securityAndComplianceAccessLevel: project.security_and_compliance_access_level
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -69,21 +69,26 @@ class WebHook < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def disable!
|
def disable!
|
||||||
update!(recent_failures: FAILURE_THRESHOLD + 1)
|
update_attribute(:recent_failures, FAILURE_THRESHOLD + 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
def enable!
|
def enable!
|
||||||
return if recent_failures == 0 && disabled_until.nil? && backoff_count == 0
|
return if recent_failures == 0 && disabled_until.nil? && backoff_count == 0
|
||||||
|
|
||||||
update!(recent_failures: 0, disabled_until: nil, backoff_count: 0)
|
assign_attributes(recent_failures: 0, disabled_until: nil, backoff_count: 0)
|
||||||
|
save(validate: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def backoff!
|
def backoff!
|
||||||
update!(disabled_until: next_backoff.from_now, backoff_count: backoff_count.succ.clamp(0, MAX_FAILURES))
|
assign_attributes(disabled_until: next_backoff.from_now, backoff_count: backoff_count.succ.clamp(0, MAX_FAILURES))
|
||||||
|
save(validate: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def failed!
|
def failed!
|
||||||
update!(recent_failures: recent_failures + 1) if recent_failures < MAX_FAILURES
|
return unless recent_failures < MAX_FAILURES
|
||||||
|
|
||||||
|
assign_attributes(recent_failures: recent_failures + 1)
|
||||||
|
save(validate: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Overridden in ProjectHook and GroupHook, other webhooks are not rate-limited.
|
# Overridden in ProjectHook and GroupHook, other webhooks are not rate-limited.
|
||||||
|
|
|
||||||
|
|
@ -435,7 +435,7 @@ class Project < ApplicationRecord
|
||||||
delegate :restrict_user_defined_variables, :restrict_user_defined_variables=, to: :ci_cd_settings, allow_nil: true
|
delegate :restrict_user_defined_variables, :restrict_user_defined_variables=, to: :ci_cd_settings, allow_nil: true
|
||||||
delegate :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true
|
delegate :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true
|
||||||
delegate :allow_merge_on_skipped_pipeline, :allow_merge_on_skipped_pipeline?,
|
delegate :allow_merge_on_skipped_pipeline, :allow_merge_on_skipped_pipeline?,
|
||||||
:allow_merge_on_skipped_pipeline=, :has_confluence?, :allow_editing_commit_messages?,
|
:allow_merge_on_skipped_pipeline=, :has_confluence?,
|
||||||
to: :project_setting
|
to: :project_setting
|
||||||
delegate :active?, to: :prometheus_integration, allow_nil: true, prefix: true
|
delegate :active?, to: :prometheus_integration, allow_nil: true, prefix: true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ProjectSetting < ApplicationRecord
|
class ProjectSetting < ApplicationRecord
|
||||||
|
include IgnorableColumns
|
||||||
|
|
||||||
|
ignore_column :allow_editing_commit_messages, remove_with: '14.4', remove_after: '2021-09-10'
|
||||||
|
|
||||||
belongs_to :project, inverse_of: :project_setting
|
belongs_to :project, inverse_of: :project_setting
|
||||||
|
|
||||||
enum squash_option: {
|
enum squash_option: {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ module Users
|
||||||
verify: 1,
|
verify: 1,
|
||||||
trial: 2,
|
trial: 2,
|
||||||
team: 3,
|
team: 3,
|
||||||
experience: 4
|
experience: 4,
|
||||||
|
team_short: 5
|
||||||
}, _suffix: true
|
}, _suffix: true
|
||||||
|
|
||||||
scope :without_track_and_series, -> (track, series) do
|
scope :without_track_and_series, -> (track, series) do
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DiffFileConflictType
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
|
included do
|
||||||
|
expose :conflict_type do |diff_file, options|
|
||||||
|
conflict_file = conflict_file(options, diff_file)
|
||||||
|
|
||||||
|
next unless conflict_file
|
||||||
|
|
||||||
|
conflict_file.conflict_type(diff_file)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def conflict_file(options, diff_file)
|
||||||
|
strong_memoize(:conflict_file) do
|
||||||
|
options[:conflicts] && options[:conflicts][diff_file.new_path]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class DiffFileEntity < DiffFileBaseEntity
|
class DiffFileEntity < DiffFileBaseEntity
|
||||||
|
include DiffFileConflictType
|
||||||
include CommitsHelper
|
include CommitsHelper
|
||||||
include IconsHelper
|
include IconsHelper
|
||||||
include Gitlab::Utils::StrongMemoize
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
@ -88,10 +89,4 @@ class DiffFileEntity < DiffFileBaseEntity
|
||||||
# If nothing is present, inline will be the default.
|
# If nothing is present, inline will be the default.
|
||||||
options.fetch(:diff_view, :inline).to_sym
|
options.fetch(:diff_view, :inline).to_sym
|
||||||
end
|
end
|
||||||
|
|
||||||
def conflict_file(options, diff_file)
|
|
||||||
strong_memoize(:conflict_file) do
|
|
||||||
options[:conflicts] && options[:conflicts][diff_file.new_path]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class DiffFileMetadataEntity < Grape::Entity
|
class DiffFileMetadataEntity < Grape::Entity
|
||||||
|
include DiffFileConflictType
|
||||||
|
|
||||||
expose :added_lines
|
expose :added_lines
|
||||||
expose :removed_lines
|
expose :removed_lines
|
||||||
expose :new_path
|
expose :new_path
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,13 @@
|
||||||
|
|
||||||
class DiffsMetadataEntity < DiffsEntity
|
class DiffsMetadataEntity < DiffsEntity
|
||||||
unexpose :diff_files
|
unexpose :diff_files
|
||||||
expose :diff_files, using: DiffFileMetadataEntity do |diffs, _|
|
expose :diff_files do |diffs, options|
|
||||||
diffs.raw_diff_files(sorted: true)
|
DiffFileMetadataEntity.represent(
|
||||||
|
diffs.raw_diff_files(sorted: true),
|
||||||
|
options.merge(
|
||||||
|
conflicts: conflicts(allow_tree_conflicts: options[:allow_tree_conflicts])
|
||||||
|
)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :conflict_resolution_path do |_, options|
|
expose :conflict_resolution_path do |_, options|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,13 @@ module Namespaces
|
||||||
completed_actions: [:created],
|
completed_actions: [:created],
|
||||||
incomplete_actions: [:git_write]
|
incomplete_actions: [:git_write]
|
||||||
},
|
},
|
||||||
|
team_short: {
|
||||||
|
interval_days: [1],
|
||||||
|
completed_actions: [:git_write],
|
||||||
|
incomplete_actions: [:user_added]
|
||||||
|
},
|
||||||
verify: {
|
verify: {
|
||||||
interval_days: [1, 5, 10],
|
interval_days: [2, 6, 11],
|
||||||
completed_actions: [:git_write],
|
completed_actions: [:git_write],
|
||||||
incomplete_actions: [:pipeline_created]
|
incomplete_actions: [:pipeline_created]
|
||||||
},
|
},
|
||||||
|
|
@ -98,13 +103,11 @@ module Namespaces
|
||||||
|
|
||||||
def can_perform_action?(user, group)
|
def can_perform_action?(user, group)
|
||||||
case track
|
case track
|
||||||
when :create
|
when :create, :verify
|
||||||
user.can?(:create_projects, group)
|
|
||||||
when :verify
|
|
||||||
user.can?(:create_projects, group)
|
user.can?(:create_projects, group)
|
||||||
when :trial
|
when :trial
|
||||||
user.can?(:start_trial, group)
|
user.can?(:start_trial, group)
|
||||||
when :team
|
when :team, :team_short
|
||||||
user.can?(:admin_group_member, group)
|
user.can?(:admin_group_member, group)
|
||||||
when :experience
|
when :experience
|
||||||
true
|
true
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
%p.profile-settings-content
|
%p.profile-settings-content
|
||||||
= s_("DeployTokens|Pick a name for your unique deploy token.")
|
- group_deploy_tokens_help_link_url = help_page_path('user/project/deploy_tokens/index.md')
|
||||||
|
- group_deploy_tokens_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_deploy_tokens_help_link_url }
|
||||||
|
= s_('DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}').html_safe % { link_start: group_deploy_tokens_help_link_start, link_end: '</a>'.html_safe }
|
||||||
|
|
||||||
= form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post, remote: Feature.enabled?(:ajax_new_deploy_token, group_or_project) do |f|
|
= form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post, remote: Feature.enabled?(:ajax_new_deploy_token, group_or_project) do |f|
|
||||||
= form_errors(token)
|
= form_errors(token)
|
||||||
|
|
@ -7,23 +9,26 @@
|
||||||
.form-group
|
.form-group
|
||||||
= f.label :name, class: 'label-bold'
|
= f.label :name, class: 'label-bold'
|
||||||
= f.text_field :name, class: 'form-control gl-form-input', data: { qa_selector: 'deploy_token_name_field' }, required: true
|
= f.text_field :name, class: 'form-control gl-form-input', data: { qa_selector: 'deploy_token_name_field' }, required: true
|
||||||
|
.text-secondary= s_('DeployTokens|Enter a unique name for your deploy token.')
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
= f.label :expires_at, _('Expires at (optional)'), class: 'label-bold'
|
= f.label :expires_at, _('Expiration date (optional)'), class: 'label-bold'
|
||||||
= f.text_field :expires_at, class: 'datepicker form-control', data: { qa_selector: 'deploy_token_expires_at_field' }, value: f.object.expires_at
|
= f.text_field :expires_at, class: 'datepicker form-control', data: { qa_selector: 'deploy_token_expires_at_field' }, value: f.object.expires_at
|
||||||
.text-secondary= s_('DeployTokens|Unless you enter a date, the token does not expire.')
|
.text-secondary= s_('DeployTokens|Enter an expiration date for your token. Defaults to never expire.')
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
= f.label :username, _('Username (optional)'), class: 'label-bold'
|
= f.label :username, _('Username (optional)'), class: 'label-bold'
|
||||||
= f.text_field :username, class: 'form-control'
|
= f.text_field :username, class: 'form-control'
|
||||||
.text-secondary= s_('DeployTokens|Unless you specify a username, it is set to "gitlab+deploy-token-{n}".')
|
.text-secondary
|
||||||
|
= html_escape(s_('DeployTokens|Enter a username for your token. Defaults to %{code_start}gitlab+deploy-token-{n}%{code_end}.')) % { code_start: '<code>'.html_safe, code_end: '</code>'.html_safe }
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
= f.label :scopes, _('Scopes [Select 1 or more]'), class: 'label-bold'
|
= f.label :scopes, _('Scopes (select at least one)'), class: 'label-bold'
|
||||||
%fieldset.form-group.form-check
|
%fieldset.form-group.form-check
|
||||||
= f.check_box :read_repository, class: 'form-check-input', data: { qa_selector: 'deploy_token_read_repository_checkbox' }
|
= f.check_box :read_repository, class: 'form-check-input', data: { qa_selector: 'deploy_token_read_repository_checkbox' }
|
||||||
= f.label :read_repository, 'read_repository', class: 'label-bold form-check-label'
|
= f.label :read_repository, 'read_repository', class: 'label-bold form-check-label'
|
||||||
.text-secondary= s_('DeployTokens|Allows read-only access to the repository.')
|
.text-secondary
|
||||||
|
= s_('DeployTokens|Allows read-only access to the repository.')
|
||||||
|
|
||||||
- if container_registry_enabled?(group_or_project)
|
- if container_registry_enabled?(group_or_project)
|
||||||
%fieldset.form-group.form-check
|
%fieldset.form-group.form-check
|
||||||
|
|
@ -34,18 +39,18 @@
|
||||||
%fieldset.form-group.form-check
|
%fieldset.form-group.form-check
|
||||||
= f.check_box :write_registry, class: 'form-check-input'
|
= f.check_box :write_registry, class: 'form-check-input'
|
||||||
= f.label :write_registry, 'write_registry', class: 'label-bold form-check-label'
|
= f.label :write_registry, 'write_registry', class: 'label-bold form-check-label'
|
||||||
.text-secondary= s_('DeployTokens|Allows write access to registry images.')
|
.text-secondary= s_('DeployTokens|Allows read and write access to registry images.')
|
||||||
|
|
||||||
- if packages_registry_enabled?(group_or_project)
|
- if packages_registry_enabled?(group_or_project)
|
||||||
%fieldset.form-group.form-check
|
%fieldset.form-group.form-check
|
||||||
= f.check_box :read_package_registry, class: 'form-check-input', data: { qa_selector: 'deploy_token_read_package_registry_checkbox' }
|
= f.check_box :read_package_registry, class: 'form-check-input', data: { qa_selector: 'deploy_token_read_package_registry_checkbox' }
|
||||||
= f.label :read_package_registry, 'read_package_registry', class: 'label-bold form-check-label'
|
= f.label :read_package_registry, 'read_package_registry', class: 'label-bold form-check-label'
|
||||||
.text-secondary= s_('DeployTokens|Allows read access to the package registry.')
|
.text-secondary= s_('DeployTokens|Allows read-only access to the package registry.')
|
||||||
|
|
||||||
%fieldset.form-group.form-check
|
%fieldset.form-group.form-check
|
||||||
= f.check_box :write_package_registry, class: 'form-check-input'
|
= f.check_box :write_package_registry, class: 'form-check-input'
|
||||||
= f.label :write_package_registry, 'write_package_registry', class: 'label-bold form-check-label'
|
= f.label :write_package_registry, 'write_package_registry', class: 'label-bold form-check-label'
|
||||||
.text-secondary= s_('DeployTokens|Allows write access to the package registry.')
|
.text-secondary= s_('DeployTokens|Allows read and write access to the package registry.')
|
||||||
|
|
||||||
.gl-mt-3
|
.gl-mt-3
|
||||||
= f.submit s_('DeployTokens|Create deploy token'), class: 'btn gl-button btn-confirm', data: { qa_selector: 'create_deploy_token_button' }
|
= f.submit s_('DeployTokens|Create deploy token'), class: 'btn gl-button btn-confirm', data: { qa_selector: 'create_deploy_token_button' }
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
- if @new_deploy_token.persisted?
|
- if @new_deploy_token.persisted?
|
||||||
= render 'shared/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token
|
= render 'shared/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token
|
||||||
%h5.gl-mt-0
|
%h5.gl-mt-0
|
||||||
= s_('DeployTokens|Add a deploy token')
|
= s_('DeployTokens|New deploy token')
|
||||||
= render 'shared/deploy_tokens/form', group_or_project: group_or_project, token: @new_deploy_token, presenter: @deploy_tokens
|
= render 'shared/deploy_tokens/form', group_or_project: group_or_project, token: @new_deploy_token, presenter: @deploy_tokens
|
||||||
%hr
|
%hr
|
||||||
= render 'shared/deploy_tokens/table', group_or_project: group_or_project, active_tokens: @deploy_tokens
|
= render 'shared/deploy_tokens/table', group_or_project: group_or_project, active_tokens: @deploy_tokens
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
---
|
|
||||||
name: allow_editing_commit_messages
|
|
||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49152/
|
|
||||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/290779
|
|
||||||
milestone: '13.7'
|
|
||||||
type: development
|
|
||||||
group:
|
|
||||||
default_enabled: false
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: diff_searching_usage_data
|
||||||
|
introduced_by_url:
|
||||||
|
rollout_issue_url:
|
||||||
|
milestone: '14.2'
|
||||||
|
type: development
|
||||||
|
group: group::code review
|
||||||
|
default_enabled: true
|
||||||
|
|
@ -65,6 +65,7 @@
|
||||||
- 'i_code_review_diff_multiple_files'
|
- 'i_code_review_diff_multiple_files'
|
||||||
- 'i_code_review_user_load_conflict_ui'
|
- 'i_code_review_user_load_conflict_ui'
|
||||||
- 'i_code_review_user_resolve_conflict'
|
- 'i_code_review_user_resolve_conflict'
|
||||||
|
- 'i_code_review_user_searches_diff'
|
||||||
- name: code_review_category_monthly_active_users
|
- name: code_review_category_monthly_active_users
|
||||||
operator: OR
|
operator: OR
|
||||||
source: redis
|
source: redis
|
||||||
|
|
@ -122,6 +123,7 @@
|
||||||
- 'i_code_review_diff_multiple_files'
|
- 'i_code_review_diff_multiple_files'
|
||||||
- 'i_code_review_user_load_conflict_ui'
|
- 'i_code_review_user_load_conflict_ui'
|
||||||
- 'i_code_review_user_resolve_conflict'
|
- 'i_code_review_user_resolve_conflict'
|
||||||
|
- 'i_code_review_user_searches_diff'
|
||||||
- name: code_review_extension_category_monthly_active_users
|
- name: code_review_extension_category_monthly_active_users
|
||||||
operator: OR
|
operator: OR
|
||||||
source: redis
|
source: redis
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
key_path: redis_hll_counters.code_review.i_code_review_user_searches_diff_monthly
|
||||||
|
description: Count of users who search merge request diffs
|
||||||
|
product_section: dev
|
||||||
|
product_stage: create
|
||||||
|
product_group: group::code review
|
||||||
|
product_category: code_review
|
||||||
|
value_type: number
|
||||||
|
status: implemented
|
||||||
|
milestone: '14.2'
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66522
|
||||||
|
time_frame: 28d
|
||||||
|
data_source: redis_hll
|
||||||
|
data_category: Optional
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
key_path: redis_hll_counters.code_review.i_code_review_user_searches_diff_weekly
|
||||||
|
description: Count of users who search merge request diffs
|
||||||
|
product_section: dev
|
||||||
|
product_stage: create
|
||||||
|
product_group: group::code review
|
||||||
|
product_category: code_review
|
||||||
|
value_type: number
|
||||||
|
status: implemented
|
||||||
|
milestone: '14.2'
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66522
|
||||||
|
time_frame: 7d
|
||||||
|
data_source: redis_hll
|
||||||
|
data_category: Optional
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
key_path: counts.diff_searches
|
||||||
|
description: Total count of merge request diff searches
|
||||||
|
product_section: dev
|
||||||
|
product_stage: create
|
||||||
|
product_group: group::code review
|
||||||
|
product_category: code_review
|
||||||
|
value_type: number
|
||||||
|
status: implemented
|
||||||
|
milestone: '14.2'
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66522
|
||||||
|
time_frame: all
|
||||||
|
data_source: redis
|
||||||
|
data_category: Optional
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
key_path: counts.in_product_marketing_email_team_short_0_cta_clicked
|
||||||
|
name: "count_clicks_on_the_first_email_of_the_team_short_track_for_in_product_marketing_emails"
|
||||||
|
description: Total clicks on the team_short track's first email
|
||||||
|
product_section:
|
||||||
|
product_stage: growth
|
||||||
|
product_group: group::activation
|
||||||
|
product_category: onboarding
|
||||||
|
value_type: number
|
||||||
|
status: implemented
|
||||||
|
milestone: "14.2"
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66854
|
||||||
|
time_frame: all
|
||||||
|
data_source: database
|
||||||
|
data_category: Optional
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
key_path: counts.in_product_marketing_email_team_short_0_sent
|
||||||
|
name: "count_sent_first_email_of_the_team_short_track_for_in_product_marketing_emails"
|
||||||
|
description: Total sent emails of the team_short track's first email
|
||||||
|
product_section:
|
||||||
|
product_stage: growth
|
||||||
|
product_group: group::activation
|
||||||
|
product_category: onboarding
|
||||||
|
value_type: number
|
||||||
|
status: implemented
|
||||||
|
milestone: "14.2"
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66854
|
||||||
|
time_frame: all
|
||||||
|
data_source: database
|
||||||
|
data_category: Optional
|
||||||
|
distribution:
|
||||||
|
- ce
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- free
|
||||||
|
- premium
|
||||||
|
- ultimate
|
||||||
|
|
@ -283,7 +283,14 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
||||||
resource :cycle_analytics, only: :show, path: 'value_stream_analytics'
|
resource :cycle_analytics, only: :show, path: 'value_stream_analytics'
|
||||||
scope module: :cycle_analytics, as: 'cycle_analytics', path: 'value_stream_analytics' do
|
scope module: :cycle_analytics, as: 'cycle_analytics', path: 'value_stream_analytics' do
|
||||||
resources :value_streams, only: [:index] do
|
resources :value_streams, only: [:index] do
|
||||||
resources :stages, only: [:index]
|
resources :stages, only: [:index] do
|
||||||
|
member do
|
||||||
|
get :median
|
||||||
|
get :average
|
||||||
|
get :records
|
||||||
|
get :count
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
resource :summary, controller: :summary, only: :show
|
resource :summary, controller: :summary, only: :show
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,10 @@ class AddAllowToEditCommitToProjectSettings < ActiveRecord::Migration[6.0]
|
||||||
DOWNTIME = false
|
DOWNTIME = false
|
||||||
|
|
||||||
def up
|
def up
|
||||||
with_lock_retries do
|
# no-op
|
||||||
add_column :project_settings, :allow_editing_commit_messages, :boolean, default: false, null: false
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def down
|
def down
|
||||||
with_lock_retries do
|
# no-op
|
||||||
remove_column :project_settings, :allow_editing_commit_messages
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,16 @@ Returns [`CiConfig`](#ciconfig).
|
||||||
| <a id="queryciconfigprojectpath"></a>`projectPath` | [`ID!`](#id) | The project of the CI config. |
|
| <a id="queryciconfigprojectpath"></a>`projectPath` | [`ID!`](#id) | The project of the CI config. |
|
||||||
| <a id="queryciconfigsha"></a>`sha` | [`String`](#string) | Sha for the pipeline. |
|
| <a id="queryciconfigsha"></a>`sha` | [`String`](#string) | Sha for the pipeline. |
|
||||||
|
|
||||||
|
### `Query.ciMinutesUsage`
|
||||||
|
|
||||||
|
The monthly CI minutes usage data for the current user.
|
||||||
|
|
||||||
|
Returns [`CiMinutesNamespaceMonthlyUsageConnection`](#ciminutesnamespacemonthlyusageconnection).
|
||||||
|
|
||||||
|
This field returns a [connection](#connections). It accepts the
|
||||||
|
four standard [pagination arguments](#connection-pagination-arguments):
|
||||||
|
`before: String`, `after: String`, `first: Int`, `last: Int`.
|
||||||
|
|
||||||
### `Query.containerRepository`
|
### `Query.containerRepository`
|
||||||
|
|
||||||
Find a container repository.
|
Find a container repository.
|
||||||
|
|
@ -2533,7 +2543,7 @@ Input type: `IssueSetDueDateInput`
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| ---- | ---- | ----------- |
|
| ---- | ---- | ----------- |
|
||||||
| <a id="mutationissuesetduedateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
| <a id="mutationissuesetduedateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||||
| <a id="mutationissuesetduedateduedate"></a>`dueDate` | [`Time`](#time) | The desired due date for the issue, due date will be removed if absent or set to null. |
|
| <a id="mutationissuesetduedateduedate"></a>`dueDate` | [`Time`](#time) | The desired due date for the issue. Due date is removed if null. |
|
||||||
| <a id="mutationissuesetduedateiid"></a>`iid` | [`String!`](#string) | The IID of the issue to mutate. |
|
| <a id="mutationissuesetduedateiid"></a>`iid` | [`String!`](#string) | The IID of the issue to mutate. |
|
||||||
| <a id="mutationissuesetduedateprojectpath"></a>`projectPath` | [`ID!`](#id) | The project the issue to mutate is in. |
|
| <a id="mutationissuesetduedateprojectpath"></a>`projectPath` | [`ID!`](#id) | The project the issue to mutate is in. |
|
||||||
|
|
||||||
|
|
@ -4876,6 +4886,52 @@ The edge type for [`CiJob`](#cijob).
|
||||||
| <a id="cijobedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
|
| <a id="cijobedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
|
||||||
| <a id="cijobedgenode"></a>`node` | [`CiJob`](#cijob) | The item at the end of the edge. |
|
| <a id="cijobedgenode"></a>`node` | [`CiJob`](#cijob) | The item at the end of the edge. |
|
||||||
|
|
||||||
|
#### `CiMinutesNamespaceMonthlyUsageConnection`
|
||||||
|
|
||||||
|
The connection type for [`CiMinutesNamespaceMonthlyUsage`](#ciminutesnamespacemonthlyusage).
|
||||||
|
|
||||||
|
##### Fields
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| ---- | ---- | ----------- |
|
||||||
|
| <a id="ciminutesnamespacemonthlyusageconnectionedges"></a>`edges` | [`[CiMinutesNamespaceMonthlyUsageEdge]`](#ciminutesnamespacemonthlyusageedge) | A list of edges. |
|
||||||
|
| <a id="ciminutesnamespacemonthlyusageconnectionnodes"></a>`nodes` | [`[CiMinutesNamespaceMonthlyUsage]`](#ciminutesnamespacemonthlyusage) | A list of nodes. |
|
||||||
|
| <a id="ciminutesnamespacemonthlyusageconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
|
||||||
|
|
||||||
|
#### `CiMinutesNamespaceMonthlyUsageEdge`
|
||||||
|
|
||||||
|
The edge type for [`CiMinutesNamespaceMonthlyUsage`](#ciminutesnamespacemonthlyusage).
|
||||||
|
|
||||||
|
##### Fields
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| ---- | ---- | ----------- |
|
||||||
|
| <a id="ciminutesnamespacemonthlyusageedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
|
||||||
|
| <a id="ciminutesnamespacemonthlyusageedgenode"></a>`node` | [`CiMinutesNamespaceMonthlyUsage`](#ciminutesnamespacemonthlyusage) | The item at the end of the edge. |
|
||||||
|
|
||||||
|
#### `CiMinutesProjectMonthlyUsageConnection`
|
||||||
|
|
||||||
|
The connection type for [`CiMinutesProjectMonthlyUsage`](#ciminutesprojectmonthlyusage).
|
||||||
|
|
||||||
|
##### Fields
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| ---- | ---- | ----------- |
|
||||||
|
| <a id="ciminutesprojectmonthlyusageconnectionedges"></a>`edges` | [`[CiMinutesProjectMonthlyUsageEdge]`](#ciminutesprojectmonthlyusageedge) | A list of edges. |
|
||||||
|
| <a id="ciminutesprojectmonthlyusageconnectionnodes"></a>`nodes` | [`[CiMinutesProjectMonthlyUsage]`](#ciminutesprojectmonthlyusage) | A list of nodes. |
|
||||||
|
| <a id="ciminutesprojectmonthlyusageconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
|
||||||
|
|
||||||
|
#### `CiMinutesProjectMonthlyUsageEdge`
|
||||||
|
|
||||||
|
The edge type for [`CiMinutesProjectMonthlyUsage`](#ciminutesprojectmonthlyusage).
|
||||||
|
|
||||||
|
##### Fields
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| ---- | ---- | ----------- |
|
||||||
|
| <a id="ciminutesprojectmonthlyusageedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
|
||||||
|
| <a id="ciminutesprojectmonthlyusageedgenode"></a>`node` | [`CiMinutesProjectMonthlyUsage`](#ciminutesprojectmonthlyusage) | The item at the end of the edge. |
|
||||||
|
|
||||||
#### `CiRunnerConnection`
|
#### `CiRunnerConnection`
|
||||||
|
|
||||||
The connection type for [`CiRunner`](#cirunner).
|
The connection type for [`CiRunner`](#cirunner).
|
||||||
|
|
@ -7858,6 +7914,25 @@ Represents the total number of issues and their weights for a particular day.
|
||||||
| ---- | ---- | ----------- |
|
| ---- | ---- | ----------- |
|
||||||
| <a id="cijobtokenscopetypeprojects"></a>`projects` | [`ProjectConnection!`](#projectconnection) | Allow list of projects that can be accessed by CI Job tokens created by this project. (see [Connections](#connections)) |
|
| <a id="cijobtokenscopetypeprojects"></a>`projects` | [`ProjectConnection!`](#projectconnection) | Allow list of projects that can be accessed by CI Job tokens created by this project. (see [Connections](#connections)) |
|
||||||
|
|
||||||
|
### `CiMinutesNamespaceMonthlyUsage`
|
||||||
|
|
||||||
|
#### Fields
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| ---- | ---- | ----------- |
|
||||||
|
| <a id="ciminutesnamespacemonthlyusageminutes"></a>`minutes` | [`Int`](#int) | The total number of minutes used by all projects in the namespace. |
|
||||||
|
| <a id="ciminutesnamespacemonthlyusagemonth"></a>`month` | [`String`](#string) | The month related to the usage data. |
|
||||||
|
| <a id="ciminutesnamespacemonthlyusageprojects"></a>`projects` | [`CiMinutesProjectMonthlyUsageConnection`](#ciminutesprojectmonthlyusageconnection) | CI minutes usage data for projects in the namespace. (see [Connections](#connections)) |
|
||||||
|
|
||||||
|
### `CiMinutesProjectMonthlyUsage`
|
||||||
|
|
||||||
|
#### Fields
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| ---- | ---- | ----------- |
|
||||||
|
| <a id="ciminutesprojectmonthlyusageminutes"></a>`minutes` | [`Int`](#int) | The number of CI minutes used by the project in the month. |
|
||||||
|
| <a id="ciminutesprojectmonthlyusagename"></a>`name` | [`String`](#string) | The name of the project. |
|
||||||
|
|
||||||
### `CiRunner`
|
### `CiRunner`
|
||||||
|
|
||||||
#### Fields
|
#### Fields
|
||||||
|
|
|
||||||
|
|
@ -3277,7 +3277,7 @@ dashboards.
|
||||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207528) in GitLab 13.0.
|
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207528) in GitLab 13.0.
|
||||||
> - Requires [GitLab Runner](https://docs.gitlab.com/runner/) 11.5 and above.
|
> - Requires [GitLab Runner](https://docs.gitlab.com/runner/) 11.5 and above.
|
||||||
|
|
||||||
The `terraform` report obtains a Terraform `tfplan.json` file. [JQ processing required to remove credentials](../../user/infrastructure/mr_integration.md#setup). The collected Terraform
|
The `terraform` report obtains a Terraform `tfplan.json` file. [JQ processing required to remove credentials](../../user/infrastructure/mr_integration.md#configure-terraform-report-artifacts). The collected Terraform
|
||||||
plan report uploads to GitLab as an artifact and displays
|
plan report uploads to GitLab as an artifact and displays
|
||||||
in merge requests. For more information, see
|
in merge requests. For more information, see
|
||||||
[Output `terraform plan` information into a merge request](../../user/infrastructure/mr_integration.md).
|
[Output `terraform plan` information into a merge request](../../user/infrastructure/mr_integration.md).
|
||||||
|
|
|
||||||
|
|
@ -1366,6 +1366,31 @@ argument :my_arg, GraphQL::Types::String,
|
||||||
description: "A description of the argument."
|
description: "A description of the argument."
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Nullability
|
||||||
|
|
||||||
|
Arguments can be marked as `required: true` which means the value must be present and not `null`.
|
||||||
|
If a required argument's value can be `null`, use the `required: :nullable` declaration.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
argument :due_date,
|
||||||
|
Types::TimeType,
|
||||||
|
required: :nullable,
|
||||||
|
description: 'The desired due date for the issue. Due date is removed if null.'
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above example, the `due_date` argument must be given, but unlike the GraphQL spec, the value can be `null`.
|
||||||
|
This allows 'unsetting' the due date in a single mutation rather than creating a new mutation for removing the due date.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
{ due_date: null } # => OK
|
||||||
|
{ due_date: "2025-01-10" } # => OK
|
||||||
|
{ } # => invalid (not given)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Keywords
|
||||||
|
|
||||||
Each GraphQL `argument` defined is passed to the `#resolve` method
|
Each GraphQL `argument` defined is passed to the `#resolve` method
|
||||||
of a mutation as keyword arguments.
|
of a mutation as keyword arguments.
|
||||||
|
|
||||||
|
|
@ -1377,6 +1402,8 @@ def resolve(my_arg:)
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Input Types
|
||||||
|
|
||||||
`graphql-ruby` wraps up arguments into an
|
`graphql-ruby` wraps up arguments into an
|
||||||
[input type](https://graphql.org/learn/schema/#input-types).
|
[input type](https://graphql.org/learn/schema/#input-types).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2638,6 +2638,34 @@ Status: `data_available`
|
||||||
|
|
||||||
Tiers: `free`, `premium`, `ultimate`
|
Tiers: `free`, `premium`, `ultimate`
|
||||||
|
|
||||||
|
### `counts.in_product_marketing_email_team_short_0_cta_clicked`
|
||||||
|
|
||||||
|
Total clicks on the team_short track's first email
|
||||||
|
|
||||||
|
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210727095918_in_product_marketing_email_team_short_0_cta_clicked.yml)
|
||||||
|
|
||||||
|
Group: `group::activation`
|
||||||
|
|
||||||
|
Data Category: `Optional`
|
||||||
|
|
||||||
|
Status: `implemented`
|
||||||
|
|
||||||
|
Tiers: `free`, `premium`, `ultimate`
|
||||||
|
|
||||||
|
### `counts.in_product_marketing_email_team_short_0_sent`
|
||||||
|
|
||||||
|
Total sent emails of the team_short track's first email
|
||||||
|
|
||||||
|
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210727095923_in_product_marketing_email_team_short_0_sent.yml)
|
||||||
|
|
||||||
|
Group: `group::activation`
|
||||||
|
|
||||||
|
Data Category: `Optional`
|
||||||
|
|
||||||
|
Status: `implemented`
|
||||||
|
|
||||||
|
Tiers: `free`, `premium`, `ultimate`
|
||||||
|
|
||||||
### `counts.in_product_marketing_email_trial_0_cta_clicked`
|
### `counts.in_product_marketing_email_trial_0_cta_clicked`
|
||||||
|
|
||||||
Total clicks on the verify trial's first email
|
Total clicks on the verify trial's first email
|
||||||
|
|
@ -7650,6 +7678,20 @@ Status: `data_available`
|
||||||
|
|
||||||
Tiers: `free`, `premium`, `ultimate`
|
Tiers: `free`, `premium`, `ultimate`
|
||||||
|
|
||||||
|
### `counts.user_searches_diffs`
|
||||||
|
|
||||||
|
Count of users who search merge request diffs
|
||||||
|
|
||||||
|
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210723075525_user_searches_diffs.yml)
|
||||||
|
|
||||||
|
Group: `group::code review`
|
||||||
|
|
||||||
|
Data Category: `Optional`
|
||||||
|
|
||||||
|
Status: `implemented`
|
||||||
|
|
||||||
|
Tiers: `free`, `premium`, `ultimate`
|
||||||
|
|
||||||
### `counts.web_hooks`
|
### `counts.web_hooks`
|
||||||
|
|
||||||
Missing description
|
Missing description
|
||||||
|
|
@ -10646,6 +10688,20 @@ Status: `data_available`
|
||||||
|
|
||||||
Tiers: `free`, `premium`, `ultimate`
|
Tiers: `free`, `premium`, `ultimate`
|
||||||
|
|
||||||
|
### `redis_hll_counters.code_review.i_code_review_searches_in_diff_monthly`
|
||||||
|
|
||||||
|
Count of searches in merge request diffs
|
||||||
|
|
||||||
|
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210722132444_i_code_review_searches_in_diff_monthly.yml)
|
||||||
|
|
||||||
|
Group: `group::code review`
|
||||||
|
|
||||||
|
Data Category: `Optional`
|
||||||
|
|
||||||
|
Status: `implemented`
|
||||||
|
|
||||||
|
Tiers: `free`, `premium`, `ultimate`
|
||||||
|
|
||||||
### `redis_hll_counters.code_review.i_code_review_user_add_suggestion_monthly`
|
### `redis_hll_counters.code_review.i_code_review_user_add_suggestion_monthly`
|
||||||
|
|
||||||
Count of unique users per month who added a suggestion
|
Count of unique users per month who added a suggestion
|
||||||
|
|
@ -11514,6 +11570,20 @@ Status: `data_available`
|
||||||
|
|
||||||
Tiers: `free`, `premium`, `ultimate`
|
Tiers: `free`, `premium`, `ultimate`
|
||||||
|
|
||||||
|
### `redis_hll_counters.code_review.i_code_review_user_searches_diff_monthly`
|
||||||
|
|
||||||
|
Count of users who search merge request diffs
|
||||||
|
|
||||||
|
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210720144005_i_code_review_user_searches_diff_monthly.yml)
|
||||||
|
|
||||||
|
Group: `group::code review`
|
||||||
|
|
||||||
|
Data Category: `Optional`
|
||||||
|
|
||||||
|
Status: `implemented`
|
||||||
|
|
||||||
|
Tiers: `free`, `premium`, `ultimate`
|
||||||
|
|
||||||
### `redis_hll_counters.code_review.i_code_review_user_single_file_diffs_monthly`
|
### `redis_hll_counters.code_review.i_code_review_user_single_file_diffs_monthly`
|
||||||
|
|
||||||
Count of unique users per month with diffs viewed file by file
|
Count of unique users per month with diffs viewed file by file
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
---
|
||||||
|
stage: Create
|
||||||
|
group: Ecosystem
|
||||||
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||||
|
---
|
||||||
|
|
||||||
|
# Configure the Jira integration in GitLab
|
||||||
|
|
||||||
|
You can set up the [Jira integration](index.md#jira-integration)
|
||||||
|
by configuring your project settings in GitLab.
|
||||||
|
You can also configure these settings at a [group level](../../user/admin_area/settings/project_integration_management.md#manage-group-level-default-settings-for-a-project-integration),
|
||||||
|
and for self-managed GitLab, at an [instance level](../../user/admin_area/settings/project_integration_management.md#manage-instance-level-default-settings-for-a-project-integration).
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- Ensure your GitLab installation does not use a [relative URL](development_panel.md#limitations).
|
||||||
|
- For **Jira Server**, ensure you have a Jira username and password.
|
||||||
|
See [authentication in Jira](index.md#authentication-in-jira).
|
||||||
|
- For **Jira on Atlassian cloud**, ensure you have an API token
|
||||||
|
and the email address you used to create the token.
|
||||||
|
See [authentication in Jira](index.md#authentication-in-jira).
|
||||||
|
|
||||||
|
To configure your project:
|
||||||
|
|
||||||
|
1. Go to your project and select [**Settings > Integrations**](../../user/project/integrations/overview.md#accessing-integrations).
|
||||||
|
1. Select **Jira**.
|
||||||
|
1. Select **Enable integration**.
|
||||||
|
1. Select **Trigger** actions. Your choice determines whether a mention of Jira issue
|
||||||
|
(in a GitLab commit, merge request, or both) creates a cross-link in Jira back to GitLab.
|
||||||
|
1. To comment in the Jira issue when a **Trigger** action is made in GitLab, select
|
||||||
|
**Enable comments**.
|
||||||
|
1. To transition Jira issues when a
|
||||||
|
[closing reference](../../user/project/issues/managing_issues.md#closing-issues-automatically)
|
||||||
|
is made in GitLab, select **Enable Jira transitions**.
|
||||||
|
1. Provide Jira configuration information:
|
||||||
|
- **Web URL**: The base URL to the Jira instance web interface you're linking to
|
||||||
|
this GitLab project, such as `https://jira.example.com`.
|
||||||
|
- **Jira API URL**: The base URL to the Jira instance API, such as `https://jira-api.example.com`.
|
||||||
|
Defaults to the **Web URL** value if not set. Leave blank if using **Jira on Atlassian cloud**.
|
||||||
|
- **Username or Email**:
|
||||||
|
For **Jira Server**, use `username`. For **Jira on Atlassian cloud**, use `email`.
|
||||||
|
- **Password/API token**:
|
||||||
|
Use `password` for **Jira Server** or `API token` for **Jira on Atlassian cloud**.
|
||||||
|
1. To enable users to view Jira issues inside the GitLab project **(PREMIUM)**, select **Enable Jira issues** and
|
||||||
|
enter a Jira project key.
|
||||||
|
|
||||||
|
You can display issues only from a single Jira project in a given GitLab project.
|
||||||
|
|
||||||
|
WARNING:
|
||||||
|
If you enable Jira issues with this setting, all users with access to this GitLab project
|
||||||
|
can view all issues from the specified Jira project.
|
||||||
|
|
||||||
|
1. To enable issue creation for vulnerabilities **(ULTIMATE)**, select **Enable Jira issues creation from vulnerabilities**.
|
||||||
|
1. Select the **Jira issue type**. If the dropdown is empty, select refresh (**{retry}**) and try again.
|
||||||
|
1. To verify the Jira connection is working, select **Test settings**.
|
||||||
|
1. Select **Save changes**.
|
||||||
|
|
||||||
|
Your GitLab project can now interact with all Jira projects in your instance and the project now
|
||||||
|
displays a Jira link that opens the Jira project.
|
||||||
|
|
@ -68,8 +68,8 @@ To simplify administration, we recommend that a GitLab group maintainer or group
|
||||||
|
|
||||||
| Jira usage | GitLab.com customers need | GitLab self-managed customers need |
|
| Jira usage | GitLab.com customers need | GitLab self-managed customers need |
|
||||||
|------------|---------------------------|------------------------------------|
|
|------------|---------------------------|------------------------------------|
|
||||||
| [Atlassian cloud](https://www.atlassian.com/cloud) | The [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview) application installed from the [Atlassian Marketplace](https://marketplace.atlassian.com). This offers real-time sync between GitLab and Jira. | The [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview), using a workaround process. See the documentation for [installing the GitLab Jira Cloud application for self-managed instances](connect-app.md#install-the-gitlabcom-for-jira-cloud-app-for-self-managed-instances) for more information. |
|
| [Atlassian cloud](https://www.atlassian.com/cloud) | The [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview) application installed from the [Atlassian Marketplace](https://marketplace.atlassian.com). This offers real-time sync between GitLab and Jira. For more information, see the documentation for the [GitLab.com for Jira Cloud app](connect-app.md). | The [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview), using a workaround process. See the documentation for [installing the GitLab Jira Cloud application for self-managed instances](connect-app.md#install-the-gitlabcom-for-jira-cloud-app-for-self-managed-instances) for more information. |
|
||||||
| Your own server | The Jira DVCS (distributed version control system) connector. This syncs data hourly. | The [Jira DVCS Connector](dvcs.md). |
|
| Your own server | The [Jira DVCS (distributed version control system) connector](dvcs.md). This syncs data hourly. | The [Jira DVCS Connector](dvcs.md). |
|
||||||
|
|
||||||
Each GitLab project can be configured to connect to an entire Jira instance. That means after
|
Each GitLab project can be configured to connect to an entire Jira instance. That means after
|
||||||
configuration, one GitLab project can interact with all Jira projects in that instance. For:
|
configuration, one GitLab project can interact with all Jira projects in that instance. For:
|
||||||
|
|
@ -82,57 +82,6 @@ configuration, one GitLab project can interact with all Jira projects in that in
|
||||||
If you have a single Jira instance, you can pre-fill the settings. For more information, read the
|
If you have a single Jira instance, you can pre-fill the settings. For more information, read the
|
||||||
documentation for [central administration of project integrations](../../user/admin_area/settings/project_integration_management.md).
|
documentation for [central administration of project integrations](../../user/admin_area/settings/project_integration_management.md).
|
||||||
|
|
||||||
To enable the integration in GitLab, you must:
|
|
||||||
|
|
||||||
1. [Configure the project in Jira](index.md#jira-integration).
|
|
||||||
The supported Jira versions are `v6.x`, `v7.x`, and `v8.x`.
|
|
||||||
1. [Enter the correct values in GitLab](#configure-gitlab).
|
|
||||||
|
|
||||||
### Configure GitLab
|
|
||||||
|
|
||||||
To enable the integration in your GitLab project, after you
|
|
||||||
[configure your Jira project](index.md#jira-integration):
|
|
||||||
|
|
||||||
1. Ensure your GitLab installation does not use a relative URL, as described in
|
|
||||||
[Limitations](#limitations).
|
|
||||||
1. Go to your project and select [**Settings > Integrations**](../../user/project/integrations/overview.md#accessing-integrations).
|
|
||||||
1. Select **Jira**.
|
|
||||||
1. Select **Enable integration**.
|
|
||||||
1. Select **Trigger** actions. Your choice determines whether a mention of Jira issue
|
|
||||||
(in a GitLab commit, merge request, or both) creates a cross-link in Jira back to GitLab.
|
|
||||||
1. To comment in the Jira issue when a **Trigger** action is made in GitLab, select
|
|
||||||
**Enable comments**.
|
|
||||||
1. To transition Jira issues when a
|
|
||||||
[closing reference](../../user/project/issues/managing_issues.md#closing-issues-automatically)
|
|
||||||
is made in GitLab, select **Enable Jira transitions**.
|
|
||||||
1. Provide Jira configuration information:
|
|
||||||
- **Web URL**: The base URL to the Jira instance web interface you're linking to
|
|
||||||
this GitLab project, such as `https://jira.example.com`.
|
|
||||||
- **Jira API URL**: The base URL to the Jira instance API, such as `https://jira-api.example.com`.
|
|
||||||
Defaults to the **Web URL** value if not set. Leave blank if using **Jira on Atlassian cloud**.
|
|
||||||
- **Username or Email**:
|
|
||||||
For **Jira Server**, use `username`. For **Jira on Atlassian cloud**, use `email`.
|
|
||||||
See [authentication in Jira](index.md#authentication-in-jira).
|
|
||||||
- **Password/API token**:
|
|
||||||
Use `password` for **Jira Server** or `API token` for **Jira on Atlassian cloud**.
|
|
||||||
See [authentication in Jira](index.md#authentication-in-jira).
|
|
||||||
1. To enable users to view Jira issues inside the GitLab project **(PREMIUM)**, select **Enable Jira issues** and
|
|
||||||
enter a Jira project key.
|
|
||||||
|
|
||||||
You can display issues only from a single Jira project in a given GitLab project.
|
|
||||||
|
|
||||||
WARNING:
|
|
||||||
If you enable Jira issues with this setting, all users with access to this GitLab project
|
|
||||||
can view all issues from the specified Jira project.
|
|
||||||
|
|
||||||
1. To enable issue creation for vulnerabilities **(ULTIMATE)**, select **Enable Jira issues creation from vulnerabilities**.
|
|
||||||
1. Select the **Jira issue type**. If the dropdown is empty, select refresh (**{retry}**) and try again.
|
|
||||||
1. To verify the Jira connection is working, select **Test settings**.
|
|
||||||
1. Select **Save changes**.
|
|
||||||
|
|
||||||
Your GitLab project can now interact with all Jira projects in your instance and the project now
|
|
||||||
displays a Jira link that opens the Jira project.
|
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
This integration is not supported on GitLab instances under a
|
This integration is not supported on GitLab instances under a
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ The supported Jira versions are `v6.x`, `v7.x`, and `v8.x`.
|
||||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||||
For an overview, see [Agile Management - GitLab-Jira Basic Integration](https://www.youtube.com/watch?v=fWvwkx5_00E&feature=youtu.be).
|
For an overview, see [Agile Management - GitLab-Jira Basic Integration](https://www.youtube.com/watch?v=fWvwkx5_00E&feature=youtu.be).
|
||||||
|
|
||||||
To set up the integration, [configure the project settings](development_panel.md#configure-gitlab) in GitLab.
|
To set up the integration, [configure the project settings](configure.md) in GitLab.
|
||||||
You can also configure these settings at a [group level](../../user/admin_area/settings/project_integration_management.md#manage-group-level-default-settings-for-a-project-integration),
|
You can also configure these settings at a [group level](../../user/admin_area/settings/project_integration_management.md#manage-group-level-default-settings-for-a-project-integration),
|
||||||
and for self-managed GitLab, at an [instance level](../../user/admin_area/settings/project_integration_management.md#manage-instance-level-default-settings-for-a-project-integration).
|
and for self-managed GitLab, at an [instance level](../../user/admin_area/settings/project_integration_management.md#manage-instance-level-default-settings-for-a-project-integration).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
# Jira integration issue management **(FREE)**
|
# Jira integration issue management **(FREE)**
|
||||||
|
|
||||||
Integrating issue management with Jira requires you to [configure Jira](index.md#jira-integration)
|
Integrating issue management with Jira requires you to [configure Jira](index.md#jira-integration)
|
||||||
and [enable the Jira service](development_panel.md#configure-gitlab) in GitLab.
|
and [enable the Jira integration](configure.md) in GitLab.
|
||||||
After you configure and enable the integration, you can reference and close Jira
|
After you configure and enable the integration, you can reference and close Jira
|
||||||
issues by mentioning the Jira ID in GitLab commits and merge requests.
|
issues by mentioning the Jira ID in GitLab commits and merge requests.
|
||||||
|
|
||||||
|
|
@ -102,7 +102,7 @@ Consider this example:
|
||||||
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3622) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
|
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3622) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
|
||||||
|
|
||||||
You can browse, search, and view issues from a selected Jira project directly in GitLab,
|
You can browse, search, and view issues from a selected Jira project directly in GitLab,
|
||||||
if your GitLab administrator [has configured it](development_panel.md#configure-gitlab).
|
if your GitLab administrator [has configured it](configure.md).
|
||||||
|
|
||||||
To do this, in GitLab, go to your project and select **Jira > Issues list**. The issue list
|
To do this, in GitLab, go to your project and select **Jira > Issues list**. The issue list
|
||||||
sorts by **Created date** by default, with the newest issues listed at the top:
|
sorts by **Created date** by default, with the newest issues listed at the top:
|
||||||
|
|
@ -149,7 +149,7 @@ When you configure automatic issue transitions, you can transition a referenced
|
||||||
Jira issue to the next available status with a category of **Done**. To configure
|
Jira issue to the next available status with a category of **Done**. To configure
|
||||||
this setting:
|
this setting:
|
||||||
|
|
||||||
1. Refer to the [Configure GitLab](development_panel.md#configure-gitlab) instructions.
|
1. Refer to the [Configure GitLab](configure.md) instructions.
|
||||||
1. Select the **Enable Jira transitions** check box.
|
1. Select the **Enable Jira transitions** check box.
|
||||||
1. Select the **Move to Done** option.
|
1. Select the **Move to Done** option.
|
||||||
|
|
||||||
|
|
@ -167,7 +167,7 @@ For advanced workflows, you can specify custom Jira transition IDs:
|
||||||
**action** parameter in the URL.
|
**action** parameter in the URL.
|
||||||
The transition ID may vary between workflows (for example, a bug instead of a
|
The transition ID may vary between workflows (for example, a bug instead of a
|
||||||
story), even if the status you're changing to is the same.
|
story), even if the status you're changing to is the same.
|
||||||
1. Refer to the [Configure GitLab](development_panel.md#configure-gitlab) instructions.
|
1. Refer to the [Configure GitLab](configure.md) instructions.
|
||||||
1. Select the **Enable Jira transitions** setting.
|
1. Select the **Enable Jira transitions** setting.
|
||||||
1. Select the **Custom transitions** option.
|
1. Select the **Custom transitions** option.
|
||||||
1. Enter your transition IDs in the text field. If you insert multiple transition IDs
|
1. Enter your transition IDs in the text field. If you insert multiple transition IDs
|
||||||
|
|
@ -179,7 +179,7 @@ For advanced workflows, you can specify custom Jira transition IDs:
|
||||||
GitLab can cross-link source commits or merge requests with Jira issues without
|
GitLab can cross-link source commits or merge requests with Jira issues without
|
||||||
adding a comment to the Jira issue:
|
adding a comment to the Jira issue:
|
||||||
|
|
||||||
1. Refer to the [Configure GitLab](development_panel.md#configure-gitlab) instructions.
|
1. Refer to the [Configure GitLab](configure.md) instructions.
|
||||||
1. Clear the **Enable comments** check box.
|
1. Clear the **Enable comments** check box.
|
||||||
|
|
||||||
## Enable or disable the ability to require an associated Jira issue on merge requests
|
## Enable or disable the ability to require an associated Jira issue on merge requests
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ on Atlassian cloud. To create the API token:
|
||||||
1. Select **Create API token** to display a modal window with an API token.
|
1. Select **Create API token** to display a modal window with an API token.
|
||||||
1. To copy the API token, select **Copy to clipboard**, or select **View** and write
|
1. To copy the API token, select **Copy to clipboard**, or select **View** and write
|
||||||
down the new API token. You need this value when you
|
down the new API token. You need this value when you
|
||||||
[configure GitLab](development_panel.md#configure-gitlab).
|
[configure GitLab](configure.md).
|
||||||
|
|
||||||
You need the newly created token, and the email
|
You need the newly created token, and the email
|
||||||
address you used when you created it, when you
|
address you used when you created it, when you
|
||||||
[configure GitLab](development_panel.md#configure-gitlab).
|
[configure GitLab](configure.md).
|
||||||
|
|
|
||||||
|
|
@ -80,4 +80,4 @@ After creating the group in Jira, grant permissions to the group by creating a p
|
||||||
|
|
||||||
Write down the new Jira username and its
|
Write down the new Jira username and its
|
||||||
password, as you need them when
|
password, as you need them when
|
||||||
[configuring GitLab](development_panel.md#configure-gitlab).
|
[configuring GitLab](configure.md).
|
||||||
|
|
|
||||||
|
|
@ -67,12 +67,6 @@ Amazon S3 or Google Cloud Storage. Its features include:
|
||||||
|
|
||||||
Read more on setting up and [using GitLab Managed Terraform states](../terraform_state.md)
|
Read more on setting up and [using GitLab Managed Terraform states](../terraform_state.md)
|
||||||
|
|
||||||
WARNING:
|
|
||||||
Like any other job artifact, Terraform plan data is [viewable by anyone with Guest access](../../permissions.md) to the repository.
|
|
||||||
Neither Terraform nor GitLab encrypts the plan file by default. If your Terraform plan
|
|
||||||
includes sensitive data such as passwords, access tokens, or certificates, GitLab strongly
|
|
||||||
recommends encrypting plan output or modifying the project visibility settings.
|
|
||||||
|
|
||||||
## Terraform module registry
|
## Terraform module registry
|
||||||
|
|
||||||
GitLab can be used as a [Terraform module registry](../../packages/terraform_module_registry/index.md)
|
GitLab can be used as a [Terraform module registry](../../packages/terraform_module_registry/index.md)
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,17 @@ you can expose details from `terraform plan` runs directly into a merge request
|
||||||
enabling you to see statistics about the resources that Terraform creates,
|
enabling you to see statistics about the resources that Terraform creates,
|
||||||
modifies, or destroys.
|
modifies, or destroys.
|
||||||
|
|
||||||
## Setup
|
WARNING:
|
||||||
|
Like any other job artifact, Terraform Plan data is [viewable by anyone with Guest access](../permissions.md) to the repository.
|
||||||
|
Neither Terraform nor GitLab encrypts the plan file by default. If your Terraform Plan
|
||||||
|
includes sensitive data such as passwords, access tokens, or certificates, we strongly
|
||||||
|
recommend encrypting plan output or modifying the project visibility settings.
|
||||||
|
|
||||||
|
## Configure Terraform report artifacts
|
||||||
|
|
||||||
NOTE:
|
|
||||||
GitLab ships with a [pre-built CI template](iac/index.md#quick-start) that uses GitLab Managed Terraform state and integrates Terraform changes into merge requests. We recommend customizing the pre-built image and relying on the `gitlab-terraform` helper provided within for a quick setup.
|
GitLab ships with a [pre-built CI template](iac/index.md#quick-start) that uses GitLab Managed Terraform state and integrates Terraform changes into merge requests. We recommend customizing the pre-built image and relying on the `gitlab-terraform` helper provided within for a quick setup.
|
||||||
|
|
||||||
To manually configure a GitLab Terraform Report artifact requires the following steps:
|
To manually configure a GitLab Terraform Report artifact:
|
||||||
|
|
||||||
1. For simplicity, let's define a few reusable variables to allow us to
|
1. For simplicity, let's define a few reusable variables to allow us to
|
||||||
refer to these files multiple times:
|
refer to these files multiple times:
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ module API
|
||||||
optional :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)'
|
optional :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)'
|
||||||
optional :expires_at, type: DateTime, desc: 'Date string in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`)'
|
optional :expires_at, type: DateTime, desc: 'Date string in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`)'
|
||||||
end
|
end
|
||||||
put ":id/invitations/:email", requirements: { email: /[^\/]+/ } do
|
put ":id/invitations/:email", requirements: { email: %r{[^/]+} } do
|
||||||
source = find_source(source_type, params.delete(:id))
|
source = find_source(source_type, params.delete(:id))
|
||||||
invite_email = params[:email]
|
invite_email = params[:email]
|
||||||
authorize_admin_source!(source_type, source)
|
authorize_admin_source!(source_type, source)
|
||||||
|
|
@ -88,7 +88,7 @@ module API
|
||||||
params do
|
params do
|
||||||
requires :email, type: String, desc: 'The email address of the invitation'
|
requires :email, type: String, desc: 'The email address of the invitation'
|
||||||
end
|
end
|
||||||
delete ":id/invitations/:email", requirements: { email: /[^\/]+/ } do
|
delete ":id/invitations/:email", requirements: { email: %r{[^/]+} } do
|
||||||
source = find_source(source_type, params[:id])
|
source = find_source(source_type, params[:id])
|
||||||
invite_email = params[:email]
|
invite_email = params[:email]
|
||||||
authorize_admin_source!(source_type, source)
|
authorize_admin_source!(source_type, source)
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,43 @@
|
||||||
|
|
||||||
module Backup
|
module Backup
|
||||||
Error = Class.new(StandardError)
|
Error = Class.new(StandardError)
|
||||||
|
|
||||||
|
class FileBackupError < Backup::Error
|
||||||
|
attr_reader :app_files_dir, :backup_tarball
|
||||||
|
|
||||||
|
def initialize(app_files_dir, backup_tarball)
|
||||||
|
@app_files_dir = app_files_dir
|
||||||
|
@backup_tarball = backup_tarball
|
||||||
|
end
|
||||||
|
|
||||||
|
def message
|
||||||
|
"Failed to create compressed file '#{backup_tarball}' when trying to backup the following paths: '#{app_files_dir}'"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class RepositoryBackupError < Backup::Error
|
||||||
|
attr_reader :container, :backup_repos_path
|
||||||
|
|
||||||
|
def initialize(container, backup_repos_path)
|
||||||
|
@container = container
|
||||||
|
@backup_repos_path = backup_repos_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def message
|
||||||
|
"Failed to create compressed file '#{backup_repos_path}' when trying to backup the following paths: '#{container.disk_path}'"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class DatabaseBackupError < Backup::Error
|
||||||
|
attr_reader :config, :db_file_name
|
||||||
|
|
||||||
|
def initialize(config, db_file_name)
|
||||||
|
@config = config
|
||||||
|
@db_file_name = db_file_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def message
|
||||||
|
"Failed to create compressed file '#{db_file_name}' when trying to backup the main database:\n - host: '#{config[:host]}'\n - port: '#{config[:port]}'\n - database: '#{config[:database]}'"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Analytics
|
||||||
|
module CycleAnalytics
|
||||||
|
class RequestParams
|
||||||
|
include ActiveModel::Model
|
||||||
|
include ActiveModel::Validations
|
||||||
|
include ActiveModel::Attributes
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
|
MAX_RANGE_DAYS = 180.days.freeze
|
||||||
|
DEFAULT_DATE_RANGE = 29.days # 30 including Date.today
|
||||||
|
|
||||||
|
STRONG_PARAMS_DEFINITION = [
|
||||||
|
:created_before,
|
||||||
|
:created_after,
|
||||||
|
:author_username,
|
||||||
|
:milestone_title,
|
||||||
|
:sort,
|
||||||
|
:direction,
|
||||||
|
:page,
|
||||||
|
:stage_id,
|
||||||
|
:end_event_filter,
|
||||||
|
label_name: [].freeze,
|
||||||
|
assignee_username: [].freeze,
|
||||||
|
project_ids: [].freeze
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
FINDER_PARAM_NAMES = [
|
||||||
|
:assignee_username,
|
||||||
|
:author_username,
|
||||||
|
:milestone_title,
|
||||||
|
:label_name
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
attr_writer :project_ids
|
||||||
|
|
||||||
|
attribute :created_after, :datetime
|
||||||
|
attribute :created_before, :datetime
|
||||||
|
attribute :group
|
||||||
|
attribute :current_user
|
||||||
|
attribute :value_stream
|
||||||
|
attribute :sort
|
||||||
|
attribute :direction
|
||||||
|
attribute :page
|
||||||
|
attribute :project
|
||||||
|
attribute :stage_id
|
||||||
|
attribute :end_event_filter
|
||||||
|
|
||||||
|
FINDER_PARAM_NAMES.each do |param_name|
|
||||||
|
attribute param_name
|
||||||
|
end
|
||||||
|
|
||||||
|
validates :created_after, presence: true
|
||||||
|
validates :created_before, presence: true
|
||||||
|
|
||||||
|
validate :validate_created_before
|
||||||
|
validate :validate_date_range
|
||||||
|
|
||||||
|
def initialize(params = {})
|
||||||
|
super(params)
|
||||||
|
|
||||||
|
self.created_before = (self.created_before || Time.current).at_end_of_day
|
||||||
|
self.created_after = (created_after || default_created_after).at_beginning_of_day
|
||||||
|
self.end_event_filter ||= Gitlab::Analytics::CycleAnalytics::BaseQueryBuilder::DEFAULT_END_EVENT_FILTER
|
||||||
|
end
|
||||||
|
|
||||||
|
def project_ids
|
||||||
|
Array(@project_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_data_collector_params
|
||||||
|
{
|
||||||
|
current_user: current_user,
|
||||||
|
from: created_after,
|
||||||
|
to: created_before,
|
||||||
|
project_ids: project_ids,
|
||||||
|
sort: sort&.to_sym,
|
||||||
|
direction: direction&.to_sym,
|
||||||
|
page: page,
|
||||||
|
end_event_filter: end_event_filter.to_sym
|
||||||
|
}.merge(attributes.symbolize_keys.slice(*FINDER_PARAM_NAMES))
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_data_attributes
|
||||||
|
{}.tap do |attrs|
|
||||||
|
attrs[:group] = group_data_attributes if group
|
||||||
|
attrs[:value_stream] = value_stream_data_attributes.to_json if value_stream
|
||||||
|
attrs[:created_after] = created_after.to_date.iso8601
|
||||||
|
attrs[:created_before] = created_before.to_date.iso8601
|
||||||
|
attrs[:projects] = group_projects(project_ids) if group && project_ids.present?
|
||||||
|
attrs[:labels] = label_name.to_json if label_name.present?
|
||||||
|
attrs[:assignees] = assignee_username.to_json if assignee_username.present?
|
||||||
|
attrs[:author] = author_username if author_username.present?
|
||||||
|
attrs[:milestone] = milestone_title if milestone_title.present?
|
||||||
|
attrs[:sort] = sort if sort.present?
|
||||||
|
attrs[:direction] = direction if direction.present?
|
||||||
|
attrs[:stage] = stage_data_attributes.to_json if stage_id.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def group_data_attributes
|
||||||
|
{
|
||||||
|
id: group.id,
|
||||||
|
name: group.name,
|
||||||
|
parent_id: group.parent_id,
|
||||||
|
full_path: group.full_path,
|
||||||
|
avatar_url: group.avatar_url
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def value_stream_data_attributes
|
||||||
|
{
|
||||||
|
id: value_stream.id,
|
||||||
|
name: value_stream.name,
|
||||||
|
is_custom: value_stream.custom?
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_projects(project_ids)
|
||||||
|
GroupProjectsFinder.new(
|
||||||
|
group: group,
|
||||||
|
current_user: current_user,
|
||||||
|
options: { include_subgroups: true },
|
||||||
|
project_ids_relation: project_ids
|
||||||
|
)
|
||||||
|
.execute
|
||||||
|
.with_route
|
||||||
|
.map { |project| project_data_attributes(project) }
|
||||||
|
.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def project_data_attributes(project)
|
||||||
|
{
|
||||||
|
id: project.to_gid.to_s,
|
||||||
|
name: project.name,
|
||||||
|
path_with_namespace: project.path_with_namespace,
|
||||||
|
avatar_url: project.avatar_url
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def stage_data_attributes
|
||||||
|
return unless stage
|
||||||
|
|
||||||
|
{
|
||||||
|
id: stage.id || stage.name,
|
||||||
|
title: stage.name
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_created_before
|
||||||
|
return if created_after.nil? || created_before.nil?
|
||||||
|
|
||||||
|
errors.add(:created_before, :invalid) if created_after > created_before
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_date_range
|
||||||
|
return if created_after.nil? || created_before.nil?
|
||||||
|
|
||||||
|
if (created_before - created_after) > MAX_RANGE_DAYS
|
||||||
|
errors.add(:created_after, s_('CycleAnalytics|The given date range is larger than 180 days'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_created_after
|
||||||
|
if created_before
|
||||||
|
(created_before - DEFAULT_DATE_RANGE)
|
||||||
|
else
|
||||||
|
DEFAULT_DATE_RANGE.ago
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def stage
|
||||||
|
return unless value_stream
|
||||||
|
|
||||||
|
strong_memoize(:stage) do
|
||||||
|
::Analytics::CycleAnalytics::StageFinder.new(parent: project || group, stage_id: stage_id).execute if stage_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -11,7 +11,7 @@ module Gitlab
|
||||||
PATTERN = %r{^\/([^\/]|\\/)+[^\\]\/[ismU]*}.freeze
|
PATTERN = %r{^\/([^\/]|\\/)+[^\\]\/[ismU]*}.freeze
|
||||||
|
|
||||||
def initialize(regexp)
|
def initialize(regexp)
|
||||||
super(regexp.gsub(/\\\//, '/'))
|
super(regexp.gsub(%r{\\/}, '/'))
|
||||||
|
|
||||||
unless Gitlab::UntrustedRegexp::RubySyntax.valid?(@value)
|
unless Gitlab::UntrustedRegexp::RubySyntax.valid?(@value)
|
||||||
raise Lexer::SyntaxError, 'Invalid regular expression!'
|
raise Lexer::SyntaxError, 'Invalid regular expression!'
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ module Gitlab
|
||||||
# 'raw' holds the Gitlab::Git::Conflict::File that this instance wraps
|
# 'raw' holds the Gitlab::Git::Conflict::File that this instance wraps
|
||||||
attr_reader :raw
|
attr_reader :raw
|
||||||
|
|
||||||
delegate :type, :content, :path, :their_path, :our_path, :our_mode, :our_blob, :repository, to: :raw
|
delegate :type, :content, :path, :ancestor_path, :their_path, :our_path, :our_mode, :our_blob, :repository, to: :raw
|
||||||
|
|
||||||
def initialize(raw, merge_request:)
|
def initialize(raw, merge_request:)
|
||||||
@raw = raw
|
@raw = raw
|
||||||
|
|
@ -227,6 +227,26 @@ module Gitlab
|
||||||
new_path: our_path)
|
new_path: our_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def conflict_type(diff_file)
|
||||||
|
if ancestor_path.present?
|
||||||
|
if our_path.present? && their_path.present?
|
||||||
|
:both_modified
|
||||||
|
elsif their_path.blank?
|
||||||
|
:modified_source_removed_target
|
||||||
|
else
|
||||||
|
:modified_target_removed_source
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if our_path.present? && their_path.present?
|
||||||
|
:both_added
|
||||||
|
elsif their_path.blank?
|
||||||
|
diff_file.renamed_file? ? :renamed_same_file : :removed_target_renamed_source
|
||||||
|
else
|
||||||
|
:removed_source_renamed_target
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def map_raw_lines(raw_lines)
|
def map_raw_lines(raw_lines)
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,11 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def progress
|
def progress(current: series + 1, total: total_series, track_name: track.to_s.humanize)
|
||||||
if Gitlab.com?
|
if Gitlab.com?
|
||||||
s_('InProductMarketing|This is email %{current_series} of %{total_series} in the %{track} series.') % { current_series: series + 1, total_series: total_series, track: track.to_s.humanize }
|
s_('InProductMarketing|This is email %{current_series} of %{total_series} in the %{track} series.') % { current_series: current, total_series: total, track: track_name }
|
||||||
else
|
else
|
||||||
s_('InProductMarketing|This is email %{current_series} of %{total_series} in the %{track} series. To disable notification emails sent by your local GitLab instance, either contact your administrator or %{unsubscribe_link}.') % { current_series: series + 1, total_series: total_series, track: track.to_s.humanize, unsubscribe_link: unsubscribe_link }
|
s_('InProductMarketing|This is email %{current_series} of %{total_series} in the %{track} series. To disable notification emails sent by your local GitLab instance, either contact your administrator or %{unsubscribe_link}.') % { current_series: current, total_series: total, track: track_name, unsubscribe_link: unsubscribe_link }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -109,7 +109,7 @@ module Gitlab
|
||||||
private
|
private
|
||||||
|
|
||||||
def track
|
def track
|
||||||
self.class.name.demodulize.downcase.to_sym
|
self.class.name.demodulize.underscore.to_sym
|
||||||
end
|
end
|
||||||
|
|
||||||
def total_series
|
def total_series
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,10 @@ module Gitlab
|
||||||
s_('InProductMarketing|Invite your team now')
|
s_('InProductMarketing|Invite your team now')
|
||||||
][series]
|
][series]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def progress
|
||||||
|
super(current: series + 2, total: 4)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Email
|
||||||
|
module Message
|
||||||
|
module InProductMarketing
|
||||||
|
class TeamShort < Base
|
||||||
|
def subject_line
|
||||||
|
s_('InProductMarketing|Team up in GitLab for greater efficiency')
|
||||||
|
end
|
||||||
|
|
||||||
|
def tagline
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def title
|
||||||
|
s_('InProductMarketing|Turn coworkers into collaborators')
|
||||||
|
end
|
||||||
|
|
||||||
|
def subtitle
|
||||||
|
s_('InProductMarketing|Invite your team today to build better code (and processes) together')
|
||||||
|
end
|
||||||
|
|
||||||
|
def body_line1
|
||||||
|
''
|
||||||
|
end
|
||||||
|
|
||||||
|
def body_line2
|
||||||
|
''
|
||||||
|
end
|
||||||
|
|
||||||
|
def cta_text
|
||||||
|
s_('InProductMarketing|Invite your colleagues today')
|
||||||
|
end
|
||||||
|
|
||||||
|
def progress
|
||||||
|
super(total: 4, track_name: 'Team')
|
||||||
|
end
|
||||||
|
|
||||||
|
def logo_path
|
||||||
|
'mailers/in_product_marketing/team-0.png'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -6,13 +6,14 @@ module Gitlab
|
||||||
class File
|
class File
|
||||||
UnsupportedEncoding = Class.new(StandardError)
|
UnsupportedEncoding = Class.new(StandardError)
|
||||||
|
|
||||||
attr_reader :their_path, :our_path, :our_mode, :repository, :commit_oid
|
attr_reader :ancestor_path, :their_path, :our_path, :our_mode, :repository, :commit_oid
|
||||||
|
|
||||||
attr_accessor :raw_content
|
attr_accessor :raw_content
|
||||||
|
|
||||||
def initialize(repository, commit_oid, conflict, raw_content)
|
def initialize(repository, commit_oid, conflict, raw_content)
|
||||||
@repository = repository
|
@repository = repository
|
||||||
@commit_oid = commit_oid
|
@commit_oid = commit_oid
|
||||||
|
@ancestor_path = conflict[:ancestor][:path]
|
||||||
@their_path = conflict[:theirs][:path]
|
@their_path = conflict[:theirs][:path]
|
||||||
@our_path = conflict[:ours][:path]
|
@our_path = conflict[:ours][:path]
|
||||||
@our_mode = conflict[:ours][:mode]
|
@our_mode = conflict[:ours][:mode]
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ module Gitlab
|
||||||
|
|
||||||
def conflict_from_gitaly_file_header(header)
|
def conflict_from_gitaly_file_header(header)
|
||||||
{
|
{
|
||||||
|
ancestor: { path: header.ancestor_path },
|
||||||
ours: { path: header.our_path, mode: header.our_mode },
|
ours: { path: header.our_path, mode: header.our_mode },
|
||||||
theirs: { path: header.their_path }
|
theirs: { path: header.their_path }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ module Gitlab
|
||||||
"put" => %w(200 202 204 400 401 403 404 405 406 409 410 422 500)
|
"put" => %w(200 202 204 400 401 403 404 405 406 409 410 422 500)
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
HEALTH_ENDPOINT = /^\/-\/(liveness|readiness|health|metrics)\/?$/.freeze
|
HEALTH_ENDPOINT = %r{^/-/(liveness|readiness|health|metrics)/?$}.freeze
|
||||||
|
|
||||||
FEATURE_CATEGORY_DEFAULT = 'unknown'
|
FEATURE_CATEGORY_DEFAULT = 'unknown'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ module Gitlab
|
||||||
|
|
||||||
IGNORABLE_SQL = %w{BEGIN COMMIT}.freeze
|
IGNORABLE_SQL = %w{BEGIN COMMIT}.freeze
|
||||||
DB_COUNTERS = %i{db_count db_write_count db_cached_count}.freeze
|
DB_COUNTERS = %i{db_count db_write_count db_cached_count}.freeze
|
||||||
SQL_COMMANDS_WITH_COMMENTS_REGEX = /\A(\/\*.*\*\/\s)?((?!(.*[^\w'"](DELETE|UPDATE|INSERT INTO)[^\w'"])))(WITH.*)?(SELECT)((?!(FOR UPDATE|FOR SHARE)).)*$/i.freeze
|
SQL_COMMANDS_WITH_COMMENTS_REGEX = %r{\A(/\*.*\*/\s)?((?!(.*[^\w'"](DELETE|UPDATE|INSERT INTO)[^\w'"])))(WITH.*)?(SELECT)((?!(FOR UPDATE|FOR SHARE)).)*$}i.freeze
|
||||||
|
|
||||||
SQL_DURATION_BUCKET = [0.05, 0.1, 0.25].freeze
|
SQL_DURATION_BUCKET = [0.05, 0.1, 0.25].freeze
|
||||||
TRANSACTION_DURATION_BUCKET = [0.1, 0.25, 1].freeze
|
TRANSACTION_DURATION_BUCKET = [0.1, 0.25, 1].freeze
|
||||||
|
|
|
||||||
|
|
@ -184,19 +184,19 @@ module Gitlab
|
||||||
# - Must not have a scheme, such as http:// or https://
|
# - Must not have a scheme, such as http:// or https://
|
||||||
# - Must not have a port number, such as :8080 or :8443
|
# - Must not have a port number, such as :8080 or :8443
|
||||||
|
|
||||||
@go_package_regex ||= /
|
@go_package_regex ||= %r{
|
||||||
\b (?# word boundary)
|
\b (?# word boundary)
|
||||||
(?<domain>
|
(?<domain>
|
||||||
[0-9a-z](?:(?:-|[0-9a-z]){0,61}[0-9a-z])? (?# first domain)
|
[0-9a-z](?:(?:-|[0-9a-z]){0,61}[0-9a-z])? (?# first domain)
|
||||||
(?:\.[0-9a-z](?:(?:-|[0-9a-z]){0,61}[0-9a-z])?)* (?# inner domains)
|
(?:\.[0-9a-z](?:(?:-|[0-9a-z]){0,61}[0-9a-z])?)* (?# inner domains)
|
||||||
\.[a-z]{2,} (?# top-level domain)
|
\.[a-z]{2,} (?# top-level domain)
|
||||||
)
|
)
|
||||||
(?<path>\/(?:
|
(?<path>/(?:
|
||||||
[-\/$_.+!*'(),0-9a-z] (?# plain URL character)
|
[-/$_.+!*'(),0-9a-z] (?# plain URL character)
|
||||||
| %[0-9a-f]{2})* (?# URL encoded character)
|
| %[0-9a-f]{2})* (?# URL encoded character)
|
||||||
)? (?# path)
|
)? (?# path)
|
||||||
\b (?# word boundary)
|
\b (?# word boundary)
|
||||||
/ix.freeze
|
}ix.freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
def generic_package_version_regex
|
def generic_package_version_regex
|
||||||
|
|
@ -416,7 +416,7 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def base64_regex
|
def base64_regex
|
||||||
@base64_regex ||= /(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?/.freeze
|
@base64_regex ||= %r{(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?}.freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
def feature_flag_regex
|
def feature_flag_regex
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ module Gitlab
|
||||||
MergeRequestCounter,
|
MergeRequestCounter,
|
||||||
DesignsCounter,
|
DesignsCounter,
|
||||||
KubernetesAgentCounter,
|
KubernetesAgentCounter,
|
||||||
StaticSiteEditorCounter
|
StaticSiteEditorCounter,
|
||||||
|
DiffsCounter
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
UsageDataCounterError = Class.new(StandardError)
|
UsageDataCounterError = Class.new(StandardError)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module UsageDataCounters
|
||||||
|
class DiffsCounter < BaseCounter
|
||||||
|
KNOWN_EVENTS = %w[searches].freeze
|
||||||
|
PREFIX = 'diff'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -232,3 +232,8 @@
|
||||||
redis_slot: code_review
|
redis_slot: code_review
|
||||||
category: code_review
|
category: code_review
|
||||||
aggregation: weekly
|
aggregation: weekly
|
||||||
|
- name: i_code_review_user_searches_diff
|
||||||
|
redis_slot: code_review
|
||||||
|
category: code_review
|
||||||
|
aggregation: weekly
|
||||||
|
feature_flag: diff_searching_usage_data
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ module Gitlab
|
||||||
return unless path.is_a?(String)
|
return unless path.is_a?(String)
|
||||||
|
|
||||||
path = decode_path(path)
|
path = decode_path(path)
|
||||||
path_regex = /(\A(\.{1,2})\z|\A\.\.[\/\\]|[\/\\]\.\.\z|[\/\\]\.\.[\/\\]|\n)/
|
path_regex = %r{(\A(\.{1,2})\z|\A\.\.[/\\]|[/\\]\.\.\z|[/\\]\.\.[/\\]|\n)}
|
||||||
|
|
||||||
if path.match?(path_regex)
|
if path.match?(path_regex)
|
||||||
raise PathTraversalAttackError, 'Invalid path'
|
raise PathTraversalAttackError, 'Invalid path'
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ module ProductAnalytics
|
||||||
URL = Gitlab.config.gitlab.url + '/-/sp.js'
|
URL = Gitlab.config.gitlab.url + '/-/sp.js'
|
||||||
|
|
||||||
# The collector URL minus protocol and /i
|
# The collector URL minus protocol and /i
|
||||||
COLLECTOR_URL = Gitlab.config.gitlab.url.sub(/\Ahttps?\:\/\//, '') + '/-/collector'
|
COLLECTOR_URL = Gitlab.config.gitlab.url.sub(%r{\Ahttps?\://}, '') + '/-/collector'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -8612,7 +8612,7 @@ msgstr ""
|
||||||
msgid "ContainerRegistry|Delete selected tags"
|
msgid "ContainerRegistry|Delete selected tags"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone."
|
msgid "ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone. Please type the following to confirm: %{code}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "ContainerRegistry|Deletion disabled due to missing or insufficient permissions."
|
msgid "ContainerRegistry|Deletion disabled due to missing or insufficient permissions."
|
||||||
|
|
@ -10918,30 +10918,30 @@ msgstr ""
|
||||||
msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
|
msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "DeployTokens|Add a deploy token"
|
msgid "DeployTokens|Allows read and write access to registry images."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "DeployTokens|Allows read access to the package registry."
|
msgid "DeployTokens|Allows read and write access to the package registry."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "DeployTokens|Allows read-only access to registry images."
|
msgid "DeployTokens|Allows read-only access to registry images."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "DeployTokens|Allows read-only access to the package registry."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "DeployTokens|Allows read-only access to the repository."
|
msgid "DeployTokens|Allows read-only access to the repository."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "DeployTokens|Allows write access to registry images."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "DeployTokens|Allows write access to the package registry."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "DeployTokens|Copy deploy token"
|
msgid "DeployTokens|Copy deploy token"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "DeployTokens|Copy username"
|
msgid "DeployTokens|Copy username"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "DeployTokens|Create deploy token"
|
msgid "DeployTokens|Create deploy token"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -10954,6 +10954,15 @@ msgstr ""
|
||||||
msgid "DeployTokens|Deploy tokens allow access to packages, your repository, and registry images."
|
msgid "DeployTokens|Deploy tokens allow access to packages, your repository, and registry images."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "DeployTokens|Enter a unique name for your deploy token."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "DeployTokens|Enter a username for your token. Defaults to %{code_start}gitlab+deploy-token-{n}%{code_end}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "DeployTokens|Enter an expiration date for your token. Defaults to never expire."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "DeployTokens|Expires"
|
msgid "DeployTokens|Expires"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -10963,7 +10972,7 @@ msgstr ""
|
||||||
msgid "DeployTokens|Name"
|
msgid "DeployTokens|Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "DeployTokens|Pick a name for your unique deploy token."
|
msgid "DeployTokens|New deploy token"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "DeployTokens|Revoke"
|
msgid "DeployTokens|Revoke"
|
||||||
|
|
@ -10984,12 +10993,6 @@ msgstr ""
|
||||||
msgid "DeployTokens|This username supports access. %{link_start}What kind of access?%{link_end}"
|
msgid "DeployTokens|This username supports access. %{link_start}What kind of access?%{link_end}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "DeployTokens|Unless you enter a date, the token does not expire."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "DeployTokens|Unless you specify a username, it is set to \"gitlab+deploy-token-{n}\"."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "DeployTokens|Use this token as a password. Save it. This password can %{i_start}not%{i_end} be recovered."
|
msgid "DeployTokens|Use this token as a password. Save it. This password can %{i_start}not%{i_end} be recovered."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -13333,6 +13336,9 @@ msgstr ""
|
||||||
msgid "Expiration date"
|
msgid "Expiration date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Expiration date (optional)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Expired"
|
msgid "Expired"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -13345,9 +13351,6 @@ msgstr ""
|
||||||
msgid "Expires"
|
msgid "Expires"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Expires at (optional)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Expires in %{expires_at}"
|
msgid "Expires in %{expires_at}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -17001,6 +17004,9 @@ msgstr ""
|
||||||
msgid "InProductMarketing|Invite your team now"
|
msgid "InProductMarketing|Invite your team now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "InProductMarketing|Invite your team today to build better code (and processes) together"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "InProductMarketing|It's all in the stats"
|
msgid "InProductMarketing|It's all in the stats"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -17082,6 +17088,9 @@ msgstr ""
|
||||||
msgid "InProductMarketing|Take your source code management to the next level"
|
msgid "InProductMarketing|Take your source code management to the next level"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "InProductMarketing|Team up in GitLab for greater efficiency"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "InProductMarketing|Team work makes the dream work"
|
msgid "InProductMarketing|Team work makes the dream work"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -17118,6 +17127,9 @@ msgstr ""
|
||||||
msgid "InProductMarketing|Try it yourself"
|
msgid "InProductMarketing|Try it yourself"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "InProductMarketing|Turn coworkers into collaborators"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "InProductMarketing|Twitter"
|
msgid "InProductMarketing|Twitter"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -25701,9 +25713,6 @@ msgstr ""
|
||||||
msgid "ProjectSettings|Allow"
|
msgid "ProjectSettings|Allow"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "ProjectSettings|Allow editing commit messages"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "ProjectSettings|Always show thumbs-up and thumbs-down award emoji buttons on issues, merge requests, and snippets."
|
msgid "ProjectSettings|Always show thumbs-up and thumbs-down award emoji buttons on issues, merge requests, and snippets."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -25731,9 +25740,6 @@ msgstr ""
|
||||||
msgid "ProjectSettings|Choose your merge method, merge options, merge checks, merge suggestions, and set up a default description template for merge requests."
|
msgid "ProjectSettings|Choose your merge method, merge options, merge checks, merge suggestions, and set up a default description template for merge requests."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "ProjectSettings|Commit authors can edit commit messages on unprotected branches."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "ProjectSettings|Configure your project resources and monitor their health."
|
msgid "ProjectSettings|Configure your project resources and monitor their health."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -28655,7 +28661,7 @@ msgstr ""
|
||||||
msgid "Scopes"
|
msgid "Scopes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Scopes [Select 1 or more]"
|
msgid "Scopes (select at least one)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Scopes can't be blank"
|
msgid "Scopes can't be blank"
|
||||||
|
|
@ -38304,9 +38310,6 @@ msgstr ""
|
||||||
msgid "can only have one escalation policy"
|
msgid "can only have one escalation policy"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "can't be enabled because signed commits are required for this project"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "can't be the same as the source project"
|
msgid "can't be the same as the source project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,26 +7,58 @@ RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do
|
||||||
let_it_be(:group) { create(:group) }
|
let_it_be(:group) { create(:group) }
|
||||||
let_it_be(:project) { create(:project, group: group) }
|
let_it_be(:project) { create(:project, group: group) }
|
||||||
|
|
||||||
let(:params) { { namespace_id: group, project_id: project, value_stream_id: 'default' } }
|
let(:params) do
|
||||||
|
{
|
||||||
|
namespace_id: group,
|
||||||
|
project_id: project,
|
||||||
|
value_stream_id: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET index' do
|
shared_examples 'project-level value stream analytics endpoint' do
|
||||||
context 'when user is member of the project' do
|
|
||||||
before do
|
before do
|
||||||
project.add_developer(user)
|
project.add_developer(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'succeeds' do
|
it 'succeeds' do
|
||||||
get :index, params: params
|
get action, params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'project-level value stream analytics request error examples' do
|
||||||
|
context 'when invalid value stream id is given' do
|
||||||
|
before do
|
||||||
|
params[:value_stream_id] = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders 404' do
|
||||||
|
get action, params: params
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is not member of the project' do
|
||||||
|
it 'renders 404' do
|
||||||
|
get action, params: params
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET index' do
|
||||||
|
let(:action) { :index }
|
||||||
|
|
||||||
|
it_behaves_like 'project-level value stream analytics endpoint' do
|
||||||
it 'exposes the default stages' do
|
it 'exposes the default stages' do
|
||||||
get :index, params: params
|
get action, params: params
|
||||||
|
|
||||||
expect(json_response['stages'].size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size)
|
expect(json_response['stages'].size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size)
|
||||||
end
|
end
|
||||||
|
|
@ -37,31 +69,109 @@ RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do
|
||||||
expect(list_service).to receive(:allowed?).and_return(false)
|
expect(list_service).to receive(:allowed?).and_return(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
get :index, params: params
|
get action, params: params
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:forbidden)
|
expect(response).to have_gitlab_http_status(:forbidden)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when invalid value stream id is given' do
|
it_behaves_like 'project-level value stream analytics request error examples'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET median' do
|
||||||
|
let(:action) { :median }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
params[:value_stream_id] = 1
|
params[:id] = 'issue'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'renders 404' do
|
it_behaves_like 'project-level value stream analytics endpoint' do
|
||||||
get :index, params: params
|
it 'returns the median' do
|
||||||
|
result = 2
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
expect_next_instance_of(Gitlab::Analytics::CycleAnalytics::Median) do |instance|
|
||||||
|
expect(instance).to receive(:seconds).and_return(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
get action, params: params
|
||||||
|
|
||||||
|
expect(json_response['value']).to eq(result)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user is not member of the project' do
|
it_behaves_like 'project-level value stream analytics request error examples'
|
||||||
it 'renders 404' do
|
end
|
||||||
get :index, params: params
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
describe 'GET average' do
|
||||||
|
let(:action) { :average }
|
||||||
|
|
||||||
|
before do
|
||||||
|
params[:id] = 'issue'
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'project-level value stream analytics endpoint' do
|
||||||
|
it 'returns the average' do
|
||||||
|
result = 2
|
||||||
|
|
||||||
|
expect_next_instance_of(Gitlab::Analytics::CycleAnalytics::Average) do |instance|
|
||||||
|
expect(instance).to receive(:seconds).and_return(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
get action, params: params
|
||||||
|
|
||||||
|
expect(json_response['value']).to eq(result)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'project-level value stream analytics request error examples'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET count' do
|
||||||
|
let(:action) { :count }
|
||||||
|
|
||||||
|
before do
|
||||||
|
params[:id] = 'issue'
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'project-level value stream analytics endpoint' do
|
||||||
|
it 'returns the count' do
|
||||||
|
count = 2
|
||||||
|
|
||||||
|
expect_next_instance_of(Gitlab::Analytics::CycleAnalytics::DataCollector) do |instance|
|
||||||
|
expect(instance).to receive(:count).and_return(count)
|
||||||
|
end
|
||||||
|
|
||||||
|
get action, params: params
|
||||||
|
|
||||||
|
expect(json_response['count']).to eq(count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'project-level value stream analytics request error examples'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET records' do
|
||||||
|
let(:action) { :records }
|
||||||
|
|
||||||
|
before do
|
||||||
|
params[:id] = 'issue'
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'project-level value stream analytics endpoint' do
|
||||||
|
it 'returns the records' do
|
||||||
|
result = Issue.none.page(1)
|
||||||
|
|
||||||
|
expect_next_instance_of(Gitlab::Analytics::CycleAnalytics::RecordsFetcher) do |instance|
|
||||||
|
expect(instance).to receive(:serialized_records).and_yield(result).and_return([])
|
||||||
|
end
|
||||||
|
|
||||||
|
get action, params: params
|
||||||
|
|
||||||
|
expect(json_response).to eq([])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'project-level value stream analytics request error examples'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,24 @@ RSpec.describe Projects::MergeRequests::DiffsController do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET diffs_metadata' do
|
describe 'GET diffs_metadata' do
|
||||||
|
shared_examples_for 'serializes diffs metadata with expected arguments' do
|
||||||
|
it 'returns success' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'serializes paginated merge request diff collection' do
|
||||||
|
expect_next_instance_of(DiffsMetadataSerializer) do |instance|
|
||||||
|
expect(instance).to receive(:represent)
|
||||||
|
.with(an_instance_of(collection), expected_options)
|
||||||
|
.and_call_original
|
||||||
|
end
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def go(extra_params = {})
|
def go(extra_params = {})
|
||||||
params = {
|
params = {
|
||||||
namespace_id: project.namespace.to_param,
|
namespace_id: project.namespace.to_param,
|
||||||
|
|
@ -179,14 +197,12 @@ RSpec.describe Projects::MergeRequests::DiffsController do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with valid diff_id' do
|
context 'with valid diff_id' do
|
||||||
it 'returns success' do
|
subject { go(diff_id: merge_request.merge_request_diff.id) }
|
||||||
go(diff_id: merge_request.merge_request_diff.id)
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
it_behaves_like 'serializes diffs metadata with expected arguments' do
|
||||||
end
|
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiff }
|
||||||
|
let(:expected_options) do
|
||||||
it 'serializes diffs metadata with expected arguments' do
|
{
|
||||||
expected_options = {
|
|
||||||
environment: nil,
|
environment: nil,
|
||||||
merge_request: merge_request,
|
merge_request: merge_request,
|
||||||
merge_request_diff: merge_request.merge_request_diff,
|
merge_request_diff: merge_request.merge_request_diff,
|
||||||
|
|
@ -195,16 +211,11 @@ RSpec.describe Projects::MergeRequests::DiffsController do
|
||||||
start_sha: nil,
|
start_sha: nil,
|
||||||
commit: nil,
|
commit: nil,
|
||||||
latest_diff: true,
|
latest_diff: true,
|
||||||
only_context_commits: false
|
only_context_commits: false,
|
||||||
|
allow_tree_conflicts: true,
|
||||||
|
merge_ref_head_diff: false
|
||||||
}
|
}
|
||||||
|
|
||||||
expect_next_instance_of(DiffsMetadataSerializer) do |instance|
|
|
||||||
expect(instance).to receive(:represent)
|
|
||||||
.with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff), expected_options)
|
|
||||||
.and_call_original
|
|
||||||
end
|
end
|
||||||
|
|
||||||
go(diff_id: merge_request.merge_request_diff.id)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -261,14 +272,12 @@ RSpec.describe Projects::MergeRequests::DiffsController do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with MR regular diff params' do
|
context 'with MR regular diff params' do
|
||||||
it 'returns success' do
|
subject { go }
|
||||||
go
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
it_behaves_like 'serializes diffs metadata with expected arguments' do
|
||||||
end
|
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiff }
|
||||||
|
let(:expected_options) do
|
||||||
it 'serializes diffs metadata with expected arguments' do
|
{
|
||||||
expected_options = {
|
|
||||||
environment: nil,
|
environment: nil,
|
||||||
merge_request: merge_request,
|
merge_request: merge_request,
|
||||||
merge_request_diff: merge_request.merge_request_diff,
|
merge_request_diff: merge_request.merge_request_diff,
|
||||||
|
|
@ -277,28 +286,21 @@ RSpec.describe Projects::MergeRequests::DiffsController do
|
||||||
start_sha: nil,
|
start_sha: nil,
|
||||||
commit: nil,
|
commit: nil,
|
||||||
latest_diff: true,
|
latest_diff: true,
|
||||||
only_context_commits: false
|
only_context_commits: false,
|
||||||
|
allow_tree_conflicts: true,
|
||||||
|
merge_ref_head_diff: nil
|
||||||
}
|
}
|
||||||
|
|
||||||
expect_next_instance_of(DiffsMetadataSerializer) do |instance|
|
|
||||||
expect(instance).to receive(:represent)
|
|
||||||
.with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff), expected_options)
|
|
||||||
.and_call_original
|
|
||||||
end
|
end
|
||||||
|
|
||||||
go
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with commit param' do
|
context 'with commit param' do
|
||||||
it 'returns success' do
|
subject { go(commit_id: merge_request.diff_head_sha) }
|
||||||
go(commit_id: merge_request.diff_head_sha)
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
it_behaves_like 'serializes diffs metadata with expected arguments' do
|
||||||
end
|
let(:collection) { Gitlab::Diff::FileCollection::Commit }
|
||||||
|
let(:expected_options) do
|
||||||
it 'serializes diffs metadata with expected arguments' do
|
{
|
||||||
expected_options = {
|
|
||||||
environment: nil,
|
environment: nil,
|
||||||
merge_request: merge_request,
|
merge_request: merge_request,
|
||||||
merge_request_diff: nil,
|
merge_request_diff: nil,
|
||||||
|
|
@ -307,16 +309,38 @@ RSpec.describe Projects::MergeRequests::DiffsController do
|
||||||
start_sha: nil,
|
start_sha: nil,
|
||||||
commit: merge_request.diff_head_commit,
|
commit: merge_request.diff_head_commit,
|
||||||
latest_diff: nil,
|
latest_diff: nil,
|
||||||
only_context_commits: false
|
only_context_commits: false,
|
||||||
|
allow_tree_conflicts: true,
|
||||||
|
merge_ref_head_diff: nil
|
||||||
}
|
}
|
||||||
|
end
|
||||||
expect_next_instance_of(DiffsMetadataSerializer) do |instance|
|
end
|
||||||
expect(instance).to receive(:represent)
|
|
||||||
.with(an_instance_of(Gitlab::Diff::FileCollection::Commit), expected_options)
|
|
||||||
.and_call_original
|
|
||||||
end
|
end
|
||||||
|
|
||||||
go(commit_id: merge_request.diff_head_sha)
|
context 'when display_merge_conflicts_in_diff is disabled' do
|
||||||
|
subject { go }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_feature_flags(display_merge_conflicts_in_diff: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'serializes diffs metadata with expected arguments' do
|
||||||
|
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiff }
|
||||||
|
let(:expected_options) do
|
||||||
|
{
|
||||||
|
environment: nil,
|
||||||
|
merge_request: merge_request,
|
||||||
|
merge_request_diff: merge_request.merge_request_diff,
|
||||||
|
merge_request_diffs: merge_request.merge_request_diffs,
|
||||||
|
start_version: nil,
|
||||||
|
start_sha: nil,
|
||||||
|
commit: nil,
|
||||||
|
latest_diff: true,
|
||||||
|
only_context_commits: false,
|
||||||
|
allow_tree_conflicts: false,
|
||||||
|
merge_ref_head_diff: nil
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -767,8 +767,7 @@ RSpec.describe ProjectsController do
|
||||||
id: project.path,
|
id: project.path,
|
||||||
project: {
|
project: {
|
||||||
project_setting_attributes: {
|
project_setting_attributes: {
|
||||||
show_default_award_emojis: boolean_value,
|
show_default_award_emojis: boolean_value
|
||||||
allow_editing_commit_messages: boolean_value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -776,7 +775,6 @@ RSpec.describe ProjectsController do
|
||||||
project.reload
|
project.reload
|
||||||
|
|
||||||
expect(project.show_default_award_emojis?).to eq(result)
|
expect(project.show_default_award_emojis?).to eq(result)
|
||||||
expect(project.allow_editing_commit_messages?).to eq(result)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,20 @@ RSpec.describe SearchController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
shared_examples_for 'support for active record query timeouts' do |action, params, method_to_stub, format|
|
||||||
|
before do
|
||||||
|
allow_next_instance_of(SearchService) do |service|
|
||||||
|
allow(service).to receive(method_to_stub).and_raise(ActiveRecord::QueryCanceled)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders a 408 when a timeout occurs' do
|
||||||
|
get action, params: params, format: format
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:request_timeout)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'GET #show' do
|
describe 'GET #show' do
|
||||||
it_behaves_like 'when the user cannot read cross project', :show, { search: 'hello' } do
|
it_behaves_like 'when the user cannot read cross project', :show, { search: 'hello' } do
|
||||||
it 'still allows accessing the search page' do
|
it 'still allows accessing the search page' do
|
||||||
|
|
@ -63,6 +77,7 @@ RSpec.describe SearchController do
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' }
|
it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' }
|
||||||
|
it_behaves_like 'support for active record query timeouts', :show, { search: 'hello' }, :search_objects, :html
|
||||||
|
|
||||||
context 'uses the right partials depending on scope' do
|
context 'uses the right partials depending on scope' do
|
||||||
using RSpec::Parameterized::TableSyntax
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
@ -230,6 +245,7 @@ RSpec.describe SearchController do
|
||||||
describe 'GET #count' do
|
describe 'GET #count' do
|
||||||
it_behaves_like 'when the user cannot read cross project', :count, { search: 'hello', scope: 'projects' }
|
it_behaves_like 'when the user cannot read cross project', :count, { search: 'hello', scope: 'projects' }
|
||||||
it_behaves_like 'with external authorization service enabled', :count, { search: 'hello', scope: 'projects' }
|
it_behaves_like 'with external authorization service enabled', :count, { search: 'hello', scope: 'projects' }
|
||||||
|
it_behaves_like 'support for active record query timeouts', :count, { search: 'hello', scope: 'projects' }, :search_results, :json
|
||||||
|
|
||||||
it 'returns the result count for the given term and scope' do
|
it 'returns the result count for the given term and scope' do
|
||||||
create(:project, :public, name: 'hello world')
|
create(:project, :public, name: 'hello world')
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ const defaultProps = {
|
||||||
emailsDisabled: false,
|
emailsDisabled: false,
|
||||||
packagesEnabled: true,
|
packagesEnabled: true,
|
||||||
showDefaultAwardEmojis: true,
|
showDefaultAwardEmojis: true,
|
||||||
allowEditingCommitMessages: false,
|
|
||||||
},
|
},
|
||||||
isGitlabCom: true,
|
isGitlabCom: true,
|
||||||
canDisableEmails: true,
|
canDisableEmails: true,
|
||||||
|
|
@ -53,7 +52,7 @@ describe('Settings Panel', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
const mountComponent = (
|
const mountComponent = (
|
||||||
{ currentSettings = {}, glFeatures = {}, ...customProps } = {},
|
{ currentSettings = {}, ...customProps } = {},
|
||||||
mountFn = shallowMount,
|
mountFn = shallowMount,
|
||||||
) => {
|
) => {
|
||||||
const propsData = {
|
const propsData = {
|
||||||
|
|
@ -64,9 +63,6 @@ describe('Settings Panel', () => {
|
||||||
|
|
||||||
return mountFn(settingsPanel, {
|
return mountFn(settingsPanel, {
|
||||||
propsData,
|
propsData,
|
||||||
provide: {
|
|
||||||
glFeatures,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -100,8 +96,6 @@ describe('Settings Panel', () => {
|
||||||
const findShowDefaultAwardEmojis = () =>
|
const findShowDefaultAwardEmojis = () =>
|
||||||
wrapper.find('input[name="project[project_setting_attributes][show_default_award_emojis]"]');
|
wrapper.find('input[name="project[project_setting_attributes][show_default_award_emojis]"]');
|
||||||
const findMetricsVisibilitySettings = () => wrapper.find({ ref: 'metrics-visibility-settings' });
|
const findMetricsVisibilitySettings = () => wrapper.find({ ref: 'metrics-visibility-settings' });
|
||||||
const findAllowEditingCommitMessages = () =>
|
|
||||||
wrapper.find({ ref: 'allow-editing-commit-messages' }).exists();
|
|
||||||
const findOperationsSettings = () => wrapper.find({ ref: 'operations-settings' });
|
const findOperationsSettings = () => wrapper.find({ ref: 'operations-settings' });
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
@ -582,18 +576,6 @@ describe('Settings Panel', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Settings panel with feature flags', () => {
|
|
||||||
describe('Allow edit of commit message', () => {
|
|
||||||
it('should show the allow editing of commit messages checkbox', () => {
|
|
||||||
wrapper = mountComponent({
|
|
||||||
glFeatures: { allowEditingCommitMessages: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findAllowEditingCommitMessages()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Analytics', () => {
|
describe('Analytics', () => {
|
||||||
it('should show the analytics toggle', () => {
|
it('should show the analytics toggle', () => {
|
||||||
wrapper = mountComponent();
|
wrapper = mountComponent();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { GlSprintf } from '@gitlab/ui';
|
import { GlSprintf, GlFormInput } from '@gitlab/ui';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
import component from '~/registry/explorer/components/details_page/delete_modal.vue';
|
import component from '~/registry/explorer/components/details_page/delete_modal.vue';
|
||||||
import {
|
import {
|
||||||
REMOVE_TAG_CONFIRMATION_TEXT,
|
REMOVE_TAG_CONFIRMATION_TEXT,
|
||||||
|
|
@ -12,8 +13,9 @@ import { GlModal } from '../../stubs';
|
||||||
describe('Delete Modal', () => {
|
describe('Delete Modal', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
const findModal = () => wrapper.find(GlModal);
|
const findModal = () => wrapper.findComponent(GlModal);
|
||||||
const findDescription = () => wrapper.find('[data-testid="description"]');
|
const findDescription = () => wrapper.find('[data-testid="description"]');
|
||||||
|
const findInputComponent = () => wrapper.findComponent(GlFormInput);
|
||||||
|
|
||||||
const mountComponent = (propsData) => {
|
const mountComponent = (propsData) => {
|
||||||
wrapper = shallowMount(component, {
|
wrapper = shallowMount(component, {
|
||||||
|
|
@ -25,6 +27,13 @@ describe('Delete Modal', () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const expectPrimaryActionStatus = (disabled = true) =>
|
||||||
|
expect(findModal().props('actionPrimary')).toMatchObject(
|
||||||
|
expect.objectContaining({
|
||||||
|
attributes: [{ variant: 'danger' }, { disabled }],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
wrapper = null;
|
wrapper = null;
|
||||||
|
|
@ -65,11 +74,49 @@ describe('Delete Modal', () => {
|
||||||
it('has the correct description', () => {
|
it('has the correct description', () => {
|
||||||
mountComponent({ deleteImage: true });
|
mountComponent({ deleteImage: true });
|
||||||
|
|
||||||
expect(wrapper.text()).toContain(DELETE_IMAGE_CONFIRMATION_TEXT);
|
expect(wrapper.text()).toContain(
|
||||||
|
DELETE_IMAGE_CONFIRMATION_TEXT.replace('%{code}', '').trim(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete button', () => {
|
||||||
|
const itemsToBeDeleted = [{ project: { path: 'foo' } }];
|
||||||
|
|
||||||
|
it('is disabled by default', () => {
|
||||||
|
mountComponent({ deleteImage: true });
|
||||||
|
|
||||||
|
expectPrimaryActionStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if the user types something different from the project path is disabled', async () => {
|
||||||
|
mountComponent({ deleteImage: true, itemsToBeDeleted });
|
||||||
|
|
||||||
|
findInputComponent().vm.$emit('input', 'bar');
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expectPrimaryActionStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if the user types the project path it is enabled', async () => {
|
||||||
|
mountComponent({ deleteImage: true, itemsToBeDeleted });
|
||||||
|
|
||||||
|
findInputComponent().vm.$emit('input', 'foo');
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expectPrimaryActionStatus(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when we are deleting tags', () => {
|
describe('when we are deleting tags', () => {
|
||||||
|
it('delete button is enabled', () => {
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
expectPrimaryActionStatus(false);
|
||||||
|
});
|
||||||
|
|
||||||
describe('itemsToBeDeleted contains one element', () => {
|
describe('itemsToBeDeleted contains one element', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mountComponent({ itemsToBeDeleted: [{ path: 'foo' }] });
|
mountComponent({ itemsToBeDeleted: [{ path: 'foo' }] });
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { GlButton, GlIcon } from '@gitlab/ui';
|
import { GlDropdownItem, GlIcon } from '@gitlab/ui';
|
||||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
import VueApollo from 'vue-apollo';
|
import VueApollo from 'vue-apollo';
|
||||||
import { useFakeDate } from 'helpers/fake_date';
|
import { useFakeDate } from 'helpers/fake_date';
|
||||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
import { GlDropdown } from 'jest/registry/explorer/stubs';
|
||||||
import component from '~/registry/explorer/components/details_page/details_header.vue';
|
import component from '~/registry/explorer/components/details_page/details_header.vue';
|
||||||
import {
|
import {
|
||||||
UNSCHEDULED_STATUS,
|
UNSCHEDULED_STATUS,
|
||||||
|
|
@ -48,8 +49,8 @@ describe('Details Header', () => {
|
||||||
const findTitle = () => findByTestId('title');
|
const findTitle = () => findByTestId('title');
|
||||||
const findTagsCount = () => findByTestId('tags-count');
|
const findTagsCount = () => findByTestId('tags-count');
|
||||||
const findCleanup = () => findByTestId('cleanup');
|
const findCleanup = () => findByTestId('cleanup');
|
||||||
const findDeleteButton = () => wrapper.find(GlButton);
|
const findDeleteButton = () => wrapper.findComponent(GlDropdownItem);
|
||||||
const findInfoIcon = () => wrapper.find(GlIcon);
|
const findInfoIcon = () => wrapper.findComponent(GlIcon);
|
||||||
|
|
||||||
const waitForMetadataItems = async () => {
|
const waitForMetadataItems = async () => {
|
||||||
// Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available
|
// Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available
|
||||||
|
|
@ -84,6 +85,8 @@ describe('Details Header', () => {
|
||||||
mocks,
|
mocks,
|
||||||
stubs: {
|
stubs: {
|
||||||
TitleArea,
|
TitleArea,
|
||||||
|
GlDropdown,
|
||||||
|
GlDropdownItem,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -152,10 +155,11 @@ describe('Details Header', () => {
|
||||||
it('has the correct props', () => {
|
it('has the correct props', () => {
|
||||||
mountComponent();
|
mountComponent();
|
||||||
|
|
||||||
expect(findDeleteButton().props()).toMatchObject({
|
expect(findDeleteButton().attributes()).toMatchObject(
|
||||||
|
expect.objectContaining({
|
||||||
variant: 'danger',
|
variant: 'danger',
|
||||||
disabled: false,
|
}),
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits the correct event', () => {
|
it('emits the correct event', () => {
|
||||||
|
|
@ -168,16 +172,16 @@ describe('Details Header', () => {
|
||||||
|
|
||||||
it.each`
|
it.each`
|
||||||
canDelete | disabled | isDisabled
|
canDelete | disabled | isDisabled
|
||||||
${true} | ${false} | ${false}
|
${true} | ${false} | ${undefined}
|
||||||
${true} | ${true} | ${true}
|
${true} | ${true} | ${'true'}
|
||||||
${false} | ${false} | ${true}
|
${false} | ${false} | ${'true'}
|
||||||
${false} | ${true} | ${true}
|
${false} | ${true} | ${'true'}
|
||||||
`(
|
`(
|
||||||
'when canDelete is $canDelete and disabled is $disabled is $isDisabled that the button is disabled',
|
'when canDelete is $canDelete and disabled is $disabled is $isDisabled that the button is disabled',
|
||||||
({ canDelete, disabled, isDisabled }) => {
|
({ canDelete, disabled, isDisabled }) => {
|
||||||
mountComponent({ propsData: { image: { ...defaultImage, canDelete }, disabled } });
|
mountComponent({ propsData: { image: { ...defaultImage, canDelete }, disabled } });
|
||||||
|
|
||||||
expect(findDeleteButton().props('disabled')).toBe(isDisabled);
|
expect(findDeleteButton().attributes('disabled')).toBe(isDisabled);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,7 @@ export const containerRepositoryMock = {
|
||||||
expirationPolicyCleanupStatus: 'UNSCHEDULED',
|
expirationPolicyCleanupStatus: 'UNSCHEDULED',
|
||||||
project: {
|
project: {
|
||||||
visibility: 'public',
|
visibility: 'public',
|
||||||
|
path: 'gitlab-test',
|
||||||
containerExpirationPolicy: {
|
containerExpirationPolicy: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
nextRunAt: '2020-11-27T08:59:27Z',
|
nextRunAt: '2020-11-27T08:59:27Z',
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import {
|
||||||
GlModal as RealGlModal,
|
GlModal as RealGlModal,
|
||||||
GlEmptyState as RealGlEmptyState,
|
GlEmptyState as RealGlEmptyState,
|
||||||
GlSkeletonLoader as RealGlSkeletonLoader,
|
GlSkeletonLoader as RealGlSkeletonLoader,
|
||||||
|
GlDropdown as RealGlDropdown,
|
||||||
} from '@gitlab/ui';
|
} from '@gitlab/ui';
|
||||||
import { RouterLinkStub } from '@vue/test-utils';
|
import { RouterLinkStub } from '@vue/test-utils';
|
||||||
import { stubComponent } from 'helpers/stub_component';
|
import { stubComponent } from 'helpers/stub_component';
|
||||||
|
|
@ -38,3 +39,7 @@ export const ListItem = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const GlDropdown = stubComponent(RealGlDropdown, {
|
||||||
|
template: '<div><slot></slot></div>',
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe ::Mutations::BaseMutation do
|
||||||
|
include GraphqlHelpers
|
||||||
|
|
||||||
|
describe 'argument nullability' do
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:context) { { current_user: user } }
|
||||||
|
|
||||||
|
subject(:mutation) { mutation_class.new(object: nil, context: context, field: nil) }
|
||||||
|
|
||||||
|
describe 'when using a mutation with correct argument declarations' do
|
||||||
|
context 'when argument is nullable and required' do
|
||||||
|
let(:mutation_class) do
|
||||||
|
Class.new(described_class) do
|
||||||
|
argument :foo, GraphQL::STRING_TYPE, required: :nullable
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
specify do
|
||||||
|
expect { subject.ready? }.to raise_error(ArgumentError, /must be provided: foo/)
|
||||||
|
end
|
||||||
|
|
||||||
|
specify do
|
||||||
|
expect { subject.ready?(foo: nil) }.not_to raise_error
|
||||||
|
end
|
||||||
|
|
||||||
|
specify do
|
||||||
|
expect { subject.ready?(foo: "bar") }.not_to raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when argument is required and NOT nullable' do
|
||||||
|
let(:mutation_class) do
|
||||||
|
Class.new(described_class) do
|
||||||
|
argument :foo, GraphQL::STRING_TYPE, required: true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
specify do
|
||||||
|
expect { subject.ready? }.to raise_error(ArgumentError, /must be provided/)
|
||||||
|
end
|
||||||
|
|
||||||
|
specify do
|
||||||
|
expect { subject.ready?(foo: nil) }.to raise_error(ArgumentError, /must be provided/)
|
||||||
|
end
|
||||||
|
|
||||||
|
specify do
|
||||||
|
expect { subject.ready?(foo: "bar") }.not_to raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -41,7 +41,7 @@ RSpec.describe Mutations::Ci::Runner::Delete do
|
||||||
let(:mutation_params) { {} }
|
let(:mutation_params) { {} }
|
||||||
|
|
||||||
it 'raises an error' do
|
it 'raises an error' do
|
||||||
expect { subject }.to raise_error(ArgumentError, "missing keyword: :id")
|
expect { subject }.to raise_error(ArgumentError, "Arguments must be provided: id")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ RSpec.describe Mutations::Ci::Runner::Update do
|
||||||
let(:mutation_params) { {} }
|
let(:mutation_params) { {} }
|
||||||
|
|
||||||
it 'raises an error' do
|
it 'raises an error' do
|
||||||
expect { subject }.to raise_error(ArgumentError, "missing keyword: :id")
|
expect { subject }.to raise_error(ArgumentError, "Arguments must be provided: id")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Types::BaseArgument do
|
RSpec.describe Types::BaseArgument do
|
||||||
include_examples 'Gitlab-style deprecations' do
|
|
||||||
let_it_be(:field) do
|
let_it_be(:field) do
|
||||||
Types::BaseField.new(name: 'field', type: String, null: true)
|
Types::BaseField.new(name: 'field', type: String, null: true)
|
||||||
end
|
end
|
||||||
|
|
@ -13,5 +12,32 @@ RSpec.describe Types::BaseArgument do
|
||||||
def subject(args = {})
|
def subject(args = {})
|
||||||
described_class.new(**base_args.merge(args))
|
described_class.new(**base_args.merge(args))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
include_examples 'Gitlab-style deprecations'
|
||||||
|
|
||||||
|
describe 'required argument declarations' do
|
||||||
|
it 'accepts nullable, required arguments' do
|
||||||
|
arguments = base_args.merge({ required: :nullable })
|
||||||
|
|
||||||
|
expect { subject(arguments) }.not_to raise_error
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'accepts required, non-nullable arguments' do
|
||||||
|
arguments = base_args.merge({ required: true })
|
||||||
|
|
||||||
|
expect { subject(arguments) }.not_to raise_error
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'accepts non-required arguments' do
|
||||||
|
arguments = base_args.merge({ required: false })
|
||||||
|
|
||||||
|
expect { subject(arguments) }.not_to raise_error
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'accepts no required argument declaration' do
|
||||||
|
arguments = base_args
|
||||||
|
|
||||||
|
expect { subject(arguments) }.not_to raise_error
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -917,34 +917,4 @@ RSpec.describe ProjectsHelper do
|
||||||
subject
|
subject
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#project_permissions_settings' do
|
|
||||||
context 'with no project_setting associated' do
|
|
||||||
it 'includes a value for edit commit messages' do
|
|
||||||
settings = project_permissions_settings(project)
|
|
||||||
|
|
||||||
expect(settings[:allowEditingCommitMessages]).to be_falsy
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when commits are allowed to be edited' do
|
|
||||||
it 'includes the edit commit message value' do
|
|
||||||
project.create_project_setting(allow_editing_commit_messages: true)
|
|
||||||
|
|
||||||
settings = project_permissions_settings(project)
|
|
||||||
|
|
||||||
expect(settings[:allowEditingCommitMessages]).to be_truthy
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when commits are not allowed to be edited' do
|
|
||||||
it 'returns false to the edit commit message value' do
|
|
||||||
project.create_project_setting(allow_editing_commit_messages: false)
|
|
||||||
|
|
||||||
settings = project_permissions_settings(project)
|
|
||||||
|
|
||||||
expect(settings[:allowEditingCommitMessages]).to be_falsy
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Backup::DatabaseBackupError do
|
||||||
|
let(:config) do
|
||||||
|
{
|
||||||
|
host: 'localhost',
|
||||||
|
port: 5432,
|
||||||
|
database: 'gitlabhq_test'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:db_file_name) { File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz') }
|
||||||
|
|
||||||
|
subject { described_class.new(config, db_file_name) }
|
||||||
|
|
||||||
|
it { is_expected.to respond_to :config }
|
||||||
|
it { is_expected.to respond_to :db_file_name }
|
||||||
|
|
||||||
|
it 'expects exception message to include database file' do
|
||||||
|
expect(subject.message).to include("#{db_file_name}")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'expects exception message to include database paths being back-up' do
|
||||||
|
expect(subject.message).to include("#{config[:host]}")
|
||||||
|
expect(subject.message).to include("#{config[:port]}")
|
||||||
|
expect(subject.message).to include("#{config[:database]}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Backup::FileBackupError do
|
||||||
|
let_it_be(:lfs) { create(:lfs_object) }
|
||||||
|
let_it_be(:upload) { create(:upload) }
|
||||||
|
|
||||||
|
let(:backup_tarball) { '/tmp/backup/uploads' }
|
||||||
|
|
||||||
|
shared_examples 'includes backup path' do
|
||||||
|
it { is_expected.to respond_to :app_files_dir }
|
||||||
|
it { is_expected.to respond_to :backup_tarball }
|
||||||
|
|
||||||
|
it 'expects exception message to include file backup path location' do
|
||||||
|
expect(subject.message).to include("#{subject.backup_tarball}")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'expects exception message to include file being back-up' do
|
||||||
|
expect(subject.message).to include("#{subject.app_files_dir}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with lfs file' do
|
||||||
|
subject { described_class.new(lfs, backup_tarball) }
|
||||||
|
|
||||||
|
it_behaves_like 'includes backup path'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with uploads file' do
|
||||||
|
subject { described_class.new(upload, backup_tarball) }
|
||||||
|
|
||||||
|
it_behaves_like 'includes backup path'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Backup::RepositoryBackupError do
|
||||||
|
let_it_be(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') }
|
||||||
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
|
let_it_be(:wiki) { ProjectWiki.new(project, nil ) }
|
||||||
|
|
||||||
|
let(:backup_repos_path) { '/tmp/backup/repositories' }
|
||||||
|
|
||||||
|
shared_examples 'includes backup path' do
|
||||||
|
it { is_expected.to respond_to :container }
|
||||||
|
it { is_expected.to respond_to :backup_repos_path }
|
||||||
|
|
||||||
|
it 'expects exception message to include repo backup path location' do
|
||||||
|
expect(subject.message).to include("#{subject.backup_repos_path}")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'expects exception message to include container being back-up' do
|
||||||
|
expect(subject.message).to include("#{subject.container.disk_path}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with snippet repository' do
|
||||||
|
subject { described_class.new(snippet, backup_repos_path) }
|
||||||
|
|
||||||
|
it_behaves_like 'includes backup path'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with project repository' do
|
||||||
|
subject { described_class.new(project, backup_repos_path) }
|
||||||
|
|
||||||
|
it_behaves_like 'includes backup path'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with wiki repository' do
|
||||||
|
subject { described_class.new(wiki, backup_repos_path) }
|
||||||
|
|
||||||
|
it_behaves_like 'includes backup path'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -18,7 +18,15 @@ RSpec.describe Gitlab::Conflict::File do
|
||||||
let(:conflict_file) { described_class.new(raw_conflict_file, merge_request: merge_request) }
|
let(:conflict_file) { described_class.new(raw_conflict_file, merge_request: merge_request) }
|
||||||
|
|
||||||
describe 'delegates' do
|
describe 'delegates' do
|
||||||
|
it { expect(conflict_file).to delegate_method(:type).to(:raw) }
|
||||||
|
it { expect(conflict_file).to delegate_method(:content).to(:raw) }
|
||||||
it { expect(conflict_file).to delegate_method(:path).to(:raw) }
|
it { expect(conflict_file).to delegate_method(:path).to(:raw) }
|
||||||
|
it { expect(conflict_file).to delegate_method(:ancestor_path).to(:raw) }
|
||||||
|
it { expect(conflict_file).to delegate_method(:their_path).to(:raw) }
|
||||||
|
it { expect(conflict_file).to delegate_method(:our_path).to(:raw) }
|
||||||
|
it { expect(conflict_file).to delegate_method(:our_mode).to(:raw) }
|
||||||
|
it { expect(conflict_file).to delegate_method(:our_blob).to(:raw) }
|
||||||
|
it { expect(conflict_file).to delegate_method(:repository).to(:raw) }
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#resolve_lines' do
|
describe '#resolve_lines' do
|
||||||
|
|
@ -328,4 +336,27 @@ RSpec.describe Gitlab::Conflict::File do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#conflict_type' do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
let(:rugged_conflict) { { ancestor: { path: ancestor_path }, theirs: { path: their_path }, ours: { path: our_path } } }
|
||||||
|
let(:diff_file) { double(renamed_file?: renamed_file?) }
|
||||||
|
|
||||||
|
subject(:conflict_type) { conflict_file.conflict_type(diff_file) }
|
||||||
|
|
||||||
|
where(:ancestor_path, :their_path, :our_path, :renamed_file?, :result) do
|
||||||
|
'/ancestor/path' | '/their/path' | '/our/path' | false | :both_modified
|
||||||
|
'/ancestor/path' | '' | '/our/path' | false | :modified_source_removed_target
|
||||||
|
'/ancestor/path' | '/their/path' | '' | false | :modified_target_removed_source
|
||||||
|
'' | '/their/path' | '/our/path' | false | :both_added
|
||||||
|
'' | '' | '/our/path' | false | :removed_target_renamed_source
|
||||||
|
'' | '' | '/our/path' | true | :renamed_same_file
|
||||||
|
'' | '/their/path' | '' | false | :removed_source_renamed_target
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it { expect(conflict_type).to eq(result) }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::Email::Message::InProductMarketing::TeamShort do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
let_it_be(:group) { build(:group) }
|
||||||
|
let_it_be(:user) { build(:user) }
|
||||||
|
|
||||||
|
let(:series) { 0 }
|
||||||
|
|
||||||
|
subject(:message) { described_class.new(group: group, user: user, series: series)}
|
||||||
|
|
||||||
|
describe 'public methods' do
|
||||||
|
it 'returns value for series', :aggregate_failures do
|
||||||
|
expect(message.subject_line).to eq 'Team up in GitLab for greater efficiency'
|
||||||
|
expect(message.tagline).to be_nil
|
||||||
|
expect(message.title).to eq 'Turn coworkers into collaborators'
|
||||||
|
expect(message.subtitle).to eq 'Invite your team today to build better code (and processes) together'
|
||||||
|
expect(message.body_line1).to be_empty
|
||||||
|
expect(message.body_line2).to be_empty
|
||||||
|
expect(message.cta_text).to eq 'Invite your colleagues today'
|
||||||
|
expect(message.logo_path).to eq 'mailers/in_product_marketing/team-0.png'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#progress' do
|
||||||
|
subject { message.progress }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'on gitlab.com' do
|
||||||
|
let(:is_gitlab_com) { true }
|
||||||
|
|
||||||
|
it { is_expected.to include('This is email 1 of 4 in the Team series') }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'not on gitlab.com' do
|
||||||
|
let(:is_gitlab_com) { false }
|
||||||
|
|
||||||
|
it { is_expected.to include('This is email 1 of 4 in the Team series', Gitlab::Routing.url_helpers.profile_notifications_url) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -23,6 +23,26 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Team do
|
||||||
expect(message.body_line2).to be_present
|
expect(message.body_line2).to be_present
|
||||||
expect(message.cta_text).to be_present
|
expect(message.cta_text).to be_present
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#progress' do
|
||||||
|
subject { message.progress }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'on gitlab.com' do
|
||||||
|
let(:is_gitlab_com) { true }
|
||||||
|
|
||||||
|
it { is_expected.to include("This is email #{series + 2} of 4 in the Team series") }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'not on gitlab.com' do
|
||||||
|
let(:is_gitlab_com) { false }
|
||||||
|
|
||||||
|
it { is_expected.to include("This is email #{series + 2} of 4 in the Team series", Gitlab::Routing.url_helpers.profile_notifications_url) }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with series 2' do
|
context 'with series 2' do
|
||||||
|
|
@ -37,6 +57,26 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Team do
|
||||||
expect(message.body_line2).to be_present
|
expect(message.body_line2).to be_present
|
||||||
expect(message.cta_text).to be_present
|
expect(message.cta_text).to be_present
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#progress' do
|
||||||
|
subject { message.progress }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'on gitlab.com' do
|
||||||
|
let(:is_gitlab_com) { true }
|
||||||
|
|
||||||
|
it { is_expected.to include('This is email 4 of 4 in the Team series') }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'not on gitlab.com' do
|
||||||
|
let(:is_gitlab_com) { false }
|
||||||
|
|
||||||
|
it { is_expected.to include('This is email 4 of 4 in the Team series', Gitlab::Routing.url_helpers.profile_notifications_url) }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue