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
|
||||
Style/RegexpLiteralMixedPreserve:
|
||||
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/spec/service/docker_run/gitlab_runner_spec.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;
|
||||
Mousetrap.bind(['mod+f', 'mod+g'], () => {
|
||||
keydownTime = new Date().getTime();
|
||||
|
@ -520,6 +523,11 @@ export default {
|
|||
// and max 1000ms to be sure it the search box is filtered
|
||||
if (delta >= 0 && delta < 1000) {
|
||||
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';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', mount2faAuthentication);
|
||||
mount2faAuthentication();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initEnvironmentsFolderBundle from '~/environments/folder/environments_folder_bundle';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initEnvironmentsFolderBundle);
|
||||
initEnvironmentsFolderBundle();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import monitoringApp from '~/monitoring/monitoring_app';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', monitoringApp);
|
||||
monitoringApp();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initTerminal from '~/terminal/';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initTerminal);
|
||||
initTerminal();
|
||||
|
|
|
@ -2,7 +2,7 @@ import $ from 'jquery';
|
|||
import ShortcutsNetwork from '~/behaviors/shortcuts/shortcuts_network';
|
||||
import Network from '../network';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
(() => {
|
||||
if (!$('.network-graph').length) return;
|
||||
|
||||
const networkGraph = new Network({
|
||||
|
@ -14,4 +14,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
|
||||
// eslint-disable-next-line no-new
|
||||
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 { s__ } from '~/locale';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import {
|
||||
visibilityOptions,
|
||||
visibilityLevelDescriptions,
|
||||
|
@ -48,7 +47,7 @@ export default {
|
|||
GlFormCheckbox,
|
||||
GlToggle,
|
||||
},
|
||||
mixins: [settingsMixin, glFeatureFlagsMixin()],
|
||||
mixins: [settingsMixin],
|
||||
|
||||
props: {
|
||||
requestCveAvailable: {
|
||||
|
@ -737,22 +736,5 @@ export default {
|
|||
}}</template>
|
||||
</gl-form-checkbox>
|
||||
</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>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
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>
|
||||
import { GlModal, GlSprintf } from '@gitlab/ui';
|
||||
import { GlModal, GlSprintf, GlFormInput } from '@gitlab/ui';
|
||||
import { n__ } from '~/locale';
|
||||
import {
|
||||
REMOVE_TAG_CONFIRMATION_TEXT,
|
||||
|
@ -12,6 +12,7 @@ export default {
|
|||
components: {
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
GlFormInput,
|
||||
},
|
||||
props: {
|
||||
itemsToBeDeleted: {
|
||||
|
@ -25,7 +26,15 @@ export default {
|
|||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
projectPath: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
imageProjectPath() {
|
||||
return this.itemsToBeDeleted[0]?.project?.path;
|
||||
},
|
||||
modalTitle() {
|
||||
if (this.deleteImage) {
|
||||
return DELETE_IMAGE_CONFIRMATION_TITLE;
|
||||
|
@ -40,6 +49,7 @@ export default {
|
|||
if (this.deleteImage) {
|
||||
return {
|
||||
message: DELETE_IMAGE_CONFIRMATION_TEXT,
|
||||
item: this.imageProjectPath,
|
||||
};
|
||||
}
|
||||
if (this.itemsToBeDeleted.length > 1) {
|
||||
|
@ -55,6 +65,9 @@ export default {
|
|||
item: first?.path,
|
||||
};
|
||||
},
|
||||
disablePrimaryButton() {
|
||||
return this.deleteImage && this.projectPath !== this.imageProjectPath;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
|
@ -69,10 +82,14 @@ export default {
|
|||
ref="deleteModal"
|
||||
modal-id="delete-tag-modal"
|
||||
ok-variant="danger"
|
||||
:action-primary="{ text: __('Confirm'), attributes: { variant: 'danger' } }"
|
||||
:action-primary="{
|
||||
text: __('Delete'),
|
||||
attributes: [{ variant: 'danger' }, { disabled: disablePrimaryButton }],
|
||||
}"
|
||||
:action-cancel="{ text: __('Cancel') }"
|
||||
@primary="$emit('confirmDelete')"
|
||||
@cancel="$emit('cancelDelete')"
|
||||
@change="projectPath = ''"
|
||||
>
|
||||
<template #modal-title>{{ modalTitle }}</template>
|
||||
<p v-if="modalDescription" data-testid="description">
|
||||
|
@ -80,7 +97,13 @@ export default {
|
|||
<template #item>
|
||||
<b>{{ modalDescription.item }}</b>
|
||||
</template>
|
||||
<template #code>
|
||||
<code>{{ modalDescription.item }}</code>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<div v-if="deleteImage">
|
||||
<gl-form-input v-model="projectPath" />
|
||||
</div>
|
||||
</gl-modal>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlIcon, GlTooltipDirective, GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||
import { sprintf, n__, s__ } from '~/locale';
|
||||
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
|
||||
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
||||
|
@ -27,7 +27,7 @@ import getContainerRepositoryTagsCountQuery from '../../graphql/queries/get_cont
|
|||
|
||||
export default {
|
||||
name: 'DetailsHeader',
|
||||
components: { GlButton, GlIcon, TitleArea, MetadataItem },
|
||||
components: { GlIcon, TitleArea, MetadataItem, GlDropdown, GlDropdownItem },
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
|
@ -143,9 +143,22 @@ export default {
|
|||
/>
|
||||
</template>
|
||||
<template #right-actions>
|
||||
<gl-button variant="danger" :disabled="deleteButtonDisabled" @click="$emit('delete')">
|
||||
{{ __('Delete image repository') }}
|
||||
</gl-button>
|
||||
<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') }}
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
</template>
|
||||
</title-area>
|
||||
</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_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__(
|
||||
|
|
|
@ -12,6 +12,7 @@ query getContainerRepositoryDetails($id: ID!) {
|
|||
expirationPolicyCleanupStatus
|
||||
project {
|
||||
visibility
|
||||
path
|
||||
containerExpirationPolicy {
|
||||
enabled
|
||||
nextRunAt
|
||||
|
|
|
@ -161,7 +161,7 @@ export default {
|
|||
},
|
||||
deleteImage() {
|
||||
this.deleteImageAlert = true;
|
||||
this.itemsToBeDeleted = [{ path: this.containerRepository.path }];
|
||||
this.itemsToBeDeleted = [{ ...this.containerRepository }];
|
||||
this.$refs.deleteModal.show();
|
||||
},
|
||||
deleteImageError() {
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
import initSourcegraph from './index';
|
||||
|
||||
/**
|
||||
* Load sourcegraph in it's own listener so that it's isolated from failures.
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', initSourcegraph);
|
||||
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,9 +16,20 @@ module CycleAnalyticsParams
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
private
|
||||
|
||||
def start_date(params)
|
||||
case params[:start_date]
|
||||
when '7'
|
||||
|
@ -41,6 +52,27 @@ module CycleAnalyticsParams
|
|||
date = field.is_a?(Date) || field.is_a?(Time) ? field : Date.parse(field)
|
||||
date.to_time.utc
|
||||
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
|
||||
|
||||
CycleAnalyticsParams.prepend_mod_with('CycleAnalyticsParams')
|
||||
|
|
|
@ -40,7 +40,7 @@ class Groups::EmailCampaignsController < Groups::ApplicationController
|
|||
project_pipelines_url(group.projects.first)
|
||||
when :trial
|
||||
'https://about.gitlab.com/free-trial/'
|
||||
when :team
|
||||
when :team, :team_short
|
||||
group_group_members_url(group)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Projects::Analytics::CycleAnalytics::StagesController < Projects::ApplicationController
|
||||
include ::Analytics::CycleAnalytics::StageActions
|
||||
extend ::Gitlab::Utils::Override
|
||||
|
||||
respond_to :json
|
||||
|
||||
feature_category :planning_analytics
|
||||
|
@ -8,37 +11,19 @@ class Projects::Analytics::CycleAnalytics::StagesController < Projects::Applicat
|
|||
before_action :authorize_read_cycle_analytics!
|
||||
before_action :only_default_value_stream_is_allowed!
|
||||
|
||||
def index
|
||||
result = list_service.execute
|
||||
private
|
||||
|
||||
if result.success?
|
||||
render json: cycle_analytics_configuration(result.payload[:stages])
|
||||
else
|
||||
render json: { message: result.message }, status: result.http_status
|
||||
end
|
||||
override :parent
|
||||
def parent
|
||||
@project
|
||||
end
|
||||
|
||||
private
|
||||
override :value_stream_class
|
||||
def value_stream_class
|
||||
Analytics::CycleAnalytics::ProjectValueStream
|
||||
end
|
||||
|
||||
def only_default_value_stream_is_allowed!
|
||||
render_404 if params[:value_stream_id] != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
|
||||
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
|
||||
|
|
|
@ -57,8 +57,14 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
|
|||
def diffs_metadata
|
||||
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)
|
||||
.represent(diffs, additional_attributes.merge(only_context_commits: show_only_context_commits?))
|
||||
.represent(diffs, options)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -44,6 +44,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
# Usage data feature flags
|
||||
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_searching_usage_data, @project, default_enabled: :yaml)
|
||||
|
||||
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
|
||||
experiment_instance.exclude! unless helpers.can_import_members?
|
||||
|
|
|
@ -31,10 +31,6 @@ class ProjectsController < Projects::ApplicationController
|
|||
# Project Export Rate Limit
|
||||
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
|
||||
push_frontend_feature_flag(:refactor_blob_viewer, @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[
|
||||
show_default_award_emojis
|
||||
squash_option
|
||||
allow_editing_commit_messages
|
||||
mr_default_target_self
|
||||
]
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@ class SearchController < ApplicationController
|
|||
include SearchHelper
|
||||
include RedisTracking
|
||||
|
||||
RESCUE_FROM_TIMEOUT_ACTIONS = [:count, :show].freeze
|
||||
|
||||
track_redis_hll_event :show, name: 'i_search_total'
|
||||
|
||||
around_action :allow_gitaly_ref_name_caching
|
||||
|
@ -154,12 +156,21 @@ class SearchController < ApplicationController
|
|||
end
|
||||
|
||||
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)
|
||||
|
||||
@timeout = true
|
||||
render status: :request_timeout
|
||||
|
||||
if count_action_name?
|
||||
render json: {}, status: :request_timeout
|
||||
else
|
||||
render status: :request_timeout
|
||||
end
|
||||
end
|
||||
|
||||
def count_action_name?
|
||||
action_name.to_sym == :count
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -31,6 +31,12 @@ module Mutations
|
|||
def ready?(**args)
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
@ -7,17 +7,8 @@ module Mutations
|
|||
|
||||
argument :due_date,
|
||||
Types::TimeType,
|
||||
required: false,
|
||||
description: 'The desired due date for the issue, ' \
|
||||
'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
|
||||
required: :nullable,
|
||||
description: 'The desired due date for the issue. Due date is removed if null.'
|
||||
|
||||
def resolve(project_path:, iid:, due_date:)
|
||||
issue = authorized_find!(project_path: project_path, iid: iid)
|
||||
|
|
|
@ -10,7 +10,29 @@ module Types
|
|||
@deprecation = gitlab_deprecation(kwargs)
|
||||
@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)
|
||||
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
|
||||
|
|
|
@ -491,7 +491,6 @@ module ProjectsHelper
|
|||
|
||||
def project_permissions_settings(project)
|
||||
feature = project.project_feature
|
||||
|
||||
{
|
||||
packagesEnabled: !!project.packages_enabled,
|
||||
visibilityLevel: project.visibility_level,
|
||||
|
@ -511,7 +510,6 @@ module ProjectsHelper
|
|||
metricsDashboardAccessLevel: feature.metrics_dashboard_access_level,
|
||||
operationsAccessLevel: feature.operations_access_level,
|
||||
showDefaultAwardEmojis: project.show_default_award_emojis?,
|
||||
allowEditingCommitMessages: project.allow_editing_commit_messages?,
|
||||
securityAndComplianceAccessLevel: project.security_and_compliance_access_level
|
||||
}
|
||||
end
|
||||
|
|
|
@ -69,21 +69,26 @@ class WebHook < ApplicationRecord
|
|||
end
|
||||
|
||||
def disable!
|
||||
update!(recent_failures: FAILURE_THRESHOLD + 1)
|
||||
update_attribute(:recent_failures, FAILURE_THRESHOLD + 1)
|
||||
end
|
||||
|
||||
def enable!
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# 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 :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true
|
||||
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
|
||||
delegate :active?, to: :prometheus_integration, allow_nil: true, prefix: true
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
|
||||
enum squash_option: {
|
||||
|
|
|
@ -19,7 +19,8 @@ module Users
|
|||
verify: 1,
|
||||
trial: 2,
|
||||
team: 3,
|
||||
experience: 4
|
||||
experience: 4,
|
||||
team_short: 5
|
||||
}, _suffix: true
|
||||
|
||||
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
|
||||
|
||||
class DiffFileEntity < DiffFileBaseEntity
|
||||
include DiffFileConflictType
|
||||
include CommitsHelper
|
||||
include IconsHelper
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
@ -88,10 +89,4 @@ class DiffFileEntity < DiffFileBaseEntity
|
|||
# If nothing is present, inline will be the default.
|
||||
options.fetch(:diff_view, :inline).to_sym
|
||||
end
|
||||
|
||||
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,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DiffFileMetadataEntity < Grape::Entity
|
||||
include DiffFileConflictType
|
||||
|
||||
expose :added_lines
|
||||
expose :removed_lines
|
||||
expose :new_path
|
||||
|
|
|
@ -2,8 +2,13 @@
|
|||
|
||||
class DiffsMetadataEntity < DiffsEntity
|
||||
unexpose :diff_files
|
||||
expose :diff_files, using: DiffFileMetadataEntity do |diffs, _|
|
||||
diffs.raw_diff_files(sorted: true)
|
||||
expose :diff_files do |diffs, options|
|
||||
DiffFileMetadataEntity.represent(
|
||||
diffs.raw_diff_files(sorted: true),
|
||||
options.merge(
|
||||
conflicts: conflicts(allow_tree_conflicts: options[:allow_tree_conflicts])
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
expose :conflict_resolution_path do |_, options|
|
||||
|
|
|
@ -8,8 +8,13 @@ module Namespaces
|
|||
completed_actions: [:created],
|
||||
incomplete_actions: [:git_write]
|
||||
},
|
||||
team_short: {
|
||||
interval_days: [1],
|
||||
completed_actions: [:git_write],
|
||||
incomplete_actions: [:user_added]
|
||||
},
|
||||
verify: {
|
||||
interval_days: [1, 5, 10],
|
||||
interval_days: [2, 6, 11],
|
||||
completed_actions: [:git_write],
|
||||
incomplete_actions: [:pipeline_created]
|
||||
},
|
||||
|
@ -98,13 +103,11 @@ module Namespaces
|
|||
|
||||
def can_perform_action?(user, group)
|
||||
case track
|
||||
when :create
|
||||
user.can?(:create_projects, group)
|
||||
when :verify
|
||||
when :create, :verify
|
||||
user.can?(:create_projects, group)
|
||||
when :trial
|
||||
user.can?(:start_trial, group)
|
||||
when :team
|
||||
when :team, :team_short
|
||||
user.can?(:admin_group_member, group)
|
||||
when :experience
|
||||
true
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
%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_errors(token)
|
||||
|
@ -7,23 +9,26 @@
|
|||
.form-group
|
||||
= 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
|
||||
.text-secondary= s_('DeployTokens|Enter a unique name for your deploy token.')
|
||||
|
||||
.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
|
||||
.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
|
||||
= f.label :username, _('Username (optional)'), class: 'label-bold'
|
||||
= 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
|
||||
= 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
|
||||
= 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'
|
||||
.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)
|
||||
%fieldset.form-group.form-check
|
||||
|
@ -34,18 +39,18 @@
|
|||
%fieldset.form-group.form-check
|
||||
= f.check_box :write_registry, class: 'form-check-input'
|
||||
= 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)
|
||||
%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.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
|
||||
= f.check_box :write_package_registry, class: 'form-check-input'
|
||||
= 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
|
||||
= 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?
|
||||
= render 'shared/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token
|
||||
%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
|
||||
%hr
|
||||
= 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_user_load_conflict_ui'
|
||||
- 'i_code_review_user_resolve_conflict'
|
||||
- 'i_code_review_user_searches_diff'
|
||||
- name: code_review_category_monthly_active_users
|
||||
operator: OR
|
||||
source: redis
|
||||
|
@ -122,6 +123,7 @@
|
|||
- 'i_code_review_diff_multiple_files'
|
||||
- 'i_code_review_user_load_conflict_ui'
|
||||
- 'i_code_review_user_resolve_conflict'
|
||||
- 'i_code_review_user_searches_diff'
|
||||
- name: code_review_extension_category_monthly_active_users
|
||||
operator: OR
|
||||
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'
|
||||
scope module: :cycle_analytics, as: 'cycle_analytics', path: 'value_stream_analytics' 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
|
||||
resource :summary, controller: :summary, only: :show
|
||||
end
|
||||
|
|
|
@ -6,14 +6,10 @@ class AddAllowToEditCommitToProjectSettings < ActiveRecord::Migration[6.0]
|
|||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
add_column :project_settings, :allow_editing_commit_messages, :boolean, default: false, null: false
|
||||
end
|
||||
# no-op
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_column :project_settings, :allow_editing_commit_messages
|
||||
end
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
|
@ -54,6 +54,16 @@ Returns [`CiConfig`](#ciconfig).
|
|||
| <a id="queryciconfigprojectpath"></a>`projectPath` | [`ID!`](#id) | The project of the CI config. |
|
||||
| <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`
|
||||
|
||||
Find a container repository.
|
||||
|
@ -2533,7 +2543,7 @@ Input type: `IssueSetDueDateInput`
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <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="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="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`
|
||||
|
||||
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)) |
|
||||
|
||||
### `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`
|
||||
|
||||
#### Fields
|
||||
|
|
|
@ -3277,7 +3277,7 @@ dashboards.
|
|||
> - [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.
|
||||
|
||||
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
|
||||
in merge requests. For more information, see
|
||||
[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."
|
||||
```
|
||||
|
||||
#### 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
|
||||
of a mutation as keyword arguments.
|
||||
|
||||
|
@ -1377,6 +1402,8 @@ def resolve(my_arg:)
|
|||
end
|
||||
```
|
||||
|
||||
#### Input Types
|
||||
|
||||
`graphql-ruby` wraps up arguments into an
|
||||
[input type](https://graphql.org/learn/schema/#input-types).
|
||||
|
||||
|
|
|
@ -2638,6 +2638,34 @@ Status: `data_available`
|
|||
|
||||
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`
|
||||
|
||||
Total clicks on the verify trial's first email
|
||||
|
@ -7650,6 +7678,20 @@ Status: `data_available`
|
|||
|
||||
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`
|
||||
|
||||
Missing description
|
||||
|
@ -10646,6 +10688,20 @@ Status: `data_available`
|
|||
|
||||
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`
|
||||
|
||||
Count of unique users per month who added a suggestion
|
||||
|
@ -11514,6 +11570,20 @@ Status: `data_available`
|
|||
|
||||
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`
|
||||
|
||||
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 |
|
||||
|------------|---------------------------|------------------------------------|
|
||||
| [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. |
|
||||
| Your own server | The Jira DVCS (distributed version control system) connector. This syncs data hourly. | The [Jira DVCS Connector](dvcs.md). |
|
||||
| [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](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
|
||||
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
|
||||
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
|
||||
|
||||
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>
|
||||
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),
|
||||
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)**
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
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 **Move to Done** option.
|
||||
|
||||
|
@ -167,7 +167,7 @@ For advanced workflows, you can specify custom Jira transition IDs:
|
|||
**action** parameter in the URL.
|
||||
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.
|
||||
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 **Custom transitions** option.
|
||||
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
|
||||
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.
|
||||
|
||||
## 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. 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
|
||||
[configure GitLab](development_panel.md#configure-gitlab).
|
||||
[configure GitLab](configure.md).
|
||||
|
||||
You need the newly created token, and the email
|
||||
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
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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,
|
||||
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.
|
||||
|
||||
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
|
||||
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 :expires_at, type: DateTime, desc: 'Date string in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`)'
|
||||
end
|
||||
put ":id/invitations/:email", requirements: { email: /[^\/]+/ } do
|
||||
put ":id/invitations/:email", requirements: { email: %r{[^/]+} } do
|
||||
source = find_source(source_type, params.delete(:id))
|
||||
invite_email = params[:email]
|
||||
authorize_admin_source!(source_type, source)
|
||||
|
@ -88,7 +88,7 @@ module API
|
|||
params do
|
||||
requires :email, type: String, desc: 'The email address of the invitation'
|
||||
end
|
||||
delete ":id/invitations/:email", requirements: { email: /[^\/]+/ } do
|
||||
delete ":id/invitations/:email", requirements: { email: %r{[^/]+} } do
|
||||
source = find_source(source_type, params[:id])
|
||||
invite_email = params[:email]
|
||||
authorize_admin_source!(source_type, source)
|
||||
|
|
|
@ -2,4 +2,43 @@
|
|||
|
||||
module Backup
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
def initialize(regexp)
|
||||
super(regexp.gsub(/\\\//, '/'))
|
||||
super(regexp.gsub(%r{\\/}, '/'))
|
||||
|
||||
unless Gitlab::UntrustedRegexp::RubySyntax.valid?(@value)
|
||||
raise Lexer::SyntaxError, 'Invalid regular expression!'
|
||||
|
|
|
@ -23,7 +23,7 @@ module Gitlab
|
|||
# 'raw' holds the Gitlab::Git::Conflict::File that this instance wraps
|
||||
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:)
|
||||
@raw = raw
|
||||
|
@ -227,6 +227,26 @@ module Gitlab
|
|||
new_path: our_path)
|
||||
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
|
||||
|
||||
def map_raw_lines(raw_lines)
|
||||
|
|
|
@ -67,11 +67,11 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def progress
|
||||
def progress(current: series + 1, total: total_series, track_name: track.to_s.humanize)
|
||||
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
|
||||
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
|
||||
|
||||
|
@ -109,7 +109,7 @@ module Gitlab
|
|||
private
|
||||
|
||||
def track
|
||||
self.class.name.demodulize.downcase.to_sym
|
||||
self.class.name.demodulize.underscore.to_sym
|
||||
end
|
||||
|
||||
def total_series
|
||||
|
|
|
@ -73,6 +73,10 @@ module Gitlab
|
|||
s_('InProductMarketing|Invite your team now')
|
||||
][series]
|
||||
end
|
||||
|
||||
def progress
|
||||
super(current: series + 2, total: 4)
|
||||
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
|
||||
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
|
||||
|
||||
def initialize(repository, commit_oid, conflict, raw_content)
|
||||
@repository = repository
|
||||
@commit_oid = commit_oid
|
||||
@ancestor_path = conflict[:ancestor][:path]
|
||||
@their_path = conflict[:theirs][:path]
|
||||
@our_path = conflict[:ours][:path]
|
||||
@our_mode = conflict[:ours][:mode]
|
||||
|
|
|
@ -43,6 +43,7 @@ module Gitlab
|
|||
|
||||
def conflict_from_gitaly_file_header(header)
|
||||
{
|
||||
ancestor: { path: header.ancestor_path },
|
||||
ours: { path: header.our_path, mode: header.our_mode },
|
||||
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)
|
||||
}.freeze
|
||||
|
||||
HEALTH_ENDPOINT = /^\/-\/(liveness|readiness|health|metrics)\/?$/.freeze
|
||||
HEALTH_ENDPOINT = %r{^/-/(liveness|readiness|health|metrics)/?$}.freeze
|
||||
|
||||
FEATURE_CATEGORY_DEFAULT = 'unknown'
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ module Gitlab
|
|||
|
||||
IGNORABLE_SQL = %w{BEGIN COMMIT}.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
|
||||
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 port number, such as :8080 or :8443
|
||||
|
||||
@go_package_regex ||= /
|
||||
@go_package_regex ||= %r{
|
||||
\b (?# word boundary)
|
||||
(?<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)
|
||||
\.[a-z]{2,} (?# top-level domain)
|
||||
)
|
||||
(?<path>\/(?:
|
||||
[-\/$_.+!*'(),0-9a-z] (?# plain URL character)
|
||||
(?<path>/(?:
|
||||
[-/$_.+!*'(),0-9a-z] (?# plain URL character)
|
||||
| %[0-9a-f]{2})* (?# URL encoded character)
|
||||
)? (?# path)
|
||||
\b (?# word boundary)
|
||||
/ix.freeze
|
||||
}ix.freeze
|
||||
end
|
||||
|
||||
def generic_package_version_regex
|
||||
|
@ -416,7 +416,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def feature_flag_regex
|
||||
|
|
|
@ -15,7 +15,8 @@ module Gitlab
|
|||
MergeRequestCounter,
|
||||
DesignsCounter,
|
||||
KubernetesAgentCounter,
|
||||
StaticSiteEditorCounter
|
||||
StaticSiteEditorCounter,
|
||||
DiffsCounter
|
||||
].freeze
|
||||
|
||||
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
|
||||
category: code_review
|
||||
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)
|
||||
|
||||
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)
|
||||
raise PathTraversalAttackError, 'Invalid path'
|
||||
|
|
|
@ -6,6 +6,6 @@ module ProductAnalytics
|
|||
URL = Gitlab.config.gitlab.url + '/-/sp.js'
|
||||
|
||||
# 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
|
||||
|
|
|
@ -8612,7 +8612,7 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Delete selected tags"
|
||||
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 ""
|
||||
|
||||
msgid "ContainerRegistry|Deletion disabled due to missing or insufficient permissions."
|
||||
|
@ -10918,30 +10918,30 @@ msgstr ""
|
|||
msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
|
||||
msgstr ""
|
||||
|
||||
msgid "DeployTokens|Add a deploy token"
|
||||
msgid "DeployTokens|Allows read and write access to registry images."
|
||||
msgstr ""
|
||||
|
||||
msgid "DeployTokens|Allows read access to the package registry."
|
||||
msgid "DeployTokens|Allows read and write access to the package registry."
|
||||
msgstr ""
|
||||
|
||||
msgid "DeployTokens|Allows read-only access to registry images."
|
||||
msgstr ""
|
||||
|
||||
msgid "DeployTokens|Allows read-only access to the package registry."
|
||||
msgstr ""
|
||||
|
||||
msgid "DeployTokens|Allows read-only access to the repository."
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
msgid "DeployTokens|Copy username"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10954,6 +10954,15 @@ msgstr ""
|
|||
msgid "DeployTokens|Deploy tokens allow access to packages, your repository, and registry images."
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10963,7 +10972,7 @@ msgstr ""
|
|||
msgid "DeployTokens|Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "DeployTokens|Pick a name for your unique deploy token."
|
||||
msgid "DeployTokens|New deploy token"
|
||||
msgstr ""
|
||||
|
||||
msgid "DeployTokens|Revoke"
|
||||
|
@ -10984,12 +10993,6 @@ msgstr ""
|
|||
msgid "DeployTokens|This username supports access. %{link_start}What kind of access?%{link_end}"
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
|
@ -13333,6 +13336,9 @@ msgstr ""
|
|||
msgid "Expiration date"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expiration date (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expired"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13345,9 +13351,6 @@ msgstr ""
|
|||
msgid "Expires"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expires at (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expires in %{expires_at}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -17001,6 +17004,9 @@ msgstr ""
|
|||
msgid "InProductMarketing|Invite your team now"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Invite your team today to build better code (and processes) together"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|It's all in the stats"
|
||||
msgstr ""
|
||||
|
||||
|
@ -17082,6 +17088,9 @@ msgstr ""
|
|||
msgid "InProductMarketing|Take your source code management to the next level"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Team up in GitLab for greater efficiency"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Team work makes the dream work"
|
||||
msgstr ""
|
||||
|
||||
|
@ -17118,6 +17127,9 @@ msgstr ""
|
|||
msgid "InProductMarketing|Try it yourself"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Turn coworkers into collaborators"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Twitter"
|
||||
msgstr ""
|
||||
|
||||
|
@ -25701,9 +25713,6 @@ msgstr ""
|
|||
msgid "ProjectSettings|Allow"
|
||||
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."
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Commit authors can edit commit messages on unprotected branches."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Configure your project resources and monitor their health."
|
||||
msgstr ""
|
||||
|
||||
|
@ -28655,7 +28661,7 @@ msgstr ""
|
|||
msgid "Scopes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Scopes [Select 1 or more]"
|
||||
msgid "Scopes (select at least one)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Scopes can't be blank"
|
||||
|
@ -38304,9 +38310,6 @@ msgstr ""
|
|||
msgid "can only have one escalation policy"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -7,26 +7,58 @@ RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do
|
|||
let_it_be(:group) { create(: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
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
describe 'GET index' do
|
||||
context 'when user is member of the project' do
|
||||
shared_examples 'project-level value stream analytics endpoint' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it 'succeeds' do
|
||||
get action, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'project-level value stream analytics request error examples' do
|
||||
context 'when invalid value stream id is given' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
params[:value_stream_id] = 1
|
||||
end
|
||||
|
||||
it 'succeeds' do
|
||||
get :index, params: params
|
||||
it 'renders 404' do
|
||||
get action, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
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
|
||||
get :index, params: params
|
||||
get action, params: params
|
||||
|
||||
expect(json_response['stages'].size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size)
|
||||
end
|
||||
|
@ -37,31 +69,109 @@ RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do
|
|||
expect(list_service).to receive(:allowed?).and_return(false)
|
||||
end
|
||||
|
||||
get :index, params: params
|
||||
get action, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invalid value stream id is given' do
|
||||
before do
|
||||
params[:value_stream_id] = 1
|
||||
end
|
||||
it_behaves_like 'project-level value stream analytics request error examples'
|
||||
end
|
||||
|
||||
it 'renders 404' do
|
||||
get :index, params: params
|
||||
describe 'GET median' do
|
||||
let(:action) { :median }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
before do
|
||||
params[:id] = 'issue'
|
||||
end
|
||||
|
||||
it_behaves_like 'project-level value stream analytics endpoint' do
|
||||
it 'returns the median' do
|
||||
result = 2
|
||||
|
||||
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
|
||||
|
||||
context 'when user is not member of the project' do
|
||||
it 'renders 404' do
|
||||
get :index, params: params
|
||||
it_behaves_like 'project-level value stream analytics request error examples'
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
@ -141,6 +141,24 @@ RSpec.describe Projects::MergeRequests::DiffsController do
|
|||
end
|
||||
|
||||
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 = {})
|
||||
params = {
|
||||
namespace_id: project.namespace.to_param,
|
||||
|
@ -179,32 +197,25 @@ RSpec.describe Projects::MergeRequests::DiffsController do
|
|||
end
|
||||
|
||||
context 'with valid diff_id' do
|
||||
it 'returns success' do
|
||||
go(diff_id: merge_request.merge_request_diff.id)
|
||||
subject { go(diff_id: merge_request.merge_request_diff.id) }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'serializes diffs metadata with expected arguments' do
|
||||
expected_options = {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
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: true,
|
||||
merge_ref_head_diff: false
|
||||
}
|
||||
end
|
||||
|
||||
go(diff_id: merge_request.merge_request_diff.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -261,62 +272,75 @@ RSpec.describe Projects::MergeRequests::DiffsController do
|
|||
end
|
||||
|
||||
context 'with MR regular diff params' do
|
||||
it 'returns success' do
|
||||
go
|
||||
subject { go }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'serializes diffs metadata with expected arguments' do
|
||||
expected_options = {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
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: true,
|
||||
merge_ref_head_diff: nil
|
||||
}
|
||||
end
|
||||
|
||||
go
|
||||
end
|
||||
end
|
||||
|
||||
context 'with commit param' do
|
||||
it 'returns success' do
|
||||
go(commit_id: merge_request.diff_head_sha)
|
||||
subject { 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
|
||||
let(:collection) { Gitlab::Diff::FileCollection::Commit }
|
||||
let(:expected_options) do
|
||||
{
|
||||
environment: nil,
|
||||
merge_request: merge_request,
|
||||
merge_request_diff: nil,
|
||||
merge_request_diffs: merge_request.merge_request_diffs,
|
||||
start_version: nil,
|
||||
start_sha: nil,
|
||||
commit: merge_request.diff_head_commit,
|
||||
latest_diff: nil,
|
||||
only_context_commits: false,
|
||||
allow_tree_conflicts: true,
|
||||
merge_ref_head_diff: nil
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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 'serializes diffs metadata with expected arguments' do
|
||||
expected_options = {
|
||||
environment: nil,
|
||||
merge_request: merge_request,
|
||||
merge_request_diff: nil,
|
||||
merge_request_diffs: merge_request.merge_request_diffs,
|
||||
start_version: nil,
|
||||
start_sha: nil,
|
||||
commit: merge_request.diff_head_commit,
|
||||
latest_diff: nil,
|
||||
only_context_commits: false
|
||||
}
|
||||
|
||||
expect_next_instance_of(DiffsMetadataSerializer) do |instance|
|
||||
expect(instance).to receive(:represent)
|
||||
.with(an_instance_of(Gitlab::Diff::FileCollection::Commit), expected_options)
|
||||
.and_call_original
|
||||
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
|
||||
|
||||
go(commit_id: merge_request.diff_head_sha)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -767,8 +767,7 @@ RSpec.describe ProjectsController do
|
|||
id: project.path,
|
||||
project: {
|
||||
project_setting_attributes: {
|
||||
show_default_award_emojis: boolean_value,
|
||||
allow_editing_commit_messages: boolean_value
|
||||
show_default_award_emojis: boolean_value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -776,7 +775,6 @@ RSpec.describe ProjectsController do
|
|||
project.reload
|
||||
|
||||
expect(project.show_default_award_emojis?).to eq(result)
|
||||
expect(project.allow_editing_commit_messages?).to eq(result)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -53,6 +53,20 @@ RSpec.describe SearchController do
|
|||
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
|
||||
it_behaves_like 'when the user cannot read cross project', :show, { search: 'hello' } do
|
||||
it 'still allows accessing the search page' do
|
||||
|
@ -63,6 +77,7 @@ RSpec.describe SearchController do
|
|||
end
|
||||
|
||||
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
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
@ -230,6 +245,7 @@ RSpec.describe SearchController do
|
|||
describe 'GET #count' do
|
||||
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 '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
|
||||
create(:project, :public, name: 'hello world')
|
||||
|
|
|
@ -27,7 +27,6 @@ const defaultProps = {
|
|||
emailsDisabled: false,
|
||||
packagesEnabled: true,
|
||||
showDefaultAwardEmojis: true,
|
||||
allowEditingCommitMessages: false,
|
||||
},
|
||||
isGitlabCom: true,
|
||||
canDisableEmails: true,
|
||||
|
@ -53,7 +52,7 @@ describe('Settings Panel', () => {
|
|||
let wrapper;
|
||||
|
||||
const mountComponent = (
|
||||
{ currentSettings = {}, glFeatures = {}, ...customProps } = {},
|
||||
{ currentSettings = {}, ...customProps } = {},
|
||||
mountFn = shallowMount,
|
||||
) => {
|
||||
const propsData = {
|
||||
|
@ -64,9 +63,6 @@ describe('Settings Panel', () => {
|
|||
|
||||
return mountFn(settingsPanel, {
|
||||
propsData,
|
||||
provide: {
|
||||
glFeatures,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -100,8 +96,6 @@ describe('Settings Panel', () => {
|
|||
const findShowDefaultAwardEmojis = () =>
|
||||
wrapper.find('input[name="project[project_setting_attributes][show_default_award_emojis]"]');
|
||||
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' });
|
||||
|
||||
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', () => {
|
||||
it('should show the analytics toggle', () => {
|
||||
wrapper = mountComponent();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { GlSprintf } from '@gitlab/ui';
|
||||
import { GlSprintf, GlFormInput } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import component from '~/registry/explorer/components/details_page/delete_modal.vue';
|
||||
import {
|
||||
REMOVE_TAG_CONFIRMATION_TEXT,
|
||||
|
@ -12,8 +13,9 @@ import { GlModal } from '../../stubs';
|
|||
describe('Delete Modal', () => {
|
||||
let wrapper;
|
||||
|
||||
const findModal = () => wrapper.find(GlModal);
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
const findDescription = () => wrapper.find('[data-testid="description"]');
|
||||
const findInputComponent = () => wrapper.findComponent(GlFormInput);
|
||||
|
||||
const mountComponent = (propsData) => {
|
||||
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(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
|
@ -65,11 +74,49 @@ describe('Delete Modal', () => {
|
|||
it('has the correct description', () => {
|
||||
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', () => {
|
||||
it('delete button is enabled', () => {
|
||||
mountComponent();
|
||||
|
||||
expectPrimaryActionStatus(false);
|
||||
});
|
||||
|
||||
describe('itemsToBeDeleted contains one element', () => {
|
||||
beforeEach(() => {
|
||||
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 VueApollo from 'vue-apollo';
|
||||
import { useFakeDate } from 'helpers/fake_date';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
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 {
|
||||
UNSCHEDULED_STATUS,
|
||||
|
@ -48,8 +49,8 @@ describe('Details Header', () => {
|
|||
const findTitle = () => findByTestId('title');
|
||||
const findTagsCount = () => findByTestId('tags-count');
|
||||
const findCleanup = () => findByTestId('cleanup');
|
||||
const findDeleteButton = () => wrapper.find(GlButton);
|
||||
const findInfoIcon = () => wrapper.find(GlIcon);
|
||||
const findDeleteButton = () => wrapper.findComponent(GlDropdownItem);
|
||||
const findInfoIcon = () => wrapper.findComponent(GlIcon);
|
||||
|
||||
const waitForMetadataItems = async () => {
|
||||
// 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,
|
||||
stubs: {
|
||||
TitleArea,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -152,10 +155,11 @@ describe('Details Header', () => {
|
|||
it('has the correct props', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findDeleteButton().props()).toMatchObject({
|
||||
variant: 'danger',
|
||||
disabled: false,
|
||||
});
|
||||
expect(findDeleteButton().attributes()).toMatchObject(
|
||||
expect.objectContaining({
|
||||
variant: 'danger',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('emits the correct event', () => {
|
||||
|
@ -168,16 +172,16 @@ describe('Details Header', () => {
|
|||
|
||||
it.each`
|
||||
canDelete | disabled | isDisabled
|
||||
${true} | ${false} | ${false}
|
||||
${true} | ${true} | ${true}
|
||||
${false} | ${false} | ${true}
|
||||
${false} | ${true} | ${true}
|
||||
${true} | ${false} | ${undefined}
|
||||
${true} | ${true} | ${'true'}
|
||||
${false} | ${false} | ${'true'}
|
||||
${false} | ${true} | ${'true'}
|
||||
`(
|
||||
'when canDelete is $canDelete and disabled is $disabled is $isDisabled that the button is disabled',
|
||||
({ canDelete, disabled, isDisabled }) => {
|
||||
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',
|
||||
project: {
|
||||
visibility: 'public',
|
||||
path: 'gitlab-test',
|
||||
containerExpirationPolicy: {
|
||||
enabled: false,
|
||||
nextRunAt: '2020-11-27T08:59:27Z',
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
GlModal as RealGlModal,
|
||||
GlEmptyState as RealGlEmptyState,
|
||||
GlSkeletonLoader as RealGlSkeletonLoader,
|
||||
GlDropdown as RealGlDropdown,
|
||||
} from '@gitlab/ui';
|
||||
import { RouterLinkStub } from '@vue/test-utils';
|
||||
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) { {} }
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ RSpec.describe Mutations::Ci::Runner::Update do
|
|||
let(:mutation_params) { {} }
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -3,15 +3,41 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Types::BaseArgument do
|
||||
include_examples 'Gitlab-style deprecations' do
|
||||
let_it_be(:field) do
|
||||
Types::BaseField.new(name: 'field', type: String, null: true)
|
||||
let_it_be(:field) do
|
||||
Types::BaseField.new(name: 'field', type: String, null: true)
|
||||
end
|
||||
|
||||
let(:base_args) { { name: 'test', type: String, required: false, owner: field } }
|
||||
|
||||
def subject(args = {})
|
||||
described_class.new(**base_args.merge(args))
|
||||
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
|
||||
|
||||
let(:base_args) { { name: 'test', type: String, required: false, owner: field } }
|
||||
it 'accepts required, non-nullable arguments' do
|
||||
arguments = base_args.merge({ required: true })
|
||||
|
||||
def subject(args = {})
|
||||
described_class.new(**base_args.merge(args))
|
||||
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
|
||||
|
|
|
@ -917,34 +917,4 @@ RSpec.describe ProjectsHelper do
|
|||
subject
|
||||
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
|
||||
|
|
|
@ -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) }
|
||||
|
||||
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(: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
|
||||
|
||||
describe '#resolve_lines' do
|
||||
|
@ -328,4 +336,27 @@ RSpec.describe Gitlab::Conflict::File do
|
|||
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
|
||||
|
|
|
@ -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.cta_text).to be_present
|
||||
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
|
||||
|
||||
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.cta_text).to be_present
|
||||
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
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue