Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-07-19 15:09:10 +00:00
parent 690c904b5e
commit 9c8e8b5ffc
90 changed files with 1283 additions and 761 deletions

View File

@ -4153,7 +4153,6 @@ Layout/LineLength:
- 'spec/features/action_cable_logging_spec.rb'
- 'spec/features/admin/admin_abuse_reports_spec.rb'
- 'spec/features/admin/admin_mode/login_spec.rb'
- 'spec/features/admin/admin_runners_spec.rb'
- 'spec/features/admin/admin_sees_background_migrations_spec.rb'
- 'spec/features/admin/admin_sees_project_statistics_spec.rb'
- 'spec/features/admin/admin_settings_spec.rb'

View File

@ -185,7 +185,6 @@ Layout/SpaceInsideParens:
- 'spec/controllers/projects/runners_controller_spec.rb'
- 'spec/dependencies/omniauth_saml_spec.rb'
- 'spec/factories/usage_data.rb'
- 'spec/features/admin/admin_runners_spec.rb'
- 'spec/features/boards/board_filters_spec.rb'
- 'spec/features/boards/user_visits_board_spec.rb'
- 'spec/features/dashboard/datetime_on_tooltips_spec.rb'

View File

@ -1397,7 +1397,6 @@ RSpec/ContextWording:
- 'spec/features/admin/admin_mode/logout_spec.rb'
- 'spec/features/admin/admin_mode/workers_spec.rb'
- 'spec/features/admin/admin_mode_spec.rb'
- 'spec/features/admin/admin_runners_spec.rb'
- 'spec/features/admin/admin_search_settings_spec.rb'
- 'spec/features/admin/admin_settings_spec.rb'
- 'spec/features/admin/dashboard_spec.rb'
@ -1444,7 +1443,6 @@ RSpec/ContextWording:
- 'spec/features/groups/dependency_proxy_for_containers_spec.rb'
- 'spec/features/groups/dependency_proxy_spec.rb'
- 'spec/features/groups/empty_states_spec.rb'
- 'spec/features/groups/group_runners_spec.rb'
- 'spec/features/groups/group_settings_spec.rb'
- 'spec/features/groups/issues_spec.rb'
- 'spec/features/groups/labels/subscription_spec.rb'

View File

@ -87,7 +87,6 @@ RSpec/InstanceVariable:
- spec/controllers/profiles/avatars_controller_spec.rb
- spec/controllers/projects/clusters_controller_spec.rb
- spec/controllers/sessions_controller_spec.rb
- spec/features/admin/admin_runners_spec.rb
- spec/features/calendar_spec.rb
- spec/features/issues/user_filters_issues_spec.rb
- spec/features/markdown/copy_as_gfm_spec.rb

View File

@ -2,6 +2,16 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 15.1.3 (2022-07-19)
### Added (1 change)
- [Add praefect list virtual storages subcommand documentation](gitlab-org/gitlab@95689c32e2734831c00ef30de303098485ec095a) ([merge request](gitlab-org/gitlab!92708))
### Fixed (1 change)
- [Fix group access dropdown failure if no subgroups are available](gitlab-org/gitlab@518a2f55caddab0c18d0548d0a8f777afe5ae666) ([merge request](gitlab-org/gitlab!92708)) **GitLab Enterprise Edition**
## 15.1.2 (2022-07-05)
### Fixed (3 changes)

View File

@ -1,75 +0,0 @@
<script>
import { GlFormGroup, GlButton, GlFormInput } from '@gitlab/ui';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { resourceLinksFormI18n } from '../constants';
export default {
name: 'AddIssuableResourceLinkForm',
components: {
GlFormGroup,
GlButton,
GlFormInput,
},
i18n: resourceLinksFormI18n,
directives: {
autofocusonshow,
},
props: {
isSubmitting: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
linkTextValue: '',
linkValue: '',
};
},
computed: {
isSubmitButtonDisabled() {
return this.linkValue.length === 0 || this.isSubmitting;
},
},
methods: {
onFormCancel() {
this.linkValue = '';
this.linkTextValue = '';
this.$emit('add-issuable-resource-link-form-cancel');
},
},
};
</script>
<template>
<form @submit.prevent>
<gl-form-group :label="$options.i18n.linkTextLabel">
<gl-form-input
v-model="linkTextValue"
v-autofocusonshow
data-testid="link-text-input"
type="text"
/>
</gl-form-group>
<gl-form-group :label="$options.i18n.linkValueLabel">
<gl-form-input v-model="linkValue" data-testid="link-value-input" type="text" />
</gl-form-group>
<div class="gl-mt-5 gl-clearfix">
<gl-button
category="primary"
variant="confirm"
data-testid="add-button"
:disabled="isSubmitButtonDisabled"
:loading="isSubmitting"
type="submit"
class="gl-float-left"
>
{{ $options.i18n.submitButtonText }}
</gl-button>
<gl-button class="gl-float-right" @click="onFormCancel">
{{ $options.i18n.cancelButtonText }}
</gl-button>
</div>
</form>
</template>

View File

@ -1,111 +0,0 @@
<script>
import { GlLink, GlIcon, GlButton } from '@gitlab/ui';
import { resourceLinksI18n } from '../constants';
import AddIssuableResourceLinkForm from './add_issuable_resource_link_form.vue';
export default {
name: 'ResourceLinksBlock',
components: {
GlLink,
GlButton,
GlIcon,
AddIssuableResourceLinkForm,
},
i18n: resourceLinksI18n,
props: {
helpPath: {
type: String,
required: false,
default: '',
},
canAddResourceLinks: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isFormVisible: false,
isSubmitting: false,
};
},
computed: {
badgeLabel() {
return 0;
},
hasBody() {
return this.isFormVisible;
},
},
methods: {
async toggleResourceLinkForm() {
this.isFormVisible = !this.isFormVisible;
},
hideResourceLinkForm() {
this.isFormVisible = false;
},
},
};
</script>
<template>
<div id="resource-links" class="gl-mt-5">
<div class="card card-slim gl-overflow-hidden">
<div
:class="{ 'panel-empty-heading border-bottom-0': !hasBody }"
class="card-header gl-display-flex gl-justify-content-space-between"
>
<h3
class="card-title h5 position-relative gl-my-0 gl-display-flex gl-align-items-center gl-h-7"
>
<gl-link
id="user-content-resource-links"
class="anchor position-absolute gl-text-decoration-none"
href="#resource-links"
aria-hidden="true"
/>
<slot name="header-text">{{ $options.i18n.headerText }}</slot>
<gl-link
:href="helpPath"
target="_blank"
class="gl-display-flex gl-align-items-center gl-ml-2 gl-text-gray-500"
data-testid="help-link"
:aria-label="$options.i18n.helpText"
>
<gl-icon name="question" :size="12" />
</gl-link>
<div class="gl-display-inline-flex">
<div class="gl-display-inline-flex gl-mx-5">
<span class="gl-display-inline-flex gl-align-items-center">
<gl-icon name="link" class="gl-mr-2 gl-text-gray-500" />
{{ badgeLabel }}
</span>
</div>
<gl-button
v-if="canAddResourceLinks"
icon="plus"
:aria-label="$options.i18n.addButtonText"
@click="toggleResourceLinkForm"
/>
</div>
</h3>
</div>
<div
class="linked-issues-card-body bg-gray-light"
:class="{
'gl-p-5': isFormVisible,
}"
>
<div v-show="isFormVisible" class="card-body bordered-box gl-bg-white">
<add-issuable-resource-link-form
ref="resourceLinkForm"
:is-submitting="isSubmitting"
@add-issuable-resource-link-form-cancel="hideResourceLinkForm"
/>
</div>
</div>
</div>
</div>
</template>

View File

@ -1,14 +0,0 @@
import { s__ } from '~/locale';
export const resourceLinksI18n = Object.freeze({
headerText: s__('LinkedResources|Linked resources'),
helpText: s__('LinkedResources|Read more about linked resources'),
addButtonText: s__('LinkedResources|Add a resource link'),
});
export const resourceLinksFormI18n = Object.freeze({
linkTextLabel: s__('LinkedResources|Text (Optional)'),
linkValueLabel: s__('LinkedResources|Link'),
submitButtonText: s__('LinkedResources|Add'),
cancelButtonText: s__('LinkedResources|Cancel'),
});

View File

@ -1,6 +1,6 @@
import Vue from 'vue';
import ResourceLinksBlock from 'ee_component/linked_resources/components/resource_links_block.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import ResourceLinksBlock from './components/resource_links_block.vue';
export default function initLinkedResources() {
const linkedResourcesRootElement = document.querySelector('.js-linked-resources-root');

View File

@ -1,4 +1,5 @@
<script>
import { uniqueId } from 'lodash';
import { GlIcon, GlPopover, GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { timeTilRun } from '../../utils';
@ -43,6 +44,11 @@ export default {
CLEANUP_STATUS_UNFINISHED,
PARTIAL_CLEANUP_CONTINUE_MESSAGE,
},
data() {
return {
iconId: uniqueId('status-info-'),
};
},
computed: {
showStatus() {
return this.status !== UNSCHEDULED_STATUS;
@ -85,14 +91,14 @@ export default {
</span>
<gl-icon
v-if="failedDelete"
id="status-info"
:id="iconId"
:size="14"
class="gl-text-gray-500"
data-testid="extra-info"
name="information-o"
/>
<gl-popover
target="status-info"
:target="iconId"
container="status-popover-container"
v-bind="$options.statusPopoverOptions"
>

View File

@ -1,9 +1,11 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlToast } from '@gitlab/ui';
import createDefaultClient from '~/lib/graphql';
import WorkItemLinks from './work_item_links.vue';
Vue.use(VueApollo);
Vue.use(GlToast);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),

View File

@ -11,6 +11,7 @@ import {
} from '../../constants';
import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql';
import WorkItemLinksForm from './work_item_links_form.vue';
import WorkItemLinksMenu from './work_item_links_menu.vue';
export default {
components: {
@ -19,6 +20,7 @@ export default {
GlIcon,
GlLoadingIcon,
WorkItemLinksForm,
WorkItemLinksMenu,
},
props: {
workItemId: {
@ -156,19 +158,22 @@ export default {
<div
v-for="child in children"
:key="child.id"
class="gl-relative gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base"
class="gl-relative gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32"
data-testid="links-child"
>
<div>
<gl-icon :name="$options.WIDGET_TYPE_TASK_ICON" class="gl-mr-3 gl-text-gray-700" />
<span class="gl-word-break-all">{{ child.title }}</span>
</div>
<div class="gl-ml-0 gl-sm-ml-auto! gl-mt-3 gl-sm-mt-0">
<div
class="gl-ml-0 gl-sm-ml-auto! gl-mt-3 gl-sm-mt-0 gl-display-inline-flex gl-align-items-center"
>
<gl-badge :variant="badgeVariant(child.state)">
<span class="gl-sm-display-block">{{
$options.WORK_ITEM_STATUS_TEXT[child.state]
}}</span>
</gl-badge>
<work-item-links-menu :work-item-id="child.id" :parent-work-item-id="issuableGid" />
</div>
</div>
</template>

View File

@ -0,0 +1,101 @@
<script>
import { GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { produce } from 'immer';
import { s__ } from '~/locale';
import changeWorkItemParentMutation from '../../graphql/change_work_item_parent_link.mutation.graphql';
import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql';
import { WIDGET_TYPE_HIERARCHY } from '../../constants';
export default {
components: {
GlDropdownItem,
GlDropdown,
GlIcon,
},
props: {
workItemId: {
type: String,
required: true,
},
parentWorkItemId: {
type: String,
required: true,
},
},
data() {
return {
activeToast: null,
};
},
methods: {
toggleChildFromCache(data, store) {
const sourceData = store.readQuery({
query: getWorkItemLinksQuery,
variables: { id: this.parentWorkItemId },
});
const newData = produce(sourceData, (draftState) => {
const widgetHierarchy = draftState.workItem.widgets.find(
(widget) => widget.type === WIDGET_TYPE_HIERARCHY,
);
const index = widgetHierarchy.children.nodes.findIndex(
(child) => child.id === this.workItemId,
);
if (index >= 0) {
widgetHierarchy.children.nodes.splice(index, 1);
} else {
widgetHierarchy.children.nodes.push(data.workItemUpdate.workItem);
}
});
store.writeQuery({
query: getWorkItemLinksQuery,
variables: { id: this.parentWorkItemId },
data: newData,
});
},
async addChild(data) {
const { data: resp } = await this.$apollo.mutate({
mutation: changeWorkItemParentMutation,
variables: { id: this.workItemId, parentId: this.parentWorkItemId },
update: this.toggleChildFromCache.bind(this, data),
});
if (resp.workItemUpdate.errors.length === 0) {
this.activeToast?.hide();
}
},
async removeChild() {
const { data } = await this.$apollo.mutate({
mutation: changeWorkItemParentMutation,
variables: { id: this.workItemId, parentId: null },
update: this.toggleChildFromCache.bind(this, null),
});
if (data.workItemUpdate.errors.length === 0) {
this.activeToast = this.$toast.show(s__('WorkItem|Child removed'), {
action: {
text: s__('WorkItem|Undo'),
onClick: this.addChild.bind(this, data),
},
});
}
},
},
};
</script>
<template>
<span class="gl-ml-2">
<gl-dropdown category="tertiary" toggle-class="btn-icon" :right="true">
<template #button-content>
<gl-icon name="ellipsis_v" :size="14" />
</template>
<gl-dropdown-item @click="removeChild">
{{ s__('WorkItem|Remove') }}
</gl-dropdown-item>
</gl-dropdown>
</span>
</template>

View File

@ -0,0 +1,13 @@
mutation changeWorkItemParentLink($id: WorkItemID!, $parentId: WorkItemID) {
workItemUpdate(input: { id: $id, hierarchyWidget: { parentId: $parentId } }) {
workItem {
id
workItemType {
id
}
title
state
}
errors
}
}

View File

@ -51,7 +51,7 @@ module Projects
def test_suite
suite = builds.sum do |build|
build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new)
end
Gitlab::Ci::Reports::TestFailureHistory.new(suite.failed.values, project).load!

View File

@ -28,7 +28,7 @@ module Resolvers
def load_test_suite_data(builds)
suite = builds.sum do |build|
build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new)
end
Gitlab::Ci::Reports::TestFailureHistory.new(suite.failed.values, pipeline.project).load!

View File

@ -11,6 +11,7 @@ module Users
UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout'
SECURITY_NEWSLETTER_CALLOUT = 'security_newsletter_callout'
REGISTRATION_ENABLED_CALLOUT_ALLOWED_CONTROLLER_PATHS = [/^root/, /^dashboard\S*/, /^admin\S*/].freeze
WEB_HOOK_DISABLED = 'web_hook_disabled'
def show_gke_cluster_integration_callout?(project)
active_nav_link?(controller: sidebar_operations_paths) &&
@ -60,12 +61,31 @@ module Users
!user_dismissed?(SECURITY_NEWSLETTER_CALLOUT)
end
def web_hook_disabled_dismissed?(project)
return false unless project
last_failure = Gitlab::Redis::SharedState.with do |redis|
key = "web_hooks:last_failure:project-#{project.id}"
redis.get(key)
end
last_failure = DateTime.parse(last_failure) if last_failure
user_dismissed?(WEB_HOOK_DISABLED, last_failure, namespace: project.namespace)
end
private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil, namespace: nil)
return false unless current_user
current_user.dismissed_callout?(feature_name: feature_name, ignore_dismissal_earlier_than: ignore_dismissal_earlier_than)
query = { feature_name: feature_name, ignore_dismissal_earlier_than: ignore_dismissal_earlier_than }
if namespace
current_user.dismissed_callout_for_namespace?(namespace: namespace, **query)
else
current_user.dismissed_callout?(**query)
end
end
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module WebHooks
module WebHooksHelper
EXPIRY_TTL = 1.hour
def show_project_hook_failed_callout?(project:)
return false unless current_user
return false unless Feature.enabled?(:webhooks_failed_callout, project)
return false unless Feature.enabled?(:web_hooks_disable_failed, project)
return false unless Ability.allowed?(current_user, :read_web_hooks, project)
# Assumes include of Users::CalloutsHelper
return false if web_hook_disabled_dismissed?(project)
any_project_hook_failed?(project) # Most expensive query last
end
private
def any_project_hook_failed?(project)
Rails.cache.fetch("any_web_hook_failed:#{project.id}", expires_in: EXPIRY_TTL) do
ProjectHook.for_projects(project).disabled.exists?
end
end
end
end

View File

@ -1090,7 +1090,7 @@ module Ci
end
def test_reports
Gitlab::Ci::Reports::TestReports.new.tap do |test_reports|
Gitlab::Ci::Reports::TestReport.new.tap do |test_reports|
latest_test_report_builds.find_each do |build|
build.collect_test_reports!(test_reports)
end

View File

@ -27,6 +27,8 @@ class ProjectHook < WebHook
belongs_to :project
validates :project, presence: true
scope :for_projects, ->(project) { where(project: project) }
def pluralized_name
_('Webhooks')
end
@ -41,6 +43,19 @@ class ProjectHook < WebHook
project
end
override :update_last_failure
def update_last_failure
return if executable?
key = "web_hooks:last_failure:project-#{project_id}"
time = Time.current.utc.iso8601
Gitlab::Redis::SharedState.with do |redis|
prev = redis.get(key)
redis.set(key, time) if !prev || prev < time
end
end
private
override :web_hooks_disable_failed?

View File

@ -48,6 +48,11 @@ class WebHook < ApplicationRecord
where('recent_failures <= ? AND (disabled_until IS NULL OR disabled_until < ?)', FAILURE_THRESHOLD, Time.current)
end
# Inverse of executable
scope :disabled, -> do
where('recent_failures > ? OR disabled_until >= ?', FAILURE_THRESHOLD, Time.current)
end
def executable?
!temporarily_disabled? && !permanently_disabled?
end
@ -181,6 +186,10 @@ class WebHook < ApplicationRecord
raise InterpolationError, "Invalid URL template. Missing key #{e.key}"
end
def update_last_failure
# Overridden in child classes.
end
private
def web_hooks_disable_failed?

View File

@ -111,6 +111,7 @@ class Note < ApplicationRecord
end
validate :does_not_exceed_notes_limit?, on: :create, unless: [:system?, :importing?]
validate :validate_created_after
# @deprecated attachments are handled by the Upload model.
#
@ -748,6 +749,13 @@ class Note < ApplicationRecord
errors.add(:base, _('Maximum number of comments exceeded')) if noteable.notes.count >= Noteable::MAX_NOTES_LIMIT
end
def validate_created_after
return unless created_at
return if created_at >= '1970-01-01'
errors.add(:created_at, s_('Note|The created date provided is too far in the past.'))
end
def noteable_label_url_method
for_merge_request? ? :project_merge_requests_url : :project_issues_url
end

View File

@ -222,6 +222,7 @@ class User < ApplicationRecord
has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'Users::Callout'
has_many :group_callouts, class_name: 'Users::GroupCallout'
has_many :namespace_callouts, class_name: 'Users::NamespaceCallout'
has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
@ -2085,6 +2086,13 @@ class User < ApplicationRecord
callout_dismissed?(callout, ignore_dismissal_earlier_than)
end
def dismissed_callout_for_namespace?(feature_name:, namespace:, ignore_dismissal_earlier_than: nil)
source_feature_name = "#{feature_name}_#{namespace.id}"
callout = namespace_callouts_by_feature_name[source_feature_name]
callout_dismissed?(callout, ignore_dismissal_earlier_than)
end
# Load the current highest access by looking directly at the user's memberships
def current_highest_access_level
members.non_request.maximum(:access_level)
@ -2111,6 +2119,11 @@ class User < ApplicationRecord
.find_or_initialize_by(feature_name: ::Users::GroupCallout.feature_names[feature_name], group_id: group_id)
end
def find_or_initialize_namespace_callout(feature_name, namespace_id)
namespace_callouts
.find_or_initialize_by(feature_name: ::Users::NamespaceCallout.feature_names[feature_name], namespace_id: namespace_id)
end
def can_trigger_notifications?
confirmed? && !blocked? && !ghost?
end
@ -2228,6 +2241,10 @@ class User < ApplicationRecord
@group_callouts_by_feature_name ||= group_callouts.index_by(&:source_feature_name)
end
def namespace_callouts_by_feature_name
@namespace_callouts_by_feature_name ||= namespace_callouts.index_by(&:source_feature_name)
end
def authorized_groups_without_shared_membership
Group.from_union([
groups.select(*Namespace.cached_column_list),

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Users
class NamespaceCallout < ApplicationRecord
include Users::Calloutable
self.table_name = 'user_namespace_callouts'
belongs_to :namespace
enum feature_name: {
invite_members_banner: 1,
approaching_seat_count_threshold: 2, # EE-only
storage_enforcement_banner_first_enforcement_threshold: 3,
storage_enforcement_banner_second_enforcement_threshold: 4,
storage_enforcement_banner_third_enforcement_threshold: 5,
storage_enforcement_banner_fourth_enforcement_threshold: 6,
preview_user_over_limit_free_plan_alert: 7, # EE-only
user_reached_limit_free_plan_alert: 8, # EE-only
web_hook_disabled: 9
}
validates :namespace, presence: true
validates :feature_name,
presence: true,
uniqueness: { scope: [:user_id, :namespace_id] },
inclusion: { in: NamespaceCallout.feature_names.keys }
def source_feature_name
"#{feature_name}_#{namespace_id}"
end
end
end

View File

@ -492,6 +492,7 @@ class ProjectPolicy < BasePolicy
enable :update_runners_registration_token
enable :admin_project_google_cloud
enable :admin_secure_files
enable :read_web_hooks
end
rule { public_project & metrics_dashboard_allowed }.policy do

View File

@ -22,7 +22,7 @@ module Ci
private
def generate_test_suite_report(build)
build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new)
end
def tests_params(test_suite)

View File

@ -81,7 +81,7 @@ module Ci
def generate_test_suite!(build)
# Returns an instance of Gitlab::Ci::Reports::TestSuite
build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new)
end
def ci_unit_test_attrs(batch)

View File

@ -44,6 +44,7 @@ module WebHooks
end
log_state_change
hook.update_last_failure
end
rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
raise if raise_lock_error?

View File

@ -2,14 +2,18 @@
.container
= render partial: "shared/errors/graphic_422", formats: :svg
%h3 Sign-in using #{@provider} auth failed
%h3
= _('Sign-in using %{provider} auth failed') % { provider: @provider }
%p.light.subtitle Sign-in failed because #{@error}.
%p.light.subtitle
= _('Sign-in failed because %{error}.') % { error: @error }
%p Try logging in using your username or email. If you have forgotten your password, try recovering it
%p
= _('Try logging in using your username or email. If you have forgotten your password, try recovering it')
= link_to "Sign in", new_session_path(:user), class: 'gl-button btn primary'
= link_to "Recover password", new_password_path(:user), class: 'gl-button btn secondary'
= link_to _('Sign in'), new_session_path(:user), class: 'gl-button btn primary'
= link_to _('Recover password'), new_password_path(:user), class: 'gl-button btn secondary'
%hr
%p.light If none of the options work, try contacting a GitLab administrator.
%p.light
= _('If none of the options work, try contacting a GitLab administrator.')

View File

@ -6,9 +6,9 @@
= preserve(markdown(commit.description, pipeline: :single_line))
.info-well
.well-segment.pipeline-info
.icon-container.gl-vertical-align-text-bottom
= sprite_icon('clock')
.well-segment.pipeline-info{ class: "gl-align-items-baseline!" }
.icon-container
= sprite_icon('clock', css_class: 'gl-top-0!')
= pluralize @pipeline.total_size, "job"
= @pipeline.ref_text
- if @pipeline.duration
@ -20,7 +20,7 @@
- if has_pipeline_badges?(@pipeline)
.well-segment.qa-pipeline-badges
.icon-container
= sprite_icon('flag')
= sprite_icon('flag', css_class: 'gl-top-0!')
- if @pipeline.child?
- text = sprintf(s_('Pipelines|Child pipeline (%{link_start}parent%{link_end})'), { link_start: "<a href='#{pipeline_path(@pipeline.triggered_by_pipeline)}' class='text-underline'>", link_end: "</a>"}).html_safe
= gl_badge_tag text, { variant: :info, size: :sm }, { class: 'js-pipeline-child has-tooltip', title: s_("Pipelines|This is a child pipeline within the parent pipeline") }
@ -44,13 +44,13 @@
.well-segment.branch-info
.icon-container.commit-icon
= custom_icon("icon_commit")
= sprite_icon('commit', css_class: 'gl-top-0!')
= link_to commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha"
= clipboard_button(text: @pipeline.sha, title: _("Copy commit SHA"))
.well-segment.related-merge-request-info
.icon-container
= sprite_icon("git-merge")
= sprite_icon("git-merge", css_class: 'gl-top-0!')
%span.related-merge-requests
%span.js-truncated-mr-list
= @pipeline.all_related_merge_request_text(limit: 1)

View File

@ -0,0 +1,8 @@
---
name: webhooks_failed_callout
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91092
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/365535
milestone: '15.2'
type: development
group: group::integrations
default_enabled: false

View File

@ -0,0 +1,10 @@
---
table_name: user_namespace_callouts
classes:
- Users::NamespaceCallout
feature_categories:
- navigation
description: Contains records of which users have dismissed a callout, grouped by namespace.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91092
milestone: '15.2'

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class CreateUserNamespaceCallouts < Gitlab::Database::Migration[2.0]
def up
create_table :user_namespace_callouts do |t|
t.bigint :user_id, null: false
t.bigint :namespace_id, null: false, index: true
t.datetime_with_timezone :dismissed_at
t.integer :feature_name, limit: 2, null: false
end
end
def down
drop_table :user_namespace_callouts
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
class AddForeignKeysToUserNamespaceCallouts < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
add_concurrent_foreign_key :user_namespace_callouts, :users,
column: :user_id,
on_delete: :cascade
add_concurrent_foreign_key :user_namespace_callouts, :namespaces,
column: :namespace_id,
on_delete: :cascade
add_concurrent_index :user_namespace_callouts, [:user_id, :feature_name, :namespace_id],
unique: true,
name: 'index_ns_user_callouts_feature'
end
def down
remove_concurrent_index_by_name :user_namespace_callouts, 'index_ns_user_callouts_feature'
with_lock_retries do
remove_foreign_key :user_namespace_callouts, column: :user_id
remove_foreign_key :user_namespace_callouts, column: :namespace_id
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class DropTokenIndexFromCiBuilds < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
INDEX_NAME = 'index_ci_builds_on_token_partial'
def up
remove_concurrent_index_by_name :ci_builds, INDEX_NAME
end
# rubocop:disable Migration/PreventIndexCreation
def down
add_concurrent_index :ci_builds, :token, unique: true, where: 'token IS NOT NULL', name: INDEX_NAME
end
# rubocop:enable Migration/PreventIndexCreation
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class UpdateNotesInPast < Gitlab::Database::Migration[2.0]
restrict_gitlab_migration gitlab_schema: :gitlab_main
def up
loop do
update_count = define_batchable_model('notes')
.where('created_at < ?', '1970-01-01').limit(100)
.update_all(created_at: '1970-01-01 00:00:00')
break if update_count == 0
end
end
def down
# no op
end
end

View File

@ -0,0 +1 @@
29ab69647b53c331aefdd62e8fbcc1567df4424a8e7ae6f8eb7b1e9afa7a6911

View File

@ -0,0 +1 @@
6d65af0d20cd80cf3367f48c5447ff33046e982ac1cfd55aaf52a7cc2330e428

View File

@ -0,0 +1 @@
5a4a6355d1954735a05831e17c97e2879320f2cb313be56fb72e1cd2c20d9090

View File

@ -0,0 +1 @@
ea8182741ce0b30f2de23041d1f6bafaf6e04a7a7d0f50abcd04462683637596

View File

@ -21669,6 +21669,23 @@ CREATE TABLE user_interacted_projects (
project_id integer NOT NULL
);
CREATE TABLE user_namespace_callouts (
id bigint NOT NULL,
user_id bigint NOT NULL,
namespace_id bigint NOT NULL,
dismissed_at timestamp with time zone,
feature_name smallint NOT NULL
);
CREATE SEQUENCE user_namespace_callouts_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE user_namespace_callouts_id_seq OWNED BY user_namespace_callouts.id;
CREATE TABLE user_permission_export_uploads (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@ -23602,6 +23619,8 @@ ALTER TABLE ONLY user_details ALTER COLUMN user_id SET DEFAULT nextval('user_det
ALTER TABLE ONLY user_group_callouts ALTER COLUMN id SET DEFAULT nextval('user_group_callouts_id_seq'::regclass);
ALTER TABLE ONLY user_namespace_callouts ALTER COLUMN id SET DEFAULT nextval('user_namespace_callouts_id_seq'::regclass);
ALTER TABLE ONLY user_permission_export_uploads ALTER COLUMN id SET DEFAULT nextval('user_permission_export_uploads_id_seq'::regclass);
ALTER TABLE ONLY user_preferences ALTER COLUMN id SET DEFAULT nextval('user_preferences_id_seq'::regclass);
@ -25879,6 +25898,9 @@ ALTER TABLE ONLY user_highest_roles
ALTER TABLE ONLY user_interacted_projects
ADD CONSTRAINT user_interacted_projects_pkey PRIMARY KEY (project_id, user_id);
ALTER TABLE ONLY user_namespace_callouts
ADD CONSTRAINT user_namespace_callouts_pkey PRIMARY KEY (id);
ALTER TABLE ONLY user_permission_export_uploads
ADD CONSTRAINT user_permission_export_uploads_pkey PRIMARY KEY (id);
@ -27495,8 +27517,6 @@ CREATE INDEX index_ci_builds_on_status_and_type_and_runner_id ON ci_builds USING
CREATE UNIQUE INDEX index_ci_builds_on_token_encrypted ON ci_builds USING btree (token_encrypted) WHERE (token_encrypted IS NOT NULL);
CREATE UNIQUE INDEX index_ci_builds_on_token_partial ON ci_builds USING btree (token) WHERE (token IS NOT NULL);
CREATE INDEX index_ci_builds_on_updated_at ON ci_builds USING btree (updated_at);
CREATE INDEX index_ci_builds_on_upstream_pipeline_id ON ci_builds USING btree (upstream_pipeline_id) WHERE (upstream_pipeline_id IS NOT NULL);
@ -28911,6 +28931,8 @@ CREATE INDEX index_notification_settings_on_source_and_level_and_user ON notific
CREATE UNIQUE INDEX index_notifications_on_user_id_and_source_id_and_source_type ON notification_settings USING btree (user_id, source_id, source_type);
CREATE UNIQUE INDEX index_ns_user_callouts_feature ON user_namespace_callouts USING btree (user_id, feature_name, namespace_id);
CREATE INDEX index_oauth_access_grants_on_resource_owner_id ON oauth_access_grants USING btree (resource_owner_id, application_id, created_at);
CREATE UNIQUE INDEX index_oauth_access_grants_on_token ON oauth_access_grants USING btree (token);
@ -29913,6 +29935,8 @@ CREATE INDEX index_user_highest_roles_on_user_id_and_highest_access_level ON use
CREATE INDEX index_user_interacted_projects_on_user_id ON user_interacted_projects USING btree (user_id);
CREATE INDEX index_user_namespace_callouts_on_namespace_id ON user_namespace_callouts USING btree (namespace_id);
CREATE INDEX index_user_permission_export_uploads_on_user_id_and_status ON user_permission_export_uploads USING btree (user_id, status);
CREATE INDEX index_user_preferences_on_gitpod_enabled ON user_preferences USING btree (gitpod_enabled);
@ -31780,6 +31804,9 @@ ALTER TABLE ONLY ci_pipelines
ALTER TABLE ONLY geo_event_log
ADD CONSTRAINT fk_27548c6db3 FOREIGN KEY (hashed_storage_migrated_event_id) REFERENCES geo_hashed_storage_migrated_events(id) ON DELETE CASCADE;
ALTER TABLE ONLY user_namespace_callouts
ADD CONSTRAINT fk_27a69fd1bd FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY merge_requests_compliance_violations
ADD CONSTRAINT fk_290ec1ab02 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
@ -31885,6 +31912,9 @@ ALTER TABLE ONLY releases
ALTER TABLE ONLY geo_event_log
ADD CONSTRAINT fk_4a99ebfd60 FOREIGN KEY (repositories_changed_event_id) REFERENCES geo_repositories_changed_events(id) ON DELETE CASCADE;
ALTER TABLE ONLY user_namespace_callouts
ADD CONSTRAINT fk_4b1257f385 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY sbom_occurrences
ADD CONSTRAINT fk_4b88e5b255 FOREIGN KEY (component_version_id) REFERENCES sbom_component_versions(id) ON DELETE CASCADE;

View File

@ -43,6 +43,35 @@ The console is in the toolbox pod. Refer to our [Kubernetes cheat sheet](https:/
To exit the console, type: `quit`.
## Enable Active Record logging
You can enable output of Active Record debug logging in the Rails console
session by running:
```ruby
ActiveRecord::Base.logger = Logger.new($stdout)
```
This shows information about database queries triggered by any Ruby code
you may run in the console. To turn off logging again, run:
```ruby
ActiveRecord::Base.logger = nil
```
## Disable database statement timeout
You can disable the PostgreSQL statement timeout for the current Rails console
session by running:
```ruby
ActiveRecord::Base.connection.execute('SET statement_timeout TO 0')
```
This change only affects the current Rails console session and is
not persisted in the GitLab production environment or in the next Rails
console session.
## Output Rails console session history
Enter the following command on the rails console to display

View File

@ -9,83 +9,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Sometimes things don't work the way they should. Here are some tips on debugging issues out
in production.
## Starting a Rails console session
Troubleshooting and debugging your GitLab instance often requires a Rails console.
Your type of GitLab installation determines how
[to start a rails console](../operations/rails_console.md).
See also:
- [GitLab Rails Console Cheat Sheet](gitlab_rails_cheat_sheet.md).
### Enabling Active Record logging
You can enable output of Active Record debug logging in the Rails console
session by running:
```ruby
ActiveRecord::Base.logger = Logger.new($stdout)
```
This shows information about database queries triggered by any Ruby code
you may run in the console. To turn off logging again, run:
```ruby
ActiveRecord::Base.logger = nil
```
### Disabling database statement timeout
You can disable the PostgreSQL statement timeout for the current Rails console
session by running:
```ruby
ActiveRecord::Base.connection.execute('SET statement_timeout TO 0')
```
This change only affects the current Rails console session and is
not persisted in the GitLab production environment or in the next Rails
console session.
### Output Rails console session history
If you'd like to output your Rails console command history in a format that's
easy to copy and save for future reference, you can run:
```ruby
puts Readline::HISTORY.to_a
```
## Using the Rails runner
If you need to run some Ruby code in the context of your GitLab production
environment, you can do so using the [Rails runner](https://guides.rubyonrails.org/command_line.html#rails-runner). When executing a script file, the script must be accessible by the `git` user.
**For Omnibus installations**
```shell
sudo gitlab-rails runner "RAILS_COMMAND"
# Example with a two-line Ruby script
sudo gitlab-rails runner "user = User.first; puts user.username"
# Example with a ruby script file (make sure to use the full path)
sudo gitlab-rails runner /path/to/script.rb
```
**For installations from source**
```shell
sudo -u git -H bundle exec rails runner -e production "RAILS_COMMAND"
# Example with a two-line Ruby script
sudo -u git -H bundle exec rails runner -e production "user = User.first; puts user.username"
# Example with a ruby script file (make sure to use the full path)
sudo -u git -H bundle exec rails runner -e production /path/to/script.rb
```
## More information
- [Debugging Stuck Ruby Processes](https://newrelic.com/blog/best-practices/debugging-stuck-ruby-processes-what-to-do-before-you-kill-9)

View File

@ -453,12 +453,14 @@ Keyset-pagination allows for more efficient retrieval of pages and - in contrast
to offset-based pagination - runtime is independent of the size of the
collection.
This method is controlled by the following parameters:
This method is controlled by the following parameters. `order_by` and `sort` are both mandatory.
| Parameter | Description |
|--------------| ------------|
| `pagination` | `keyset` (to enable keyset pagination). |
| `per_page` | Number of items to list per page (default: `20`, max: `100`). |
| Parameter | Required | Description |
|--------------| ------------ | --------- |
| `pagination` | yes | `keyset` (to enable keyset pagination). |
| `per_page` | no | Number of items to list per page (default: `20`, max: `100`). |
| `order_by` | yes | Column by which to order by. |
| `sort` | yes | Sort order (`asc` or `desc`) |
In the following example, we list 50 [projects](projects.md) per page, ordered
by `id` ascending.

View File

@ -146,7 +146,7 @@ Parameters:
| `issue_iid` | integer | yes | The IID of an issue. |
| `body` | string | yes | The content of a note. Limited to 1,000,000 characters. |
| `confidential` | boolean | no | The confidential flag of a note. Default is false. |
| `created_at` | string | no | Date time string, ISO 8601 formatted. Example: `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
| `created_at` | string | no | Date time string, ISO 8601 formatted. It must be after 1970-01-01. Example: `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/notes?body=note"

View File

@ -316,11 +316,12 @@ and [Container Scanning](../../user/application_security/container_scanning/inde
You can find the schemas for these scanners here:
- [SAST](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/sast-report-format.json)
- [DAST](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/dast-report-format.json)
- [Dependency Scanning](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/dependency-scanning-report-format.json)
- [Cluster Image Scanning](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/cluster-image-scanning-report-format.json)
- [Container Scanning](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/container-scanning-report-format.json)
- [Coverage Fuzzing](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/coverage-fuzzing-report-format.json)
- [DAST](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/dast-report-format.json)
- [Dependency Scanning](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/dependency-scanning-report-format.json)
- [SAST](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/sast-report-format.json)
- [Secret Detection](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/secret-detection-report-format.json)
### Retention period for vulnerabilities

View File

@ -220,11 +220,12 @@ once it's imported into the database.
The type of scan. This must be one of the following:
- `container_scanning`
- `dependency_scanning`
- `dast`
- `sast`
- `cluster_image_scanning`
- `container_scanning`
- `dast`
- `dependency_scanning`
- `sast`
- `secret_detection`
### Scanner

View File

@ -145,7 +145,7 @@ To create a personal access token programmatically:
```
This code can be shortened into a single-line shell command by using the
[Rails runner](../../administration/troubleshooting/debug.md#using-the-rails-runner):
[Rails runner](../../administration/operations/rails_console.md#using-the-rails-runner):
```shell
sudo gitlab-rails runner "token = User.find_by_username('automation-bot').personal_access_tokens.create(scopes: [:read_user, :read_repository], name: 'Automation token'); token.set_token('token-string-here123'); token.save!"
@ -177,7 +177,7 @@ To revoke a token programmatically:
```
This code can be shortened into a single-line shell command using the
[Rails runner](../../administration/troubleshooting/debug.md#using-the-rails-runner):
[Rails runner](../../administration/operations/rails_console.md#using-the-rails-runner):
```shell
sudo gitlab-rails runner "PersonalAccessToken.find_by_token('token-string-here123').revoke!"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -23,7 +23,26 @@ merge requests are merged into an existing branch.
This setting is the default. It always creates a separate merge commit,
even when using [squash](../squash_and_merge.md). An example commit graph generated using this merge method:
![Commit graph for merge commits](../img/merge_method_merge_commit_v15_0.png)
```mermaid
gitGraph
commit id: "Init"
branch mr-branch-1
commit
checkout main
commit
branch mr-branch-2
commit
checkout mr-branch-1
commit
checkout main
branch squash-mr
commit id: "Squashed commits"
checkout main
merge squash-mr
merge mr-branch-1
commit
merge mr-branch-2
```
- For regular merges, it is equivalent to the command `git merge --no-ff <source-branch>`.
- For squash merges, it squashes all commits in the source branch before merging it normally. It performs actions similar to:
@ -42,7 +61,25 @@ A merge commit is created for every merge, but the branch is only merged if
a fast-forward merge is possible. This ensures that if the merge request build
succeeded, the target branch build also succeeds after the merge. An example commit graph generated using this merge method:
![Commit graph for merge commit with semi-linear history](../img/merge_method_merge_commit_with_semi_linear_history_v15_0.png)
```mermaid
gitGraph
commit id: "Init"
branch mr-branch-1
commit
commit
checkout main
merge mr-branch-1
branch mr-branch-2
commit
commit
checkout main
merge mr-branch-2
commit
branch squash-mr
commit id: "Squashed commits"
checkout main
merge squash-mr
```
When you visit the merge request page with `Merge commit with semi-linear history`
method selected, you can accept it **only if a fast-forward merge is possible**.
@ -63,7 +100,14 @@ fast-forward merge requests, you can retain a linear Git history and a way
to accept merge requests without creating merge commits. An example commit graph
generated using this merge method:
![Commit graph for fast-forward merge](../img/merge_method_ff_v15_0.png)
```mermaid
gitGraph
commit id: "Init"
commit id: "Merge mr-branch-1"
commit id: "Merge mr-branch-2"
commit id: "Commit on main"
commit id: "Merge squash-mr"
```
This method is equivalent to `git merge --ff <source-branch>` for regular merges, and to
`git merge -squash <source-branch>` for squash merges.

View File

@ -3,7 +3,7 @@
module Gitlab
module Ci
module Reports
class TestReports
class TestReport
attr_reader :test_suites
def initialize

View File

@ -9,7 +9,7 @@ module Gitlab
attr_reader :base_reports, :head_reports
def initialize(base_reports, head_reports)
@base_reports = base_reports || TestReports.new
@base_reports = base_reports || TestReport.new
@head_reports = head_reports
end

View File

@ -578,3 +578,4 @@ zentao_tracker_data: :gitlab_main
dingtalk_tracker_data: :gitlab_main
zoom_meetings: :gitlab_main
batched_background_migration_job_transition_logs: :gitlab_shared
user_namespace_callouts: :gitlab_main

View File

@ -14,7 +14,7 @@ module Gitlab
def jobs_by_migration_name
Gitlab::Database::BackgroundMigration::BatchedMigration
.executable
.created_after(2.days.ago) # Simple way to exclude migrations already running before migration testing
.created_after(3.hours.ago) # Simple way to exclude migrations already running before migration testing
.to_h do |migration|
batching_strategy = migration.batch_class.new(connection: connection)

View File

@ -6,9 +6,6 @@ module Gitlab
URL_REGEX = %r{https?://[^'" ]+}.freeze
GIT_INVALID_URL_REGEX = /^git\+#{URL_REGEX}/.freeze
REPO_REGEX = %r{[^/'" ]+/[^/'" ]+}.freeze
VALID_LINK_ATTRIBUTES = %w[href rel target].freeze
include ActionView::Helpers::SanitizeHelper
class_attribute :file_type
@ -65,10 +62,9 @@ module Gitlab
end
def link_tag(name, url)
sanitize(
%{<a href="#{ERB::Util.html_escape_once(url)}" rel="nofollow noreferrer noopener" target="_blank">#{ERB::Util.html_escape_once(name)}</a>},
attributes: VALID_LINK_ATTRIBUTES
)
href_attribute = %{href="#{ERB::Util.html_escape_once(url)}" } if Gitlab::UrlSanitizer.valid_web?(url)
%{<a #{href_attribute}rel="nofollow noreferrer noopener" target="_blank">#{ERB::Util.html_escape_once(name)}</a>}.html_safe
end
# Links package names based on regex.

View File

@ -128,6 +128,9 @@ module Gitlab
@loaded_size = @data.bytesize if @data
@loaded_all_data = @loaded_size == size
# Recalculate binary status if we loaded all data
@binary = nil if @loaded_all_data
record_metric_blob_size
record_metric_truncated(truncated?)
end

View File

@ -5,6 +5,8 @@ module Gitlab
module Keyset
class CursorBasedRequestContext
DEFAULT_SORT_DIRECTION = :desc
DEFAULT_SORT_COLUMN = :id
attr_reader :request_context
delegate :params, to: :request_context
@ -28,7 +30,7 @@ module Gitlab
end
def order_by
{ params[:order_by].to_sym => params[:sort]&.to_sym || DEFAULT_SORT_DIRECTION }
{ (params[:order_by]&.to_sym || DEFAULT_SORT_COLUMN) => (params[:sort]&.to_sym || DEFAULT_SORT_DIRECTION) }
end
end
end

View File

@ -19590,6 +19590,9 @@ msgstr ""
msgid "If no options are selected, only administrators can register runners."
msgstr ""
msgid "If none of the options work, try contacting a GitLab administrator."
msgstr ""
msgid "If the number of active users exceeds the user limit, you will be charged for the number of %{users_over_license_link} at your next license reconciliation."
msgstr ""
@ -26491,6 +26494,9 @@ msgstr ""
msgid "Notes|You're only seeing %{boldStart}other activity%{boldEnd} in the feed. To add a comment, switch to one of the following options."
msgstr ""
msgid "Note|The created date provided is too far in the past."
msgstr ""
msgid "Nothing to preview."
msgstr ""
@ -31990,6 +31996,9 @@ msgstr ""
msgid "Reconfigure"
msgstr ""
msgid "Recover password"
msgstr ""
msgid "Recovery Codes"
msgstr ""
@ -36255,6 +36264,9 @@ msgstr ""
msgid "Sign-in count:"
msgstr ""
msgid "Sign-in failed because %{error}."
msgstr ""
msgid "Sign-in page"
msgstr ""
@ -36264,6 +36276,9 @@ msgstr ""
msgid "Sign-in text"
msgstr ""
msgid "Sign-in using %{provider} auth failed"
msgstr ""
msgid "Sign-out page URL"
msgstr ""
@ -41103,6 +41118,9 @@ msgstr ""
msgid "Try grouping with different labels"
msgstr ""
msgid "Try logging in using your username or email. If you have forgotten your password, try recovering it"
msgstr ""
msgid "Try out GitLab Pipelines"
msgstr ""
@ -44021,6 +44039,9 @@ msgstr ""
msgid "WorkItem|Child items"
msgstr ""
msgid "WorkItem|Child removed"
msgstr ""
msgid "WorkItem|Closed"
msgstr ""
@ -44051,6 +44072,9 @@ msgstr ""
msgid "WorkItem|Open"
msgstr ""
msgid "WorkItem|Remove"
msgstr ""
msgid "WorkItem|Select type"
msgstr ""
@ -44084,6 +44108,9 @@ msgstr ""
msgid "WorkItem|Type"
msgstr ""
msgid "WorkItem|Undo"
msgstr ""
msgid "WorkItem|Work Items"
msgstr ""

View File

@ -29,5 +29,9 @@ FactoryBot.define do
trait :with_push_branch_filter do
push_events_branch_filter { 'my-branch-*' }
end
trait :permanently_disabled do
recent_failures { WebHook::FAILURE_THRESHOLD + 1 }
end
end
end

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
FactoryBot.define do
factory :namespace_callout, class: 'Users::NamespaceCallout' do
feature_name { :invite_members_banner }
user
namespace
end
end

View File

@ -21,7 +21,7 @@ RSpec.describe "Admin Runners" do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:project) { create(:project, namespace: namespace, creator: user) }
context "runners registration" do
describe "runners registration" do
before do
visit admin_runners_path
end
@ -164,7 +164,9 @@ RSpec.describe "Admin Runners" do
end
describe 'filter by status' do
let!(:never_contacted) { create(:ci_runner, :instance, description: 'runner-never-contacted', contacted_at: nil) }
let!(:never_contacted) do
create(:ci_runner, :instance, description: 'runner-never-contacted', contacted_at: nil)
end
before do
create(:ci_runner, :instance, description: 'runner-1', contacted_at: Time.zone.now)
@ -326,13 +328,15 @@ RSpec.describe "Admin Runners" do
visit admin_runners_path
page.within('[data-testid="runner-type-tabs"]') do
click_on 'Instance'
expect(page).to have_link('Instance', class: 'active')
end
end
it_behaves_like 'shows no runners found'
it 'shows active tab' do
expect(page).to have_link('Instance', class: 'active')
end
it 'shows no runner' do
expect(page).not_to have_content 'runner-project'
expect(page).not_to have_content 'runner-group'
@ -402,8 +406,8 @@ RSpec.describe "Admin Runners" do
end
it 'sorts by last contact date' do
create(:ci_runner, :instance, description: 'runner-1', created_at: '2018-07-12 15:37', contacted_at: '2018-07-12 15:37')
create(:ci_runner, :instance, description: 'runner-2', created_at: '2018-07-12 16:37', contacted_at: '2018-07-12 16:37')
create(:ci_runner, :instance, description: 'runner-1', contacted_at: '2018-07-12')
create(:ci_runner, :instance, description: 'runner-2', contacted_at: '2018-07-13')
visit admin_runners_path
@ -448,13 +452,13 @@ RSpec.describe "Admin Runners" do
it 'updates ACTIVE runner status to paused=false' do
visit admin_runners_path('status[]': 'ACTIVE')
expect(page).to have_current_path(admin_runners_path('paused[]': 'false') )
expect(page).to have_current_path(admin_runners_path('paused[]': 'false'))
end
it 'updates PAUSED runner status to paused=true' do
visit admin_runners_path('status[]': 'PAUSED')
expect(page).to have_current_path(admin_runners_path('paused[]': 'true') )
expect(page).to have_current_path(admin_runners_path('paused[]': 'true'))
end
end
end
@ -477,7 +481,9 @@ RSpec.describe "Admin Runners" do
describe 'runner show page breadcrumbs' do
it 'contains the current runner id and token' do
page.within '[data-testid="breadcrumb-links"]' do
expect(page.find('[data-testid="breadcrumb-current-link"]')).to have_link("##{runner.id} (#{runner.short_sha})")
expect(page.find('[data-testid="breadcrumb-current-link"]')).to have_link(
"##{runner.id} (#{runner.short_sha})"
)
end
end
end
@ -515,16 +521,16 @@ RSpec.describe "Admin Runners" do
describe "Runner edit page" do
let(:runner) { create(:ci_runner, :project) }
let!(:project1) { create(:project) }
let!(:project2) { create(:project) }
before do
@project1 = create(:project)
@project2 = create(:project)
visit edit_admin_runner_path(runner)
wait_for_requests
end
describe 'runner edit page breadcrumbs' do
describe 'breadcrumbs' do
it 'contains the current runner id and token' do
page.within '[data-testid="breadcrumb-links"]' do
expect(page).to have_link("##{runner.id} (#{runner.short_sha})")
@ -539,7 +545,7 @@ RSpec.describe "Admin Runners" do
end
end
describe 'when a runner is updated', :js do
context 'when a runner is updated', :js do
before do
click_on _('Save changes')
wait_for_requests
@ -556,21 +562,21 @@ RSpec.describe "Admin Runners" do
describe 'projects' do
it 'contains project names' do
expect(page).to have_content(@project1.full_name)
expect(page).to have_content(@project2.full_name)
expect(page).to have_content(project1.full_name)
expect(page).to have_content(project2.full_name)
end
end
describe 'search' do
before do
search_form = find('#runner-projects-search')
search_form.fill_in 'search', with: @project1.name
search_form.fill_in 'search', with: project1.name
search_form.click_button 'Search'
end
it 'contains name of correct project' do
expect(page).to have_content(@project1.full_name)
expect(page).not_to have_content(@project2.full_name)
expect(page).to have_content(project1.full_name)
expect(page).not_to have_content(project2.full_name)
end
end
@ -584,12 +590,12 @@ RSpec.describe "Admin Runners" do
assigned_project = page.find('[data-testid="assigned-projects"]')
expect(page).to have_content('Runner assigned to project.')
expect(assigned_project).to have_content(@project2.path)
expect(assigned_project).to have_content(project2.path)
end
end
context 'with specific runner' do
let(:runner) { create(:ci_runner, :project, projects: [@project1]) }
let(:runner) { create(:ci_runner, :project, projects: [project1]) }
before do
visit edit_admin_runner_path(runner)
@ -599,7 +605,7 @@ RSpec.describe "Admin Runners" do
end
context 'with locked runner' do
let(:runner) { create(:ci_runner, :project, projects: [@project1], locked: true) }
let(:runner) { create(:ci_runner, :project, projects: [project1], locked: true) }
before do
visit edit_admin_runner_path(runner)
@ -610,7 +616,7 @@ RSpec.describe "Admin Runners" do
end
describe 'disable/destroy' do
let(:runner) { create(:ci_runner, :project, projects: [@project1]) }
let(:runner) { create(:ci_runner, :project, projects: [project1]) }
before do
visit edit_admin_runner_path(runner)
@ -624,7 +630,7 @@ RSpec.describe "Admin Runners" do
new_runner_project = page.find('[data-testid="unassigned-projects"]')
expect(page).to have_content('Runner unassigned from project.')
expect(new_runner_project).to have_content(@project1.path)
expect(new_runner_project).to have_content(project1.path)
end
end
end

View File

@ -17,7 +17,7 @@ RSpec.describe "Group Runners" do
describe "Group runners page", :js do
let!(:group_registration_token) { group.runners_token }
context "runners registration" do
describe "runners registration" do
before do
visit group_runners_path(group)
end
@ -128,7 +128,7 @@ RSpec.describe "Group Runners" do
end
end
context 'filtered search' do
describe 'filtered search' do
before do
visit group_runners_path(group)
end

View File

@ -591,14 +591,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when a new failures exists' do
let(:base_reports) do
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
end
let(:head_reports) do
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
reports.get_suite('junit').add_test_case(create_test_case_java_failed)
end
@ -639,14 +639,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when an existing failure exists' do
let(:base_reports) do
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
reports.get_suite('rspec').add_test_case(create_test_case_rspec_failed)
reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
end
let(:head_reports) do
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
reports.get_suite('rspec').add_test_case(create_test_case_rspec_failed)
reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
@ -686,14 +686,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when a resolved failure exists' do
let(:base_reports) do
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
reports.get_suite('junit').add_test_case(create_test_case_java_failed)
end
end
let(:head_reports) do
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
@ -732,14 +732,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when a new error exists' do
let(:base_reports) do
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
end
let(:head_reports) do
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
reports.get_suite('junit').add_test_case(create_test_case_java_error)
end
@ -779,14 +779,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when an existing error exists' do
let(:base_reports) do
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
reports.get_suite('rspec').add_test_case(create_test_case_rspec_error)
reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
end
let(:head_reports) do
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
reports.get_suite('rspec').add_test_case(create_test_case_rspec_error)
reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
@ -825,14 +825,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when a resolved error exists' do
let(:base_reports) do
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
reports.get_suite('junit').add_test_case(create_test_case_java_error)
end
end
let(:head_reports) do
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
reports.get_suite('junit').add_test_case(create_test_case_java_success)
end
@ -871,7 +871,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'properly truncates the report' do
let(:base_reports) do
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
10.times do |index|
reports.get_suite('rspec').add_test_case(
create_test_case_rspec_failed(index))
@ -882,7 +882,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end
let(:head_reports) do
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
10.times do |index|
reports.get_suite('rspec').add_test_case(
create_test_case_rspec_failed(index))

View File

@ -464,6 +464,20 @@ RSpec.describe 'File blob', :js do
end
end
context 'binary file that appears to be text in the first 1024 bytes' do
before do
visit_blob('encoding/binary-1.bin', ref: 'binary-encoding')
end
it 'displays the blob' do
expect(page).to have_link('Download (23.81 KiB)')
# does not show a viewer switcher
expect(page).not_to have_selector('.js-blob-viewer-switcher')
expect(page).not_to have_selector('.js-copy-blob-source-btn:not(.disabled)')
expect(page).not_to have_link('Open raw')
end
end
context 'empty file' do
before do
project.add_maintainer(project.creator)

View File

@ -169,8 +169,8 @@ RSpec.describe 'Diff file viewer', :js, :with_clean_rails_cache do
wait_for_requests
end
it 'shows there is no preview' do
expect(page).to have_content('No preview for this file type')
it 'shows that file was added' do
expect(page).to have_content('File added')
end
end
end

View File

@ -1,215 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ResourceLinksBlock with defaults renders correct component 1`] = `
<div
class="gl-mt-5"
id="resource-links"
>
<div
class="card card-slim gl-overflow-hidden"
>
<div
class="card-header gl-display-flex gl-justify-content-space-between panel-empty-heading border-bottom-0"
>
<h3
class="card-title h5 position-relative gl-my-0 gl-display-flex gl-align-items-center gl-h-7"
>
<a
aria-hidden="true"
class="gl-link anchor position-absolute gl-text-decoration-none"
href="#resource-links"
id="user-content-resource-links"
/>
Linked resources
<a
aria-label="Read more about linked resources"
class="gl-link gl-display-flex gl-align-items-center gl-ml-2 gl-text-gray-500"
data-testid="help-link"
href="/help/user/project/issues/linked_resources"
rel="noopener"
target="_blank"
>
<svg
aria-hidden="true"
class="gl-icon s12"
data-testid="question-icon"
role="img"
>
<use
href="#question"
/>
</svg>
</a>
<div
class="gl-display-inline-flex"
>
<div
class="gl-display-inline-flex gl-mx-5"
>
<span
class="gl-display-inline-flex gl-align-items-center"
>
<svg
aria-hidden="true"
class="gl-mr-2 gl-text-gray-500 gl-icon s16"
data-testid="link-icon"
role="img"
>
<use
href="#link"
/>
</svg>
0
</span>
</div>
<button
aria-label="Add a resource link"
class="btn btn-default btn-md gl-button btn-icon"
type="button"
>
<!---->
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="plus-icon"
role="img"
>
<use
href="#plus"
/>
</svg>
<!---->
</button>
</div>
</h3>
</div>
<div
class="linked-issues-card-body bg-gray-light"
>
<div
class="card-body bordered-box gl-bg-white"
style="display: none;"
>
<form>
<fieldset
aria-describedby=""
class="form-group gl-form-group"
id="__BVID__14"
>
<legend
class="bv-no-focus-ring col-form-label pt-0 col-form-label"
id="__BVID__14__BV_label_"
tabindex="-1"
>
Text (Optional)
<!---->
<!---->
</legend>
<div
aria-labelledby="__BVID__14__BV_label_"
class="bv-no-focus-ring"
role="group"
tabindex="-1"
>
<input
class="gl-form-input form-control"
data-testid="link-text-input"
id="__BVID__16"
type="text"
/>
<!---->
<!---->
<!---->
</div>
</fieldset>
<fieldset
aria-describedby=""
class="form-group gl-form-group"
id="__BVID__18"
>
<legend
class="bv-no-focus-ring col-form-label pt-0 col-form-label"
id="__BVID__18__BV_label_"
tabindex="-1"
>
Link
<!---->
<!---->
</legend>
<div
aria-labelledby="__BVID__18__BV_label_"
class="bv-no-focus-ring"
role="group"
tabindex="-1"
>
<input
class="gl-form-input form-control"
data-testid="link-value-input"
id="__BVID__20"
type="text"
/>
<!---->
<!---->
<!---->
</div>
</fieldset>
<div
class="gl-mt-5 gl-clearfix"
>
<button
class="btn gl-float-left btn-confirm btn-md disabled gl-button"
data-testid="add-button"
disabled="disabled"
type="submit"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Add
</span>
</button>
<button
class="btn gl-float-right btn-default btn-md gl-button"
type="button"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Cancel
</span>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
`;

View File

@ -1,61 +0,0 @@
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import AddIssuableResourceLinkForm from '~/linked_resources/components/add_issuable_resource_link_form.vue';
describe('AddIssuableResourceLinkForm', () => {
let wrapper;
const mountComponent = () => {
wrapper = mountExtended(AddIssuableResourceLinkForm);
};
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
const findAddButton = () => wrapper.findByTestId('add-button');
const findCancelButton = () => wrapper.findByText('Cancel');
const findLinkTextInput = () => wrapper.findByTestId('link-text-input');
const findLinkValueInput = () => wrapper.findByTestId('link-value-input');
const cancelForm = async () => {
await findCancelButton().trigger('click');
};
describe('cancel form button', () => {
const closeFormEvent = { 'add-issuable-resource-link-form-cancel': [[]] };
beforeEach(() => {
mountComponent();
});
it('should close the form on cancel', async () => {
await cancelForm();
expect(wrapper.emitted()).toEqual(closeFormEvent);
});
it('keeps the button disabled without input', () => {
expect(findAddButton().props('disabled')).toBe(true);
});
it('keeps the button disabled with only text input', async () => {
findLinkTextInput().setValue('link text');
await nextTick();
expect(findAddButton().props('disabled')).toBe(true);
});
it('enables add button when link input is provided', async () => {
findLinkTextInput().setValue('link text');
findLinkValueInput().setValue('https://foo.example.com');
await nextTick();
expect(findAddButton().props('disabled')).toBe(false);
});
});
});

View File

@ -1,90 +0,0 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ResourceLinksBlock from '~/linked_resources/components/resource_links_block.vue';
import AddIssuableResourceLinkForm from '~/linked_resources/components/add_issuable_resource_link_form.vue';
describe('ResourceLinksBlock', () => {
let wrapper;
const findResourceLinkAddButton = () => wrapper.find(GlButton);
const resourceLinkForm = () => wrapper.findComponent(AddIssuableResourceLinkForm);
const helpPath = '/help/user/project/issues/linked_resources';
const mountComponent = () => {
wrapper = mountExtended(ResourceLinksBlock, {
propsData: {
helpPath,
canAddResourceLinks: true,
},
data() {
return {
isFormVisible: false,
};
},
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
};
describe('with defaults', () => {
beforeEach(() => {
mountComponent();
});
it('renders correct component', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('should show the form when add button is clicked', async () => {
await findResourceLinkAddButton().trigger('click');
expect(resourceLinkForm().isVisible()).toBe(true);
});
it('should hide the form when the hide event is emitted', async () => {
// open the form
await findResourceLinkAddButton().trigger('click');
await resourceLinkForm().vm.$emit('add-issuable-resource-link-form-cancel');
expect(resourceLinkForm().isVisible()).toBe(false);
});
});
describe('with canAddResourceLinks=false', () => {
it('does not show the add button', () => {
wrapper = shallowMount(ResourceLinksBlock, {
propsData: {
canAddResourceLinks: false,
},
});
expect(findResourceLinkAddButton().exists()).toBe(false);
expect(resourceLinkForm().isVisible()).toBe(false);
});
});
describe('with isFormVisible=true', () => {
it('renders the form with correct props', () => {
wrapper = shallowMount(ResourceLinksBlock, {
propsData: {
canAddResourceLinks: true,
},
data() {
return {
isFormVisible: true,
isSubmitting: false,
};
},
});
expect(resourceLinkForm().exists()).toBe(true);
expect(resourceLinkForm().props('isSubmitting')).toBe(false);
});
});
});

View File

@ -109,5 +109,17 @@ describe('cleanup_status', () => {
expect(findPopover().findComponent(GlLink).exists()).toBe(true);
expect(findPopover().findComponent(GlLink).attributes('href')).toBe(cleanupPolicyHelpPage);
});
it('id matches popover target attribute', () => {
mountComponent({
status: UNFINISHED_STATUS,
next_run_at: '2063-04-08T01:44:03Z',
});
const id = findExtraInfoIcon().attributes('id');
expect(id).toMatch(/status-info-[0-9]+/);
expect(findPopover().props('target')).toEqual(id);
});
});
});

View File

@ -0,0 +1,141 @@
import Vue from 'vue';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue';
import changeWorkItemParentMutation from '~/work_items/graphql/change_work_item_parent_link.mutation.graphql';
import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
import { WIDGET_TYPE_HIERARCHY } from '~/work_items/constants';
import { workItemHierarchyResponse, changeWorkItemParentMutationResponse } from '../../mock_data';
Vue.use(VueApollo);
const PARENT_ID = 'gid://gitlab/WorkItem/1';
const WORK_ITEM_ID = 'gid://gitlab/WorkItem/3';
describe('WorkItemLinksMenu', () => {
let wrapper;
let mockApollo;
const $toast = {
show: jest.fn(),
};
const createComponent = async ({
data = {},
mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse),
} = {}) => {
mockApollo = createMockApollo([
[getWorkItemLinksQuery, jest.fn().mockResolvedValue(workItemHierarchyResponse)],
[changeWorkItemParentMutation, mutationHandler],
]);
mockApollo.clients.defaultClient.cache.writeQuery({
query: getWorkItemLinksQuery,
variables: {
id: PARENT_ID,
},
data: workItemHierarchyResponse.data,
});
wrapper = shallowMountExtended(WorkItemLinksMenu, {
data() {
return {
...data,
};
},
propsData: {
workItemId: WORK_ITEM_ID,
parentWorkItemId: PARENT_ID,
},
apolloProvider: mockApollo,
mocks: {
$toast,
},
});
await waitForPromises();
};
const findDropdown = () => wrapper.find(GlDropdown);
const findRemoveDropdownItem = () => wrapper.find(GlDropdownItem);
beforeEach(async () => {
await createComponent();
});
afterEach(() => {
wrapper.destroy();
mockApollo = null;
});
it('renders dropdown and dropdown items', () => {
expect(findDropdown().exists()).toBe(true);
expect(findRemoveDropdownItem().exists()).toBe(true);
});
it('calls correct mutation with correct variables', async () => {
const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse);
createComponent({ mutationHandler });
findRemoveDropdownItem().vm.$emit('click');
await waitForPromises();
expect(mutationHandler).toHaveBeenCalledWith({
id: WORK_ITEM_ID,
parentId: null,
});
});
it('shows toast when mutation succeeds', async () => {
const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse);
createComponent({ mutationHandler });
findRemoveDropdownItem().vm.$emit('click');
await waitForPromises();
expect($toast.show).toHaveBeenCalledWith('Child removed', {
action: { onClick: expect.anything(), text: 'Undo' },
});
});
it('updates the cache when mutation succeeds', async () => {
const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse);
createComponent({ mutationHandler });
mockApollo.clients.defaultClient.cache.readQuery = jest.fn(
() => workItemHierarchyResponse.data,
);
mockApollo.clients.defaultClient.cache.writeQuery = jest.fn();
findRemoveDropdownItem().vm.$emit('click');
await waitForPromises();
// Remove the work item from parent's children
const resp = cloneDeep(workItemHierarchyResponse);
const index = resp.data.workItem.widgets
.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)
.children.nodes.findIndex((child) => child.id === WORK_ITEM_ID);
resp.data.workItem.widgets
.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)
.children.nodes.splice(index, 1);
expect(mockApollo.clients.defaultClient.cache.writeQuery).toHaveBeenCalledWith(
expect.objectContaining({
query: expect.anything(),
variables: { id: PARENT_ID },
data: resp.data,
}),
);
});
});

View File

@ -392,6 +392,25 @@ export const workItemHierarchyResponse = {
},
};
export const changeWorkItemParentMutationResponse = {
data: {
workItemUpdate: {
workItem: {
id: 'gid://gitlab/WorkItem/2',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/5',
__typename: 'WorkItemType',
},
title: 'Foo',
state: 'OPEN',
__typename: 'WorkItem',
},
errors: [],
__typename: 'WorkItemUpdatePayload',
},
},
};
export const availableWorkItemsResponse = {
data: {
workspace: {

View File

@ -222,4 +222,60 @@ RSpec.describe Users::CalloutsHelper do
it { is_expected.to be true }
end
end
describe '#web_hook_disabled_dismissed?' do
context 'without a project' do
it 'is false' do
expect(helper).not_to be_web_hook_disabled_dismissed(nil)
end
end
context 'with a project' do
let_it_be(:project) { create(:project) }
context 'the web-hook failure callout has never been dismissed' do
it 'is false' do
expect(helper).not_to be_web_hook_disabled_dismissed(project)
end
end
context 'the web-hook failure callout has been dismissed', :freeze_time do
before do
create(:namespace_callout,
feature_name: described_class::WEB_HOOK_DISABLED,
user: user,
namespace: project.namespace,
dismissed_at: 1.week.ago)
end
it 'is true' do
expect(helper).to be_web_hook_disabled_dismissed(project)
end
context 'when there was an older failure', :clean_gitlab_redis_shared_state do
let(:key) { "web_hooks:last_failure:project-#{project.id}" }
before do
Gitlab::Redis::SharedState.with { |r| r.set(key, 1.month.ago.iso8601) }
end
it 'is true' do
expect(helper).to be_web_hook_disabled_dismissed(project)
end
end
context 'when there has been a more recent failure', :clean_gitlab_redis_shared_state do
let(:key) { "web_hooks:last_failure:project-#{project.id}" }
before do
Gitlab::Redis::SharedState.with { |r| r.set(key, 1.day.ago.iso8601) }
end
it 'is false' do
expect(helper).not_to be_web_hook_disabled_dismissed(project)
end
end
end
end
end
end

View File

@ -0,0 +1,120 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WebHooks::WebHooksHelper do
let_it_be_with_reload(:project) { create(:project) }
let(:current_user) { nil }
let(:callout_dismissed) { false }
let(:web_hooks_disable_failed) { false }
let(:webhooks_failed_callout) { false }
before do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:web_hook_disabled_dismissed?).with(project).and_return(callout_dismissed)
stub_feature_flags(
webhooks_failed_callout: webhooks_failed_callout,
web_hooks_disable_failed: web_hooks_disable_failed
)
end
shared_context 'user is logged in' do
let(:current_user) { create(:user) }
end
shared_context 'webhooks_failed_callout is enabled' do
let(:webhooks_failed_callout) { true }
end
shared_context 'webhooks_failed_callout is enabled for this project' do
let(:webhooks_failed_callout) { project }
end
shared_context 'web_hooks_disable_failed is enabled' do
let(:web_hooks_disable_failed) { true }
end
shared_context 'web_hooks_disable_failed is enabled for this project' do
let(:web_hooks_disable_failed) { project }
end
shared_context 'the user has permission' do
before do
project.add_maintainer(current_user)
end
end
shared_context 'the user dismissed the callout' do
let(:callout_dismissed) { true }
end
shared_context 'a hook has failed' do
before do
create(:project_hook, :permanently_disabled, project: project)
end
end
describe '#show_project_hook_failed_callout?' do
context 'all conditions are met' do
include_context 'user is logged in'
include_context 'webhooks_failed_callout is enabled'
include_context 'web_hooks_disable_failed is enabled'
include_context 'the user has permission'
include_context 'a hook has failed'
it 'is true' do
expect(helper).to be_show_project_hook_failed_callout(project: project)
end
it 'caches the DB calls until the TTL', :use_clean_rails_memory_store_caching, :request_store do
helper.show_project_hook_failed_callout?(project: project)
travel_to((described_class::EXPIRY_TTL - 1.second).from_now) do
expect do
helper.show_project_hook_failed_callout?(project: project)
end.not_to exceed_query_limit(0)
end
travel_to((described_class::EXPIRY_TTL + 1.second).from_now) do
expect do
helper.show_project_hook_failed_callout?(project: project)
end.to exceed_query_limit(0)
end
end
end
context 'all conditions are met, project scoped flags' do
include_context 'user is logged in'
include_context 'webhooks_failed_callout is enabled for this project'
include_context 'web_hooks_disable_failed is enabled for this project'
include_context 'the user has permission'
include_context 'a hook has failed'
it 'is true' do
expect(helper).to be_show_project_hook_failed_callout(project: project)
end
end
context 'one condition is not met' do
contexts = [
'user is logged in',
'webhooks_failed_callout is enabled',
'web_hooks_disable_failed is enabled',
'the user has permission',
'a hook has failed'
]
contexts.each do |name|
context "namely #{name}" do
contexts.each { |ctx| include_context(ctx) unless ctx == name }
it 'is false' do
expect(helper).not_to be_show_project_hook_failed_callout(project: project)
end
end
end
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::TestReports do
RSpec.describe Gitlab::Ci::Reports::TestReport do
include TestReportsHelper
let(:test_reports) { described_class.new }

View File

@ -6,8 +6,8 @@ RSpec.describe Gitlab::Ci::Reports::TestReportsComparer do
include TestReportsHelper
let(:comparer) { described_class.new(base_reports, head_reports) }
let(:base_reports) { Gitlab::Ci::Reports::TestReports.new }
let(:head_reports) { Gitlab::Ci::Reports::TestReports.new }
let(:base_reports) { Gitlab::Ci::Reports::TestReport.new }
let(:head_reports) { Gitlab::Ci::Reports::TestReport.new }
describe '#suite_comparers' do
subject { comparer.suite_comparers }

View File

@ -68,10 +68,10 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
end
context 'with multiple jobs to run' do
it 'runs all jobs created within the last 48 hours' do
it 'runs all jobs created within the last 3 hours' do
old_migration = define_background_migration(migration_name)
travel 3.days
travel 4.hours
new_migration = define_background_migration('NewMigration') { travel 1.second }
migration.queue_batched_background_migration('NewMigration', table_name, :id,

View File

@ -33,7 +33,7 @@ RSpec.describe Gitlab::DependencyLinker::BaseLinker do
it 'only converts valid links' do
expect(subject).to eq(
<<~CONTENT
<span><span>#{link('http://')}</span><span>#{link('\n', url: '%5Cn')}</span><span>#{link('javascript:alert(1)', url: nil)}</span></span>
<span><span>#{link('http://', url: nil)}</span><span>#{link('\n', url: nil)}</span><span>#{link('javascript:alert(1)', url: nil)}</span></span>
<span><span>#{link('https://gitlab.com/gitlab-org/gitlab')}</span></span>
CONTENT
)

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe UpdateNotesInPast, :migration do
let(:notes) { table(:notes) }
it 'updates created_at when it is too much in the past' do
notes.create!(id: 10, note: 'note', created_at: '2009-06-01')
notes.create!(id: 11, note: 'note', created_at: '1970-01-01')
notes.create!(id: 12, note: 'note', created_at: '1600-06-01')
migrate!
expect(notes.all).to contain_exactly(
an_object_having_attributes(id: 10, created_at: DateTime.parse('2009-06-01')),
an_object_having_attributes(id: 11, created_at: DateTime.parse('1970-01-01')),
an_object_having_attributes(id: 12, created_at: DateTime.parse('1970-01-01'))
)
end
end

View File

@ -4281,7 +4281,7 @@ RSpec.describe Ci::Build do
describe '#collect_test_reports!' do
subject { build.collect_test_reports!(test_reports) }
let(:test_reports) { Gitlab::Ci::Reports::TestReports.new }
let(:test_reports) { Gitlab::Ci::Reports::TestReport.new }
it { expect(test_reports.get_suite(build.name).total_count).to eq(0) }
@ -4332,7 +4332,7 @@ RSpec.describe Ci::Build do
context 'when build is part of parallel build' do
let(:build_1) { create(:ci_build, name: 'build 1/2') }
let(:test_report) { Gitlab::Ci::Reports::TestReports.new }
let(:test_report) { Gitlab::Ci::Reports::TestReport.new }
before do
build_1.collect_test_reports!(test_report)
@ -4356,7 +4356,7 @@ RSpec.describe Ci::Build do
end
context 'when build is part of matrix build' do
let(:test_report) { Gitlab::Ci::Reports::TestReports.new }
let(:test_report) { Gitlab::Ci::Reports::TestReport.new }
let(:matrix_build_1) { create(:ci_build, :matrix) }
before do

View File

@ -15,6 +15,19 @@ RSpec.describe ProjectHook do
subject { build(:project_hook, project: create(:project)) }
end
describe '.for_projects' do
it 'finds related project hooks' do
hook_a = create(:project_hook)
hook_b = create(:project_hook)
hook_c = create(:project_hook)
expect(described_class.for_projects([hook_a.project, hook_b.project]))
.to contain_exactly(hook_a, hook_b)
expect(described_class.for_projects(hook_c.project))
.to contain_exactly(hook_c)
end
end
describe '.push_hooks' do
it 'returns hooks for push events only' do
hook = create(:project_hook, push_events: true)
@ -50,4 +63,62 @@ RSpec.describe ProjectHook do
)
end
end
describe '#update_last_failure', :clean_gitlab_redis_shared_state do
let_it_be(:hook) { create(:project_hook) }
it 'is a method of this class' do
expect { hook.update_last_failure }.not_to raise_error
end
context 'when the hook is executable' do
it 'does not update the state' do
expect(Gitlab::Redis::SharedState).not_to receive(:with)
hook.update_last_failure
end
end
context 'when the hook is failed' do
before do
allow(hook).to receive(:executable?).and_return(false)
end
def last_failure
Gitlab::Redis::SharedState.with do |redis|
redis.get("web_hooks:last_failure:project-#{hook.project.id}")
end
end
context 'there is no prior value', :freeze_time do
it 'updates the state' do
expect { hook.update_last_failure }.to change { last_failure }.to(Time.current)
end
end
context 'there is a prior value, from before now' do
it 'updates the state' do
the_future = 1.minute.from_now
hook.update_last_failure
travel_to(the_future) do
expect { hook.update_last_failure }.to change { last_failure }.to(the_future.iso8601)
end
end
end
context 'there is a prior value, from after now' do
it 'does not update the state' do
the_past = 1.minute.ago
hook.update_last_failure
travel_to(the_past) do
expect { hook.update_last_failure }.not_to change { last_failure }
end
end
end
end
end
end

View File

@ -187,8 +187,8 @@ RSpec.describe WebHook do
end
end
describe '.executable' do
let(:not_executable) do
describe '.executable/.disabled' do
let!(:not_executable) do
[
[0, Time.current],
[0, 1.minute.from_now],
@ -202,7 +202,7 @@ RSpec.describe WebHook do
end
end
let(:executables) do
let!(:executables) do
[
[0, nil],
[0, 1.day.ago],
@ -217,6 +217,7 @@ RSpec.describe WebHook do
it 'finds the correct set of project hooks' do
expect(described_class.where(project_id: project.id).executable).to match_array executables
expect(described_class.where(project_id: project.id).disabled).to match_array not_executable
end
context 'when the feature flag is not enabled' do
@ -224,7 +225,7 @@ RSpec.describe WebHook do
stub_feature_flags(web_hooks_disable_failed: false)
end
it 'is the same as all' do
specify 'enabled is the same as all' do
expect(described_class.where(project_id: project.id).executable).to match_array(executables + not_executable)
end
end
@ -635,4 +636,10 @@ RSpec.describe WebHook do
end
end
end
describe '#update_last_failure' do
it 'is a method of this class' do
expect { described_class.new.update_last_failure }.not_to raise_error
end
end
end

View File

@ -106,6 +106,22 @@ RSpec.describe Note do
end
end
describe 'created_at in the past' do
let_it_be(:noteable) { create(:issue) }
context 'when creating a note not too much in the past' do
subject { build(:note, project: noteable.project, noteable: noteable, created_at: '1990-05-06') }
it { is_expected.to be_valid }
end
context 'when creating a note too much in the past' do
subject { build(:note, project: noteable.project, noteable: noteable, created_at: '1600-05-06') }
it { is_expected.not_to be_valid }
end
end
describe 'confidentiality' do
context 'for existing public note' do
let_it_be(:existing_note) { create(:note) }

View File

@ -136,6 +136,7 @@ RSpec.describe User do
it { is_expected.to have_many(:timelogs) }
it { is_expected.to have_many(:callouts).class_name('Users::Callout') }
it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout') }
it { is_expected.to have_many(:namespace_callouts).class_name('Users::NamespaceCallout') }
describe '#user_detail' do
it 'does not persist `user_detail` by default' do
@ -6415,6 +6416,96 @@ RSpec.describe User do
end
end
describe 'Users::NamespaceCallout' do
describe '#dismissed_callout_for_namespace?' do
let_it_be(:user, refind: true) { create(:user) }
let_it_be(:namespace) { create(:namespace) }
let_it_be(:feature_name) { Users::NamespaceCallout.feature_names.each_key.first }
let(:query) do
{ feature_name: feature_name, namespace: namespace }
end
def have_dismissed_callout
be_dismissed_callout_for_namespace(**query)
end
context 'when no callout dismissal record exists' do
it 'returns false when no ignore_dismissal_earlier_than provided' do
expect(user).not_to have_dismissed_callout
end
end
context 'when dismissed callout exists' do
before_all do
create(:namespace_callout,
user: user,
namespace_id: namespace.id,
feature_name: feature_name,
dismissed_at: 4.months.ago)
end
it 'returns true when no ignore_dismissal_earlier_than provided' do
expect(user).to have_dismissed_callout
end
it 'returns true when ignore_dismissal_earlier_than is earlier than dismissed_at' do
query[:ignore_dismissal_earlier_than] = 6.months.ago
expect(user).to have_dismissed_callout
end
it 'returns false when ignore_dismissal_earlier_than is later than dismissed_at' do
query[:ignore_dismissal_earlier_than] = 2.months.ago
expect(user).not_to have_dismissed_callout
end
end
end
describe '#find_or_initialize_namespace_callout' do
let_it_be(:user, refind: true) { create(:user) }
let_it_be(:namespace) { create(:namespace) }
let_it_be(:feature_name) { Users::NamespaceCallout.feature_names.each_key.first }
subject(:callout_with_source) do
user.find_or_initialize_namespace_callout(feature_name, namespace.id)
end
context 'when callout exists' do
let!(:callout) do
create(:namespace_callout, user: user, feature_name: feature_name, namespace_id: namespace.id)
end
it 'returns existing callout' do
expect(callout_with_source).to eq(callout)
end
end
context 'when callout does not exist' do
context 'when feature name is valid' do
it 'initializes a new callout' do
expect(callout_with_source)
.to be_a_new(Users::NamespaceCallout)
.and be_valid
end
end
context 'when feature name is not valid' do
let(:feature_name) { 'notvalid' }
it 'initializes a new callout' do
expect(callout_with_source).to be_a_new(Users::NamespaceCallout)
end
it 'is not valid' do
expect(callout_with_source).not_to be_valid
end
end
end
end
end
describe '#dismissed_callout_for_group?' do
let_it_be(:user, refind: true) { create(:user) }
let_it_be(:group) { create(:group) }

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Users::NamespaceCallout do
let_it_be(:user) { create_default(:user) }
let_it_be(:namespace) { create_default(:namespace) }
let_it_be(:callout) { create(:namespace_callout) }
it_behaves_like 'having unique enum values'
describe 'relationships' do
it { is_expected.to belong_to(:namespace) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:namespace) }
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:feature_name) }
specify do
is_expected.to validate_uniqueness_of(:feature_name)
.scoped_to(:user_id, :namespace_id)
.ignoring_case_sensitivity
end
it { is_expected.to allow_value(:web_hook_disabled).for(:feature_name) }
it 'rejects invalid feature names' do
expect { callout.feature_name = :non_existent_feature }.to raise_error(ArgumentError)
end
end
describe '#source_feature_name' do
it 'provides string based off source and feature' do
expect(callout.source_feature_name).to eq "#{callout.feature_name}_#{callout.namespace_id}"
end
end
end

View File

@ -7,8 +7,8 @@ RSpec.describe TestReportsComparerEntity do
let(:entity) { described_class.new(comparer) }
let(:comparer) { Gitlab::Ci::Reports::TestReportsComparer.new(base_reports, head_reports) }
let(:base_reports) { Gitlab::Ci::Reports::TestReports.new }
let(:head_reports) { Gitlab::Ci::Reports::TestReports.new }
let(:base_reports) { Gitlab::Ci::Reports::TestReport.new }
let(:head_reports) { Gitlab::Ci::Reports::TestReport.new }
describe '#as_json' do
subject { entity.as_json }

View File

@ -8,8 +8,8 @@ RSpec.describe TestReportsComparerSerializer do
let(:project) { double(:project) }
let(:serializer) { described_class.new(project: project).represent(comparer) }
let(:comparer) { Gitlab::Ci::Reports::TestReportsComparer.new(base_reports, head_reports) }
let(:base_reports) { Gitlab::Ci::Reports::TestReports.new }
let(:head_reports) { Gitlab::Ci::Reports::TestReports.new }
let(:base_reports) { Gitlab::Ci::Reports::TestReport.new }
let(:head_reports) { Gitlab::Ci::Reports::TestReport.new }
describe '#to_json' do
subject { serializer.to_json }

View File

@ -35,6 +35,12 @@ RSpec.describe WebHooks::LogExecutionService do
expect(WebHookLog.recent.first).to have_attributes(data)
end
it 'updates the last failure' do
expect(project_hook).to receive(:update_last_failure)
service.execute
end
context 'obtaining an exclusive lease' do
let(:lease_key) { "web_hooks:update_hook_failure_state:#{project_hook.id}" }

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'errors/omniauth_error' do
let(:provider) { FFaker::Product.brand }
let(:error) { FFaker::Lorem.sentence }
before do
assign(:provider, provider)
assign(:error, error)
end
it 'renders template' do
render
expect(rendered).to have_content(provider)
expect(rendered).to have_content(_('Sign-in failed because %{error}.') % { error: error })
expect(rendered).to have_link('Sign in')
expect(rendered).to have_content(_('If none of the options work, try contacting a GitLab administrator.'))
end
end