Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-25 15:12:14 +00:00
parent 73507eaf1a
commit 5f85444a43
82 changed files with 816 additions and 902 deletions

View File

@ -130,27 +130,6 @@ variables:
REGISTRY_HOST: "registry.gitlab.com"
REGISTRY_GROUP: "gitlab-org"
# Preparing custom clone path to reduce space used by all random forks
# on GitLab.com's Shared Runners. Our main forks - especially the security
# ones - will have this variable overwritten in the project settings, so that
# a security-related code or code using our protected variables will be never
# stored on the same path as the community forks.
# Part of the solution for the `no space left on device` problem described at
# https://gitlab.com/gitlab-org/gitlab/issues/197876.
#
# For this purpose the https://gitlab.com/gitlab-org-forks group was created
# to host a placeholder for the `/builds/gitlab-org-forks` path and ensure
# that no legitimate project will ever use it and - by mistake - execute its
# job on a shared working directory. It also requires proper configuration of
# the Runner that executes the job (which was prepared for our shared runners
# by https://ops.gitlab.net/gitlab-cookbooks/chef-repo/-/merge_requests/3977).
#
# Because of all of that PLEASE DO NOT CHANGE THE PATH.
#
# For more details and reasoning that brought this change please check
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24887
GIT_CLONE_PATH: "/builds/gitlab-org-forks/${CI_PROJECT_NAME}"
include:
- local: .gitlab/ci/*.gitlab-ci.yml
- remote: 'https://gitlab.com/gitlab-org/frontend/untamper-my-lockfile/-/raw/main/templates/merge_request_pipelines.yml'

View File

@ -47,7 +47,6 @@ Style/RedundantSelf:
- 'app/models/commit_status.rb'
- 'app/models/compare.rb'
- 'app/models/concerns/after_commit_queue.rb'
- 'app/models/concerns/approvable.rb'
- 'app/models/concerns/atomic_internal_id.rb'
- 'app/models/concerns/avatarable.rb'
- 'app/models/concerns/awardable.rb'

View File

@ -1,7 +1,16 @@
<script>
import $ from 'jquery';
import { GlDropdown, GlButton, GlIcon, GlForm, GlFormGroup, GlLink } from '@gitlab/ui';
import {
GlDropdown,
GlButton,
GlIcon,
GlForm,
GlFormGroup,
GlLink,
GlFormCheckbox,
} from '@gitlab/ui';
import { mapGetters, mapActions } from 'vuex';
import { createAlert } from '~/flash';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
import Autosave from '~/autosave';
@ -15,29 +24,46 @@ export default {
GlForm,
GlFormGroup,
GlLink,
GlFormCheckbox,
MarkdownField,
ApprovalPassword: () => import('ee_component/batch_comments/components/approval_password.vue'),
},
data() {
return {
isSubmitting: false,
note: '',
noteData: {
noteable_type: '',
noteable_id: '',
note: '',
approve: false,
approval_password: '',
},
};
},
computed: {
...mapGetters(['getNotesData', 'getNoteableData', 'noteableType', 'getCurrentUserLastNote']),
},
watch: {
'noteData.approve': function noteDataApproveWatch() {
setTimeout(() => {
this.repositionDropdown();
});
},
},
mounted() {
this.autosave = new Autosave(
$(this.$refs.textarea),
`submit_review_dropdown/${this.getNoteableData.id}`,
);
this.noteData.noteable_type = this.noteableType;
this.noteData.noteable_id = this.getNoteableData.id;
// We override the Bootstrap Vue click outside behaviour
// to allow for clicking in the autocomplete dropdowns
// without this override the submit dropdown will close
// whenever a item in the autocomplete dropdown is clicked
const originalClickOutHandler = this.$refs.dropdown.$refs.dropdown.clickOutHandler;
this.$refs.dropdown.$refs.dropdown.clickOutHandler = (e) => {
const originalClickOutHandler = this.$refs.submitDropdown.$refs.dropdown.clickOutHandler;
this.$refs.submitDropdown.$refs.dropdown.clickOutHandler = (e) => {
if (!e.target.closest('.atwho-container')) {
originalClickOutHandler(e);
}
@ -45,26 +71,29 @@ export default {
},
methods: {
...mapActions('batchComments', ['publishReview']),
repositionDropdown() {
this.$refs.submitDropdown?.$refs.dropdown?.updatePopper();
},
async submitReview() {
const noteData = {
noteable_type: this.noteableType,
noteable_id: this.getNoteableData.id,
note: this.note,
};
this.isSubmitting = true;
await this.publishReview(noteData);
try {
await this.publishReview(this.noteData);
this.autosave.reset();
this.autosave.reset();
if (window.mrTabs && this.note) {
window.location.hash = `note_${this.getCurrentUserLastNote.id}`;
window.mrTabs.tabShown('show');
if (window.mrTabs && this.note) {
window.location.hash = `note_${this.getCurrentUserLastNote.id}`;
window.mrTabs.tabShown('show');
setTimeout(() =>
scrollToElement(document.getElementById(`note_${this.getCurrentUserLastNote.id}`)),
);
setTimeout(() =>
scrollToElement(document.getElementById(`note_${this.getCurrentUserLastNote.id}`)),
);
}
} catch (e) {
if (e.data?.message) {
createAlert({ message: e.data.message, captureError: true });
}
}
this.isSubmitting = false;
@ -79,8 +108,9 @@ export default {
<template>
<gl-dropdown
ref="dropdown"
ref="submitDropdown"
right
dropup
class="submit-review-dropdown"
data-qa-selector="submit_review_dropdown"
variant="info"
@ -110,7 +140,7 @@ export default {
<markdown-field
:is-submitting="isSubmitting"
:add-spacing-classes="false"
:textarea-value="note"
:textarea-value="noteData.note"
:markdown-preview-path="getNoteableData.preview_note_path"
:markdown-docs-path="getNotesData.markdownDocsPath"
:quick-actions-docs-path="getNotesData.quickActionsDocsPath"
@ -122,7 +152,7 @@ export default {
<textarea
id="review-note-body"
ref="textarea"
v-model="note"
v-model="noteData.note"
dir="auto"
:disabled="isSubmitting"
name="review[note]"
@ -139,6 +169,18 @@ export default {
</div>
</div>
</gl-form-group>
<template v-if="getNoteableData.current_user.can_approve">
<gl-form-checkbox v-model="noteData.approve" data-testid="approve_merge_request">
{{ __('Approve merge request') }}
</gl-form-checkbox>
<approval-password
v-if="getNoteableData.require_password_to_approve"
v-show="noteData.approve"
v-model="noteData.approval_password"
class="gl-mt-3"
data-testid="approve_password"
/>
</template>
<div class="gl-display-flex gl-justify-content-start gl-mt-5">
<gl-button
:loading="isSubmitting"

View File

@ -84,7 +84,11 @@ export const publishReview = ({ commit, dispatch, getters }, noteData = {}) => {
.publish(getters.getNotesData.draftsPublishPath, noteData)
.then(() => dispatch('updateDiscussionsAfterPublish'))
.then(() => commit(types.RECEIVE_PUBLISH_REVIEW_SUCCESS))
.catch(() => commit(types.RECEIVE_PUBLISH_REVIEW_ERROR));
.catch((e) => {
commit(types.RECEIVE_PUBLISH_REVIEW_ERROR);
throw e.response;
});
};
export const updateDiscussionsAfterPublish = async ({ dispatch, getters, rootGetters }) => {

View File

@ -183,7 +183,6 @@ export default {
:data-user-id="author.id"
:data-username="author.username"
>
<slot name="note-header-info"></slot>
<user-name-with-status
:name="authorName"
:availability="userAvailability(author)"
@ -207,6 +206,7 @@ export default {
@mouseleave="handleUsernameMouseLeave"
><span class="note-headline-light">@{{ author.username }}</span>
</a>
<slot name="note-header-info"></slot>
<gitlab-team-member-badge v-if="author && author.is_gitlab_employee" />
</span>
</template>

View File

@ -440,7 +440,7 @@ export default {
</gl-avatar-link>
</div>
<div v-else class="gl-float-left gl-pl-3 gl-mr-3 gl-md-pl-2 gl-md-pr-2">
<div v-else class="gl-float-left gl-pl-3 gl-md-pl-2">
<gl-avatar-link :href="author.path">
<gl-avatar
:src="author.avatar_url"

View File

@ -1,7 +1,7 @@
<template>
<section class="settings gl-py-7">
<div class="gl-lg-display-flex gl-gap-6">
<div class="gl-lg-w-40p gl-pr-10 gl-flex-shrink-0">
<div class="row">
<div class="col-lg-4">
<h4>
<slot name="title"></slot>
</h4>
@ -9,7 +9,7 @@
<slot name="description"></slot>
</p>
</div>
<div class="gl-pt-3 gl-flex-grow-1">
<div class="col-lg-8 gl-pt-3">
<slot></slot>
</div>
</div>

View File

@ -281,7 +281,6 @@ export default {
:type="graphViewType"
:show-links="showLinks"
:tip-previously-dismissed="hoverTipPreviouslyDismissed"
:is-pipeline-complete="pipeline.complete"
@dismissHoverTip="handleTipDismissal"
@updateViewType="updateViewType"
@updateShowLinksState="updateShowLinksState"

View File

@ -1,33 +1,19 @@
<script>
import {
GlAlert,
GlButton,
GlButtonGroup,
GlLoadingIcon,
GlToggle,
GlModalDirective,
} from '@gitlab/ui';
import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import PerformanceInsightsModal from '../performance_insights_modal.vue';
import { performanceModalId } from '../../constants';
import { STAGE_VIEW, LAYER_VIEW } from './constants';
export default {
name: 'GraphViewSelector',
performanceModalId,
components: {
GlAlert,
GlButton,
GlButtonGroup,
GlLoadingIcon,
GlToggle,
PerformanceInsightsModal,
},
directives: {
GlModal: GlModalDirective,
},
mixins: [Tracking.mixin()],
props: {
showLinks: {
type: Boolean,
@ -41,10 +27,6 @@ export default {
type: String,
required: true,
},
isPipelineComplete: {
type: Boolean,
required: true,
},
},
data() {
return {
@ -59,7 +41,6 @@ export default {
hoverTipText: __('Tip: Hover over a job to see the jobs it depends on to run.'),
linksLabelText: s__('GraphViewType|Show dependencies'),
viewLabelText: __('Group jobs by'),
performanceBtnText: __('Performance insights'),
},
views: {
[STAGE_VIEW]: {
@ -150,9 +131,6 @@ export default {
this.$emit('updateShowLinksState', val);
});
},
trackInsightsClick() {
this.track('click_insights_button', { label: 'performance_insights' });
},
},
};
</script>
@ -178,15 +156,6 @@ export default {
</gl-button>
</gl-button-group>
<gl-button
v-if="isPipelineComplete"
v-gl-modal="$options.performanceModalId"
data-testid="pipeline-insights-btn"
@click="trackInsightsClick"
>
{{ $options.i18n.performanceBtnText }}
</gl-button>
<div v-if="showLinksToggle" class="gl-display-flex gl-align-items-center">
<gl-toggle
v-model="showLinksActive"
@ -202,7 +171,5 @@ export default {
<gl-alert v-if="showTip" class="gl-my-5" variant="tip" @dismiss="dismissTip">
{{ $options.i18n.hoverTipText }}
</gl-alert>
<performance-insights-modal />
</div>
</template>

View File

@ -1,171 +0,0 @@
<script>
import { GlAlert, GlCard, GlLink, GlLoadingIcon, GlModal } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { humanizeTimeInterval } from '~/lib/utils/datetime_utility';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import getPerformanceInsightsQuery from '../graphql/queries/get_performance_insights.query.graphql';
import { performanceModalId } from '../constants';
import { calculateJobStats, calculateSlowestFiveJobs } from '../utils';
export default {
name: 'PerformanceInsightsModal',
i18n: {
queuedCardHeader: s__('Pipeline|Longest queued job'),
queuedCardHelp: s__(
'Pipeline|The longest queued job is the job that spent the longest time in the pending state, waiting to be picked up by a Runner',
),
executedCardHeader: s__('Pipeline|Last executed job'),
executedCardHelp: s__(
'Pipeline|The last executed job is the last job to start in the pipeline.',
),
viewDependency: s__('Pipeline|View dependency'),
slowJobsTitle: s__('Pipeline|Five slowest jobs'),
feeback: __('Feedback issue'),
insightsLimit: s__('Pipeline|Only able to show first 100 results'),
},
modal: {
title: s__('Pipeline|Performance insights'),
actionCancel: {
text: __('Close'),
attributes: {
variant: 'confirm',
},
},
},
performanceModalId,
components: {
GlAlert,
GlCard,
GlLink,
GlModal,
GlLoadingIcon,
HelpPopover,
},
inject: {
pipelineIid: {
default: '',
},
pipelineProjectPath: {
default: '',
},
},
apollo: {
jobs: {
query: getPerformanceInsightsQuery,
variables() {
return {
fullPath: this.pipelineProjectPath,
iid: this.pipelineIid,
};
},
update(data) {
return data.project?.pipeline?.jobs;
},
},
},
data() {
return {
jobs: null,
};
},
computed: {
longestQueuedJob() {
return calculateJobStats(this.jobs, 'queuedDuration');
},
lastExecutedJob() {
return calculateJobStats(this.jobs, 'startedAt');
},
slowestFiveJobs() {
return calculateSlowestFiveJobs(this.jobs);
},
queuedDurationDisplay() {
return humanizeTimeInterval(this.longestQueuedJob.queuedDuration);
},
showLimitMessage() {
return this.jobs.pageInfo.hasNextPage;
},
},
};
</script>
<template>
<gl-modal
:modal-id="$options.performanceModalId"
:title="$options.modal.title"
:action-cancel="$options.modal.actionCancel"
>
<gl-loading-icon v-if="$apollo.queries.jobs.loading" size="lg" />
<template v-else>
<gl-alert class="gl-mb-4" :dismissible="false">
<p v-if="showLimitMessage" data-testid="limit-alert-text">
{{ $options.i18n.insightsLimit }}
</p>
<gl-link href="https://gitlab.com/gitlab-org/gitlab/-/issues/365902" class="gl-mt-5">
{{ $options.i18n.feeback }}
</gl-link>
</gl-alert>
<div class="gl-display-flex gl-justify-content-space-between gl-mt-2 gl-mb-7">
<gl-card class="gl-w-half gl-mr-7 gl-text-center">
<template #header>
<span class="gl-font-weight-bold">{{ $options.i18n.queuedCardHeader }}</span>
<help-popover>
{{ $options.i18n.queuedCardHelp }}
</help-popover>
</template>
<div class="gl-display-flex gl-flex-direction-column">
<span
class="gl-font-weight-bold gl-font-size-h2 gl-mb-2"
data-testid="insights-queued-card-data"
>
{{ queuedDurationDisplay }}
</span>
<gl-link
:href="longestQueuedJob.detailedStatus.detailsPath"
data-testid="insights-queued-card-link"
>
{{ longestQueuedJob.name }}
</gl-link>
</div>
</gl-card>
<gl-card class="gl-w-half gl-text-center" data-testid="insights-executed-card">
<template #header>
<span class="gl-font-weight-bold">{{ $options.i18n.executedCardHeader }}</span>
<help-popover>
{{ $options.i18n.executedCardHelp }}
</help-popover>
</template>
<div class="gl-display-flex gl-flex-direction-column">
<span
class="gl-font-weight-bold gl-font-size-h2 gl-mb-2"
data-testid="insights-executed-card-data"
>
{{ lastExecutedJob.name }}
</span>
<gl-link
:href="lastExecutedJob.detailedStatus.detailsPath"
data-testid="insights-executed-card-link"
>
{{ $options.i18n.viewDependency }}
</gl-link>
</div>
</gl-card>
</div>
<div class="gl-mt-7">
<span class="gl-font-weight-bold">{{ $options.i18n.slowJobsTitle }}</span>
<div
v-for="job in slowestFiveJobs"
:key="job.name"
class="gl-display-flex gl-justify-content-space-between gl-mb-3 gl-mt-3 gl-p-4 gl-border-t-1 gl-border-t-solid gl-border-b-0 gl-border-b-solid gl-border-gray-100"
>
<span data-testid="insights-slow-job-stage">{{ job.stage.name }}</span>
<gl-link :href="job.detailedStatus.detailsPath" data-testid="insights-slow-job-link">{{
job.name
}}</gl-link>
</div>
</div>
</template>
</gl-modal>
</template>

View File

@ -109,5 +109,3 @@ export const DEFAULT_FIELDS = [
columnClass: 'gl-w-20p',
},
];
export const performanceModalId = 'performanceInsightsModal';

View File

@ -1,28 +0,0 @@
query getPerformanceInsightsData($fullPath: ID!, $iid: ID!) {
project(fullPath: $fullPath) {
id
pipeline(iid: $iid) {
id
jobs {
pageInfo {
hasNextPage
}
nodes {
id
duration
detailedStatus {
id
detailsPath
}
name
stage {
id
name
}
startedAt
queuedDuration
}
}
}
}
}

View File

@ -153,24 +153,3 @@ export const getPipelineDefaultTab = (url) => {
return null;
};
export const calculateJobStats = (jobs, sortField) => {
const jobNodes = [...jobs.nodes];
const sorted = jobNodes.sort((a, b) => {
return b[sortField] - a[sortField];
});
return sorted[0];
};
export const calculateSlowestFiveJobs = (jobs) => {
const jobNodes = [...jobs.nodes];
const limit = 5;
return jobNodes
.sort((a, b) => {
return b.duration - a.duration;
})
.slice(0, limit);
};

View File

@ -771,6 +771,7 @@ $tabs-holder-z-index: 250;
&.show .dropdown-menu {
width: calc(100vw - 20px);
max-width: 650px;
max-height: calc(100vh - 50px);
.gl-new-dropdown-inner {
max-height: none !important;
@ -812,8 +813,7 @@ $tabs-holder-z-index: 250;
}
.md-preview-holder {
max-height: 180px;
height: 180px;
max-height: 172px;
}
}

View File

@ -164,7 +164,7 @@ $system-note-svg-size: 16px;
}
.note-body {
padding: $gl-padding-4;
padding: $gl-padding-4 $gl-padding-4 $gl-padding-4 $gl-padding-8;
overflow-x: auto;
overflow-y: hidden;
@ -305,7 +305,7 @@ $system-note-svg-size: 16px;
height: $system-note-icon-size;
border: 1px solid $gray-10;
border-radius: $system-note-icon-size;
margin: -6px 20px 0 0;
margin: -6px 0 0;
svg {
width: $system-note-svg-size;
@ -551,6 +551,7 @@ $system-note-svg-size: 16px;
.note-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
> .note-header-info,
@ -581,7 +582,7 @@ $system-note-svg-size: 16px;
.note-header-info {
min-width: 0;
padding-left: $gl-padding-4;
padding-left: $gl-padding-8;
&.discussion {
padding-bottom: 0;

View File

@ -105,7 +105,7 @@ class Admin::UsersController < Admin::ApplicationController
return redirect_back_or_admin_user(notice: _("Error occurred. A blocked user cannot be deactivated")) if user.blocked?
return redirect_back_or_admin_user(notice: _("Successfully deactivated")) if user.deactivated?
return redirect_back_or_admin_user(notice: _("Internal users cannot be deactivated")) if user.internal?
return redirect_back_or_admin_user(notice: _("The user you are trying to deactivate has been active in the past %{minimum_inactive_days} days and cannot be deactivated") % { minimum_inactive_days: ::User::MINIMUM_INACTIVE_DAYS }) unless user.can_be_deactivated?
return redirect_back_or_admin_user(notice: _("The user you are trying to deactivate has been active in the past %{minimum_inactive_days} days and cannot be deactivated") % { minimum_inactive_days: Gitlab::CurrentSettings.deactivate_dormant_users_period }) unless user.can_be_deactivated?
user.deactivate
redirect_back_or_admin_user(notice: _("Successfully deactivated"))

View File

@ -49,8 +49,16 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
def publish
result = DraftNotes::PublishService.new(merge_request, current_user).execute(draft_note(allow_nil: true))
if Feature.enabled?(:mr_review_submit_comment, @project) && create_note_params[:note]
Notes::CreateService.new(@project, current_user, create_note_params).execute
if Feature.enabled?(:mr_review_submit_comment, @project)
::Notes::CreateService.new(@project, current_user, create_note_params).execute if create_note_params[:note]
if Gitlab::Utils.to_boolean(approve_params[:approve])
success = ::MergeRequests::ApprovalService.new(project: @project, current_user: current_user, params: approve_params).execute(merge_request)
unless success
return render json: { message: _('An error occurred while approving, please try again.') }, status: :internal_server_error
end
end
end
if result[:status] == :success
@ -115,6 +123,10 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
end
end
def approve_params
params.permit(:approve)
end
def prepare_notes_for_rendering(notes)
return [] unless notes
@ -148,3 +160,5 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
access_denied! unless can?(current_user, :create_note, merge_request)
end
end
Projects::MergeRequests::DraftsController.prepend_mod

View File

@ -211,6 +211,7 @@ class DeploymentsFinder
environment: [],
deployable: {
job_artifacts: [],
user: [],
pipeline: {
project: {
route: [],

View File

@ -92,6 +92,8 @@ module Types
description: 'Indicates the job is stuck.'
field :triggered, GraphQL::Types::Boolean, null: true,
description: 'Whether the job was triggered.'
field :web_path, GraphQL::Types::String, null: true,
description: 'Web path of the job.'
def kind
return ::Ci::Build unless [::Ci::Build, ::Ci::Bridge].include?(object.class)
@ -181,6 +183,10 @@ module Types
::Gitlab::Routing.url_helpers.project_commits_path(object.project, ref_name)
end
def web_path
::Gitlab::Routing.url_helpers.project_job_path(object.project, object)
end
def coverage
object&.coverage
end

View File

@ -50,5 +50,20 @@ module Types
field :status,
Types::DeploymentStatusEnum,
description: 'Status of the deployment.'
field :commit,
Types::CommitType,
description: 'Commit details of the deployment.',
calls_gitaly: true
field :job,
Types::Ci::JobType,
description: 'Pipeline job of the deployment.',
method: :build
field :triggerer,
Types::UserType,
description: 'User who executed the deployment.',
method: :deployed_by
end
end

View File

@ -437,7 +437,10 @@ module ApplicationSettingsHelper
:pipeline_limit_per_project_user_sha,
:invitation_flow_enforcement
].tap do |settings|
settings << :deactivate_dormant_users unless Gitlab.com?
next if Gitlab.com?
settings << :deactivate_dormant_users
settings << :deactivate_dormant_users_period
end
end

View File

@ -6,6 +6,8 @@ module Ci
include Limitable
include IgnorableColumns
TRIGGER_TOKEN_PREFIX = 'glptt-'
ignore_column :ref, remove_with: '15.4', remove_after: '2022-08-22'
self.limit_name = 'pipeline_triggers'
@ -22,7 +24,7 @@ module Ci
before_validation :set_default_values
def set_default_values
self.token = SecureRandom.hex(15) if self.token.blank?
self.token = "#{TRIGGER_TOKEN_PREFIX}#{SecureRandom.hex(20)}" if self.token.blank?
end
def last_trigger_request
@ -34,7 +36,7 @@ module Ci
end
def short_token
token[0...4] if token.present?
token.delete_prefix(TRIGGER_TOKEN_PREFIX)[0...4] if token.present?
end
def can_access_project?

View File

@ -27,12 +27,11 @@ module Approvable
scope :not_approved_by_users_with_usernames, -> (usernames) do
users = User.where(username: usernames).select(:id)
self_table = self.arel_table
app_table = Approval.arel_table
where(
Approval.where(approvals: { user_id: users })
.where(app_table[:merge_request_id].eq(self_table[:id]))
.where(app_table[:merge_request_id].eq(arel_table[:id]))
.select('true')
.arel.exists.not
)

View File

@ -222,6 +222,10 @@ class Deployment < ApplicationRecord
Ci::Build.where(id: deployable_ids)
end
def build
deployable if deployable.is_a?(::Ci::Build)
end
class << self
##
# FastDestroyAll concerns

View File

@ -3,8 +3,9 @@
class ResourceStateEvent < ResourceEvent
include IssueResourceEvent
include MergeRequestResourceEvent
include Importable
validate :exactly_one_issuable
validate :exactly_one_issuable, unless: :importing?
belongs_to :source_merge_request, class_name: 'MergeRequest', foreign_key: :source_merge_request_id

View File

@ -5,8 +5,9 @@ class ResourceTimeboxEvent < ResourceEvent
include IssueResourceEvent
include MergeRequestResourceEvent
include Importable
validate :exactly_one_issuable
validate :exactly_one_issuable, unless: :importing?
enum action: {
add: 1,

View File

@ -92,7 +92,6 @@ class User < ApplicationRecord
include ForcedEmailConfirmation
include RequireEmailVerification
MINIMUM_INACTIVE_DAYS = 90
MINIMUM_DAYS_CREATED = 7
# Override Devise::Models::Trackable#update_tracked_fields!
@ -488,7 +487,7 @@ class User < ApplicationRecord
scope :order_oldest_sign_in, -> { reorder(arel_table[:current_sign_in_at].asc.nulls_last) }
scope :order_recent_last_activity, -> { reorder(arel_table[:last_activity_on].desc.nulls_last, arel_table[:id].asc) }
scope :order_oldest_last_activity, -> { reorder(arel_table[:last_activity_on].asc.nulls_first, arel_table[:id].desc) }
scope :dormant, -> { with_state(:active).human_or_service_user.where('last_activity_on <= ?', MINIMUM_INACTIVE_DAYS.day.ago.to_date) }
scope :dormant, -> { with_state(:active).human_or_service_user.where('last_activity_on <= ?', Gitlab::CurrentSettings.deactivate_dormant_users_period.day.ago.to_date) }
scope :with_no_activity, -> { with_state(:active).human_or_service_user.where(last_activity_on: nil).where('created_at <= ?', MINIMUM_DAYS_CREATED.day.ago.to_date) }
scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) }
scope :by_ids_or_usernames, -> (ids, usernames) { where(username: usernames).or(where(id: ids)) }
@ -2322,7 +2321,7 @@ class User < ApplicationRecord
end
def no_recent_activity?
last_active_at.to_i <= MINIMUM_INACTIVE_DAYS.days.ago.to_i
last_active_at.to_i <= Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_i
end
def update_highest_role?

View File

@ -40,6 +40,10 @@ class MergeRequestNoteableEntity < IssuableEntity
expose :can_update do |merge_request|
can?(current_user, :update_merge_request, merge_request)
end
expose :can_approve do |merge_request|
merge_request.can_be_approved_by?(current_user)
end
end
expose :locked_discussion_docs_path, if: -> (merge_request) { merge_request.discussion_locked? } do |merge_request|
@ -65,3 +69,5 @@ class MergeRequestNoteableEntity < IssuableEntity
@presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user) # rubocop: disable CodeReuse/Presenter
end
end
MergeRequestNoteableEntity.prepend_mod_with('MergeRequestNoteableEntity')

View File

@ -52,7 +52,13 @@
= f.label :deactivate_dormant_users, _('Dormant users'), class: 'label-bold'
- dormant_users_help_link = help_page_path('user/admin_area/moderate_users', anchor: 'automatically-deactivate-dormant-users')
- dormant_users_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: dormant_users_help_link }
= f.gitlab_ui_checkbox_component :deactivate_dormant_users, _('Deactivate dormant users after 90 days of inactivity'), help_text: _('Users can reactivate their account by signing in. %{link_start}Learn more%{link_end}').html_safe % { link_start: dormant_users_help_link_start, link_end: '</a>'.html_safe }
= f.gitlab_ui_checkbox_component :deactivate_dormant_users, _('Deactivate dormant users after a period of inactivity'), help_text: _('Users can reactivate their account by signing in. %{link_start}Learn more%{link_end}').html_safe % { link_start: dormant_users_help_link_start, link_end: '</a>'.html_safe }
.form-group
= f.label :deactivate_dormant_users_period, _('Period of inactivity (days)'), class: 'label-light'
= f.number_field :deactivate_dormant_users_period, class: 'form-control gl-form-input', min: '1'
.form-text.text-muted
= _('Period of inactivity before deactivation')
.form-group
= f.label :personal_access_token_prefix, _('Personal Access Token prefix'), class: 'label-light'
= f.text_field :personal_access_token_prefix, placeholder: _('Maximum 20 characters'), class: 'form-control gl-form-input'

View File

@ -13,7 +13,7 @@
.settings-content
= render 'visibility_and_access'
%section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'account_and_limit_settings_content' } }
%section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'account_and_limit_settings_content', testid: 'account-limit' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Account and limit')

View File

@ -0,0 +1,8 @@
---
name: escape_gitaly_refs
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91058
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366437
milestone: '15.2'
type: development
group: group::source code
default_enabled: false

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddIssuesAuthorizationIndex < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
INDEX_NAME = 'idx_open_issues_on_project_and_confidential_and_author_and_id'
def up
prepare_async_index :issues, [:project_id, :confidential, :author_id, :id], name: INDEX_NAME, where: 'state_id = 1'
end
def down
unprepare_async_index :issues, INDEX_NAME
end
end

View File

@ -0,0 +1 @@
035e918bcb674fdf1300a5bccbad87806311e6de8589f2db57d7af9cd0108ee9

View File

@ -38,9 +38,11 @@ fail. This means the user cannot sign in or push or pull code.
The process also updates the following user information:
- Email address
- SSH public keys (if `sync_ssh_keys` is set)
- Kerberos identity (if Kerberos is enabled)
- Name. Because of a [sync issue](https://gitlab.com/gitlab-org/gitlab/-/issues/342598), `name` is not synchronized if
[**Prevent users from changing their profile name**](../../../user/admin_area/settings/account_and_limit_settings.md#disable-user-profile-name-changes) is enabled.
- Email address.
- SSH public keys if `sync_ssh_keys` is set.
- Kerberos identity if Kerberos is enabled.
### Adjust LDAP user sync schedule

View File

@ -191,6 +191,12 @@ to use the HTTPS protocol.
WARNING:
Multiple wildcards for one instance is not supported. Only one wildcard per instance can be assigned.
WARNING:
GitLab Pages does not update the OAuth application if changes are made to the redirect URI.
Before you reconfigure, remove the `gitlab_pages` section from `/etc/gitlab/gitlab-secrets.json`,
then run `gitlab-ctl reconfigure`. For more information, read
[GitLab Pages does not regenerate OAuth](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/3947).
### Wildcard domains with TLS-terminating Load Balancer
**Requirements:**

View File

@ -10199,6 +10199,7 @@ CI/CD variables for a GitLab instance.
| <a id="cijobtags"></a>`tags` | [`[String!]`](#string) | Tags for the current job. |
| <a id="cijobtriggered"></a>`triggered` | [`Boolean`](#boolean) | Whether the job was triggered. |
| <a id="cijobuserpermissions"></a>`userPermissions` | [`JobPermissions!`](#jobpermissions) | Permissions for the current user on the resource. |
| <a id="cijobwebpath"></a>`webPath` | [`String`](#string) | Web path of the job. |
### `CiJobArtifact`
@ -11019,14 +11020,17 @@ The deployment of an environment.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="deploymentcommit"></a>`commit` | [`Commit`](#commit) | Commit details of the deployment. |
| <a id="deploymentcreatedat"></a>`createdAt` | [`Time`](#time) | When the deployment record was created. |
| <a id="deploymentfinishedat"></a>`finishedAt` | [`Time`](#time) | When the deployment finished. |
| <a id="deploymentid"></a>`id` | [`ID`](#id) | Global ID of the deployment. |
| <a id="deploymentiid"></a>`iid` | [`ID`](#id) | Project-level internal ID of the deployment. |
| <a id="deploymentjob"></a>`job` | [`CiJob`](#cijob) | Pipeline job of the deployment. |
| <a id="deploymentref"></a>`ref` | [`String`](#string) | Git-Ref that the deployment ran on. |
| <a id="deploymentsha"></a>`sha` | [`String`](#string) | Git-SHA that the deployment ran on. |
| <a id="deploymentstatus"></a>`status` | [`DeploymentStatus`](#deploymentstatus) | Status of the deployment. |
| <a id="deploymenttag"></a>`tag` | [`Boolean`](#boolean) | True or false if the deployment ran on a Git-tag. |
| <a id="deploymenttriggerer"></a>`triggerer` | [`UserCore`](#usercore) | User who executed the deployment. |
| <a id="deploymentupdatedat"></a>`updatedAt` | [`Time`](#time) | When the deployment record was updated. |
### `DeploymentDetails`
@ -11037,14 +11041,17 @@ The details of the deployment.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="deploymentdetailscommit"></a>`commit` | [`Commit`](#commit) | Commit details of the deployment. |
| <a id="deploymentdetailscreatedat"></a>`createdAt` | [`Time`](#time) | When the deployment record was created. |
| <a id="deploymentdetailsfinishedat"></a>`finishedAt` | [`Time`](#time) | When the deployment finished. |
| <a id="deploymentdetailsid"></a>`id` | [`ID`](#id) | Global ID of the deployment. |
| <a id="deploymentdetailsiid"></a>`iid` | [`ID`](#id) | Project-level internal ID of the deployment. |
| <a id="deploymentdetailsjob"></a>`job` | [`CiJob`](#cijob) | Pipeline job of the deployment. |
| <a id="deploymentdetailsref"></a>`ref` | [`String`](#string) | Git-Ref that the deployment ran on. |
| <a id="deploymentdetailssha"></a>`sha` | [`String`](#string) | Git-SHA that the deployment ran on. |
| <a id="deploymentdetailsstatus"></a>`status` | [`DeploymentStatus`](#deploymentstatus) | Status of the deployment. |
| <a id="deploymentdetailstag"></a>`tag` | [`Boolean`](#boolean) | True or false if the deployment ran on a Git-tag. |
| <a id="deploymentdetailstriggerer"></a>`triggerer` | [`UserCore`](#usercore) | User who executed the deployment. |
| <a id="deploymentdetailsupdatedat"></a>`updatedAt` | [`Time`](#time) | When the deployment record was updated. |
### `Design`

View File

@ -9,7 +9,7 @@ description: "How to migrate an existing Git repository to Git LFS with BFG."
WARNING:
The following documentation is deprecated. We recommend using
[`git lfs migrate`](https://github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-migrate.1.ronn)
[`git lfs migrate`](https://github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-migrate.adoc)
instead of the method documented below.
Using Git LFS can help you to reduce the size of your Git

View File

@ -169,12 +169,13 @@ Users can also be deactivated using the [GitLab API](../../api/users.md#deactiva
### Automatically deactivate dormant users
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/320875) in GitLab 14.0.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/320875) in GitLab 14.0.
> - Customizable time period [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336747) in GitLab 15.4
Administrators can enable automatic deactivation of users who either:
- Were created more than a week ago and have not signed in.
- Have no activity in the last 90 days.
- Have no activity for a specified period of time (defaults to 90 days).
To do this:
@ -182,6 +183,7 @@ To do this:
1. On the left sidebar, select **Settings > General**.
1. Expand the **Account and limit** section.
1. Under **Dormant users**, check **Deactivate dormant users after 90 days of inactivity**.
1. Under **Period of inactivity (days)**, enter a period of time before deactivation.
1. Select **Save changes**.
When this feature is enabled, GitLab runs a job once a day to deactivate the dormant users.

View File

@ -88,6 +88,7 @@ migrated:
- Board Lists
- Boards
- Epics ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/250281) in 13.7)
- Epic resource state events ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291983) in GitLab 15.4)
- Finisher
- Group Labels ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292429) in 13.9)
- Iterations ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292428) in 13.10)
@ -116,6 +117,8 @@ On self-managed GitLab, migrating project resources are not available by default
- CI Pipelines ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/339407) in GitLab 14.6)
- Designs ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/339421) in GitLab 15.1)
- Issues ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/267946) in GitLab 14.4)
- Issue resource state events ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291983) in GitLab 15.4)
- Issue resource milestone events ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291983) in GitLab 15.4)
- Labels ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/339419) in GitLab 14.4)
- LFS Objects ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/339405) in GitLab 14.8)
- Members ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/341886) in GitLab 14.8)
@ -123,6 +126,8 @@ On self-managed GitLab, migrating project resources are not available by default
- Multiple merge request assignees ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/339520) in GitLab 15.3)
- Merge request reviewers ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/339520) in GitLab 15.3)
- Merge request approvers ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/339520) in GitLab 15.3)
- Merge request resource state events ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291983) in GitLab 15.4)
- Merge request resource milestone events ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291983) in GitLab 15.4)
- Migrate Push Rules ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/339403) in GitLab 14.6)
- Pull Requests (including external pull requests) ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/339409) in GitLab 14.5)
- Pipeline History ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/339412) in GitLab 14.6)

View File

@ -55,6 +55,7 @@ The following items are exported:
- Badges
- Subgroups (including all the aforementioned data)
- Epics
- Epic resource state events ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291983) in GitLab 15.4)
- Events
- [Wikis](../../project/wiki/group.md)
([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53247) in GitLab 13.9)

View File

@ -59,7 +59,8 @@ To compare branches in a repository:
![Delete merged branches](img/delete_merged_branches.png)
This feature allows merged branches to be deleted in bulk. Only branches that
have been merged and [are not protected](../../protected_branches.md) are deleted as part of
have been merged into the project's default branch and
[are not protected](../../protected_branches.md) are deleted as part of
this operation.
It's particularly useful to clean up old branches that were not deleted

View File

@ -62,8 +62,18 @@ The following items are exported:
- Project and wiki repositories
- Project uploads
- Project configuration, excluding integrations
- Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, time
tracking, and other project entities
- Issues
- Issue comments
- Issue resource state events ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291983) in GitLab 15.4)
- Issue resource milestone events ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291983) in GitLab 15.4)
- Merge requests
- Merge request diffs
- Merge request comments
- Merge request resource state events ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291983) in GitLab 15.4)
- Labels
- Milestones
- Snippets
- Time tracking and other project entities
- Design Management files and data
- LFS objects
- Issue boards

View File

@ -733,7 +733,7 @@ module API
unless user.can_be_deactivated?
forbidden!('A blocked user cannot be deactivated by the API') if user.blocked?
forbidden!('An internal user cannot be deactivated by the API') if user.internal?
forbidden!("The user you are trying to deactivate has been active in the past #{::User::MINIMUM_INACTIVE_DAYS} days and cannot be deactivated")
forbidden!("The user you are trying to deactivate has been active in the past #{Gitlab::CurrentSettings.deactivate_dormant_users_period} days and cannot be deactivated")
end
if user.deactivate

View File

@ -71,6 +71,21 @@ module Gitlab
encode_utf8(data, replace: UNICODE_REPLACEMENT_CHARACTER)
end
# This method escapes unsupported UTF-8 characters instead of deleting them
def encode_utf8_with_escaping!(message)
return encode!(message) if Feature.disabled?(:escape_gitaly_refs)
message = force_encode_utf8(message)
return message if message.valid_encoding?
unless message.valid_encoding?
message = message.chars.map { |char| char.valid_encoding? ? char : escape_chars(char) }.join
end
# encode and clean the bad chars
message.replace clean(message)
end
def encode_utf8(message, replace: "")
message = force_encode_utf8(message)
return message if message.valid_encoding?
@ -145,6 +160,15 @@ module Gitlab
message.force_encoding("UTF-8")
end
# Escapes \x80 - \xFF characters not supported by UTF-8
def escape_chars(char)
bytes = char.bytes
return char unless bytes.one?
"%#{bytes.first.to_s(16).upcase}"
end
def clean(message, replace: "")
message.encode(
"UTF-16BE",

View File

@ -25,7 +25,7 @@ module Gitlab
include Gitlab::EncodingHelper
def ref_name(ref)
encode!(ref).sub(%r{\Arefs/(tags|heads|remotes)/}, '')
encode_utf8_with_escaping!(ref).sub(%r{\Arefs/(tags|heads|remotes)/}, '')
end
def branch_name(ref)

View File

@ -1011,16 +1011,20 @@ module Gitlab
end
def search_files_by_name(query, ref)
safe_query = Regexp.escape(query.sub(%r{^/*}, ""))
safe_query = query.sub(%r{^/*}, "")
ref ||= root_ref
return [] if empty? || safe_query.blank?
gitaly_repository_client.search_files_by_name(ref, safe_query)
gitaly_repository_client.search_files_by_name(ref, safe_query).map do |file|
Gitlab::EncodingHelper.encode_utf8(file)
end
end
def search_files_by_regexp(filter, ref = 'HEAD')
gitaly_repository_client.search_files_by_regexp(ref, filter)
gitaly_repository_client.search_files_by_regexp(ref, filter).map do |file|
Gitlab::EncodingHelper.encode_utf8(file)
end
end
def find_commits_by_message(query, ref, path, limit, offset)

View File

@ -63,7 +63,7 @@ module Gitlab
end
def init_from_gitaly
@name = encode!(@raw_tag.name.dup)
@name = encode_utf8_with_escaping!(@raw_tag.name.dup)
@target = @raw_tag.id
@message = message_from_gitaly_tag

View File

@ -104,7 +104,7 @@ module Gitlab
return unless branch
target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit)
Gitlab::Git::Branch.new(@repository, encode!(branch.name.dup), branch.target_commit.id, target_commit)
Gitlab::Git::Branch.new(@repository, branch.name.dup, branch.target_commit.id, target_commit)
end
def find_tag(tag_name)
@ -273,7 +273,7 @@ module Gitlab
message.branches.map do |gitaly_branch|
Gitlab::Git::Branch.new(
@repository,
encode!(gitaly_branch.name.dup),
gitaly_branch.name.dup,
gitaly_branch.commit_id,
commit_from_local_branches_response(gitaly_branch)
)

View File

@ -120,6 +120,7 @@ ee:
- events:
- :push_event_payload
- :system_note_metadata
- :resource_state_events
- boards:
- :board_assignee
- :milestone

View File

@ -114,6 +114,7 @@ ee:
- :award_emoji
- events:
- :push_event_payload
- :resource_state_events
- boards:
- :board_assignee
- :milestone

View File

@ -29,6 +29,9 @@ tree:
- resource_label_events:
- label:
- :priorities
- resource_milestone_events:
- :milestone
- :resource_state_events
- designs:
- notes:
- :author
@ -82,6 +85,9 @@ tree:
- resource_label_events:
- label:
- :priorities
- resource_milestone_events:
- :milestone
- :resource_state_events
- :external_pull_requests
- ci_pipelines:
- notes:
@ -721,6 +727,22 @@ included_attributes:
- :build_git_strategy
- :build_enabled
- :security_and_compliance_enabled
resource_iteration_events:
- :user_id
- :action
- :created_at
resource_milestone_events:
- :user_id
- :action
- :created_at
- :state
resource_state_events:
- :user_id
- :state
- :created_at
- :source_commit
- :close_after_error_tracking_resolve
- :close_auto_resolve_prometheus_alert
# Do not include the following attributes for the models specified.
excluded_attributes:
@ -989,6 +1011,17 @@ excluded_attributes:
milestone_releases:
- :milestone_id
- :release_id
resource_milestone_events:
- :id
- :issue_id
- :merge_request_id
- :milestone_id
resource_state_events:
- :id
- :issue_id
- :merge_request_id
- :epic_id
- :source_merge_request_id
methods:
notes:
- :type

View File

@ -4035,6 +4035,9 @@ msgstr ""
msgid "An error occurred while adding formatted title for epic"
msgstr ""
msgid "An error occurred while approving, please try again."
msgstr ""
msgid "An error occurred while authorizing your role"
msgstr ""
@ -4873,6 +4876,9 @@ msgstr ""
msgid "Approve a merge request"
msgstr ""
msgid "Approve merge request"
msgstr ""
msgid "Approve the current merge request."
msgstr ""
@ -12297,7 +12303,7 @@ msgstr ""
msgid "Days to merge"
msgstr ""
msgid "Deactivate dormant users after 90 days of inactivity"
msgid "Deactivate dormant users after a period of inactivity"
msgstr ""
msgid "Dear Administrator,"
@ -16409,9 +16415,6 @@ msgstr ""
msgid "February"
msgstr ""
msgid "Feedback issue"
msgstr ""
msgid "Fetch and check out this merge request's feature branch:"
msgstr ""
@ -28402,9 +28405,6 @@ msgstr ""
msgid "Perform common operations on GitLab project"
msgstr ""
msgid "Performance insights"
msgstr ""
msgid "Performance optimization"
msgstr ""
@ -28480,6 +28480,12 @@ msgstr ""
msgid "Period in seconds"
msgstr ""
msgid "Period of inactivity (days)"
msgstr ""
msgid "Period of inactivity before deactivation"
msgstr ""
msgid "Permalink"
msgstr ""
@ -29191,18 +29197,9 @@ msgstr ""
msgid "Pipeline|Failed"
msgstr ""
msgid "Pipeline|Five slowest jobs"
msgstr ""
msgid "Pipeline|In progress"
msgstr ""
msgid "Pipeline|Last executed job"
msgstr ""
msgid "Pipeline|Longest queued job"
msgstr ""
msgid "Pipeline|Manual"
msgstr ""
@ -29215,18 +29212,12 @@ msgstr ""
msgid "Pipeline|Merged result pipeline"
msgstr ""
msgid "Pipeline|Only able to show first 100 results"
msgstr ""
msgid "Pipeline|Passed"
msgstr ""
msgid "Pipeline|Pending"
msgstr ""
msgid "Pipeline|Performance insights"
msgstr ""
msgid "Pipeline|Pipeline"
msgstr ""
@ -29284,12 +29275,6 @@ msgstr ""
msgid "Pipeline|Test coverage"
msgstr ""
msgid "Pipeline|The last executed job is the last job to start in the pipeline."
msgstr ""
msgid "Pipeline|The longest queued job is the job that spent the longest time in the pending state, waiting to be picked up by a Runner"
msgstr ""
msgid "Pipeline|This change will decrease the overall test coverage if merged."
msgstr ""
@ -29320,9 +29305,6 @@ msgstr ""
msgid "Pipeline|View commit"
msgstr ""
msgid "Pipeline|View dependency"
msgstr ""
msgid "Pipeline|View pipeline"
msgstr ""
@ -40856,6 +40838,9 @@ msgstr ""
msgid "To add the entry manually, provide the following details to the application on your phone."
msgstr ""
msgid "To approve this merge request, please enter your password. This project requires all approvals to be authenticated."
msgstr ""
msgid "To complete registration, we need additional details from you."
msgstr ""
@ -45572,6 +45557,9 @@ msgstr ""
msgid "Your new comment"
msgstr ""
msgid "Your password"
msgstr ""
msgid "Your password reset token has expired."
msgstr ""

View File

@ -270,19 +270,19 @@ RSpec.describe Admin::UsersController do
let(:user) { create(:user, **activity) }
context 'with no recent activity' do
let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.next.days.ago } }
let(:activity) { { last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.next.days.ago } }
it_behaves_like 'a request that deactivates the user'
end
context 'with recent activity' do
let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.pred.days.ago } }
let(:activity) { { last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.pred.days.ago } }
it 'does not deactivate the user' do
put :deactivate, params: { id: user.username }
user.reload
expect(user.deactivated?).to be_falsey
expect(flash[:notice]).to eq("The user you are trying to deactivate has been active in the past #{::User::MINIMUM_INACTIVE_DAYS} days and cannot be deactivated")
expect(flash[:notice]).to eq("The user you are trying to deactivate has been active in the past #{Gitlab::CurrentSettings.deactivate_dormant_users_period} days and cannot be deactivated")
end
end
end

View File

@ -5,7 +5,7 @@ RSpec.describe Projects::MergeRequests::DraftsController do
include RepoHelpers
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project, author: create(:user)) }
let(:user) { project.first_owner }
let(:user2) { create(:user) }
@ -417,6 +417,38 @@ RSpec.describe Projects::MergeRequests::DraftsController do
end
end
end
context 'approve merge request' do
before do
create(:draft_note, merge_request: merge_request, author: user)
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(mr_review_submit_comment: false)
end
it 'does not approve' do
post :publish, params: params.merge!(approve: true)
expect(merge_request.approvals.reload.size).to be(0)
end
end
context 'when feature flag is enabled' do
it 'approves merge request' do
post :publish, params: params.merge!(approve: true)
expect(merge_request.approvals.reload.size).to be(1)
end
it 'does not approve merge request' do
post :publish, params: params.merge!(approve: false)
expect(merge_request.approvals.reload.size).to be(0)
end
end
end
end
describe 'DELETE #destroy' do

View File

@ -102,7 +102,7 @@ RSpec.describe 'Admin updates settings' do
end
it 'change Account and Limit Settings' do
page.within('.as-account-limit') do
page.within(find('[data-testid="account-limit"]')) do
uncheck 'Gravatar enabled'
click_button 'Save changes'
end
@ -112,7 +112,7 @@ RSpec.describe 'Admin updates settings' do
end
it 'change Maximum export size' do
page.within('.as-account-limit') do
page.within(find('[data-testid="account-limit"]')) do
fill_in 'Maximum export size (MB)', with: 25
click_button 'Save changes'
end
@ -122,7 +122,7 @@ RSpec.describe 'Admin updates settings' do
end
it 'change Maximum import size' do
page.within('.as-account-limit') do
page.within(find('[data-testid="account-limit"]')) do
fill_in 'Maximum import size (MB)', with: 15
click_button 'Save changes'
end
@ -150,16 +150,20 @@ RSpec.describe 'Admin updates settings' do
it 'does not expose the setting' do
expect(page).to have_no_selector('#application_setting_deactivate_dormant_users')
end
it 'does not expose the setting' do
expect(page).to have_no_selector('#application_setting_deactivate_dormant_users_period')
end
end
context 'when not Gitlab.com' do
let(:dot_com?) { false }
it 'change Dormant users' do
expect(page).to have_unchecked_field('Deactivate dormant users after 90 days of inactivity')
it 'changes Dormant users' do
expect(page).to have_unchecked_field('Deactivate dormant users after a period of inactivity')
expect(current_settings.deactivate_dormant_users).to be_falsey
page.within('.as-account-limit') do
page.within(find('[data-testid="account-limit"]')) do
check 'application_setting_deactivate_dormant_users'
click_button 'Save changes'
end
@ -169,7 +173,22 @@ RSpec.describe 'Admin updates settings' do
page.refresh
expect(current_settings.deactivate_dormant_users).to be_truthy
expect(page).to have_checked_field('Deactivate dormant users after 90 days of inactivity')
expect(page).to have_checked_field('Deactivate dormant users after a period of inactivity')
end
it 'change Dormant users period' do
expect(page).to have_field _('Period of inactivity (days)')
page.within(find('[data-testid="account-limit"]')) do
fill_in _('application_setting_deactivate_dormant_users_period'), with: '35'
click_button 'Save changes'
end
expect(page).to have_content "Application settings saved successfully"
page.refresh
expect(page).to have_field _('Period of inactivity (days)'), with: '35'
end
end
end

View File

@ -22,11 +22,13 @@
"type": "object",
"required": [
"can_create_note",
"can_update"
"can_update",
"can_approve"
],
"properties": {
"can_create_note": { "type": "boolean" },
"can_update": { "type": "boolean" }
"can_update": { "type": "boolean" },
"can_approve": { "type": "boolean" }
},
"additionalProperties": false
},

View File

@ -394,7 +394,41 @@
"id": 1,
"issue_id": 40,
"sentry_issue_identifier": 1234567891
}
},
"resource_milestone_events": [
{
"user_id": 1,
"action": "add",
"state": "opened",
"created_at": "2022-08-17T13:06:53.547Z",
"milestone": {
"title": "v4.0",
"description": "Totam quam laborum id magnam natus eaque aspernatur.",
"created_at": "2016-06-14T15:02:04.590Z",
"updated_at": "2016-06-14T15:02:04.590Z",
"state": "active",
"iid": 5
}
}
],
"resource_state_events": [
{
"user_id": 1,
"created_at": "2022-08-17T13:08:16.838Z",
"state": "closed",
"source_commit": null,
"close_after_error_tracking_resolve": false,
"close_auto_resolve_prometheus_alert": false
},
{
"user_id": 1,
"created_at": "2022-08-17T13:08:17.702Z",
"state": "reopened",
"source_commit": null,
"close_after_error_tracking_resolve": false,
"close_auto_resolve_prometheus_alert": false
}
]
},
{
"id": 39,
@ -3215,6 +3249,40 @@
"created_at": "2020-01-07T11:21:21.235Z",
"updated_at": "2020-01-08T11:21:21.235Z"
}
],
"resource_milestone_events": [
{
"user_id": 1,
"action": "add",
"state": "opened",
"created_at": "2022-08-17T13:06:53.547Z",
"milestone": {
"title": "v4.0",
"description": "Totam quam laborum id magnam natus eaque aspernatur.",
"created_at": "2016-06-14T15:02:04.590Z",
"updated_at": "2016-06-14T15:02:04.590Z",
"state": "active",
"iid": 5
}
}
],
"resource_state_events": [
{
"user_id": 1,
"created_at": "2022-08-17T13:08:16.838Z",
"state": "closed",
"source_commit": null,
"close_after_error_tracking_resolve": false,
"close_auto_resolve_prometheus_alert": false
},
{
"user_id": 1,
"created_at": "2022-08-17T13:08:17.702Z",
"state": "reopened",
"source_commit": null,
"close_after_error_tracking_resolve": false,
"close_auto_resolve_prometheus_alert": false
}
]
},
{

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@ Vue.use(Vuex);
let wrapper;
let publishReview;
function factory() {
function factory({ canApprove = true } = {}) {
publishReview = jest.fn();
const store = new Vuex.Store({
@ -17,7 +17,11 @@ function factory() {
markdownDocsPath: '/markdown/docs',
quickActionsDocsPath: '/quickactions/docs',
}),
getNoteableData: () => ({ id: 1, preview_note_path: '/preview' }),
getNoteableData: () => ({
id: 1,
preview_note_path: '/preview',
current_user: { can_approve: canApprove },
}),
noteableType: () => 'merge_request',
},
modules: {
@ -54,6 +58,8 @@ describe('Batch comments submit dropdown', () => {
noteable_type: 'merge_request',
noteable_id: 1,
note: 'Hello world',
approve: false,
approval_password: '',
});
});
@ -66,4 +72,14 @@ describe('Batch comments submit dropdown', () => {
expect(findSubmitButton().props('loading')).toBe(true);
});
it.each`
canApprove | exists | existsText
${true} | ${true} | ${'shows'}
${false} | ${false} | ${'hides'}
`('it $existsText approve checkbox if can_approve is $canApprove', ({ canApprove, exists }) => {
factory({ canApprove });
expect(wrapper.findByTestId('approve_merge_request').exists()).toBe(exists);
});
});

View File

@ -180,6 +180,7 @@ describe('Batch comments store actions', () => {
});
it('calls service with notes data', () => {
mock.onAny().reply(200);
jest.spyOn(axios, 'post');
return actions
@ -192,7 +193,7 @@ describe('Batch comments store actions', () => {
it('dispatches error commits', () => {
mock.onAny().reply(500);
return actions.publishReview({ dispatch, commit, getters, rootGetters }).then(() => {
return actions.publishReview({ dispatch, commit, getters, rootGetters }).catch(() => {
expect(commit.mock.calls[0]).toEqual(['REQUEST_PUBLISH_REVIEW']);
expect(commit.mock.calls[1]).toEqual(['RECEIVE_PUBLISH_REVIEW_ERROR']);
});

View File

@ -30,16 +30,10 @@ import * as Api from '~/pipelines/components/graph_shared/api';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils';
import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql';
import getPerformanceInsights from '~/pipelines/graphql/queries/get_performance_insights.query.graphql';
import * as sentryUtils from '~/pipelines/utils';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { mockRunningPipelineHeaderData } from '../mock_data';
import {
mapCallouts,
mockCalloutsResponse,
mockPipelineResponse,
mockPerformanceInsightsResponse,
} from './mock_data';
import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data';
const defaultProvide = {
graphqlResourceEtag: 'frog/amphibirama/etag/',
@ -95,15 +89,11 @@ describe('Pipeline graph wrapper', () => {
const callouts = mapCallouts(calloutsList);
const getUserCalloutsHandler = jest.fn().mockResolvedValue(mockCalloutsResponse(callouts));
const getPipelineHeaderDataHandler = jest.fn().mockResolvedValue(mockRunningPipelineHeaderData);
const getPerformanceInsightsHandler = jest
.fn()
.mockResolvedValue(mockPerformanceInsightsResponse);
const requestHandlers = [
[getPipelineHeaderData, getPipelineHeaderDataHandler],
[getPipelineDetails, getPipelineDetailsHandler],
[getUserCallouts, getUserCalloutsHandler],
[getPerformanceInsights, getPerformanceInsightsHandler],
];
const apolloProvider = createMockApollo(requestHandlers);

View File

@ -1,19 +1,10 @@
import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import getPerformanceInsights from '~/pipelines/graphql/queries/get_performance_insights.query.graphql';
import { mockPerformanceInsightsResponse } from './mock_data';
Vue.use(VueApollo);
describe('the graph view selector component', () => {
let wrapper;
let trackingSpy;
const findDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]');
const findViewTypeSelector = () => wrapper.findComponent(GlButtonGroup);
@ -22,13 +13,11 @@ describe('the graph view selector component', () => {
const findSwitcherLoader = () => wrapper.find('[data-testid="switcher-loading-state"]');
const findToggleLoader = () => findDependenciesToggle().find(GlLoadingIcon);
const findHoverTip = () => wrapper.findComponent(GlAlert);
const findPipelineInsightsBtn = () => wrapper.find('[data-testid="pipeline-insights-btn"]');
const defaultProps = {
showLinks: false,
tipPreviouslyDismissed: false,
type: STAGE_VIEW,
isPipelineComplete: true,
};
const defaultData = {
@ -38,14 +27,6 @@ describe('the graph view selector component', () => {
showLinksActive: false,
};
const getPerformanceInsightsHandler = jest
.fn()
.mockResolvedValue(mockPerformanceInsightsResponse);
const requestHandlers = [[getPerformanceInsights, getPerformanceInsightsHandler]];
const apolloProvider = createMockApollo(requestHandlers);
const createComponent = ({ data = {}, mountFn = shallowMount, props = {} } = {}) => {
wrapper = mountFn(GraphViewSelector, {
propsData: {
@ -58,7 +39,6 @@ describe('the graph view selector component', () => {
...data,
};
},
apolloProvider,
});
};
@ -222,44 +202,5 @@ describe('the graph view selector component', () => {
expect(findHoverTip().exists()).toBe(false);
});
});
describe('pipeline insights', () => {
it.each`
isPipelineComplete | shouldShow
${true} | ${true}
${false} | ${false}
`(
'button should display $shouldShow if isPipelineComplete is $isPipelineComplete ',
({ isPipelineComplete, shouldShow }) => {
createComponent({
props: {
isPipelineComplete,
},
});
expect(findPipelineInsightsBtn().exists()).toBe(shouldShow);
},
);
});
describe('tracking', () => {
beforeEach(() => {
createComponent();
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
afterEach(() => {
unmockTracking();
});
it('tracks performance insights button click', () => {
findPipelineInsightsBtn().vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_insights_button', {
label: 'performance_insights',
});
});
});
});
});

View File

@ -1038,245 +1038,3 @@ export const triggerJob = {
action: null,
},
};
export const mockPerformanceInsightsResponse = {
data: {
project: {
__typename: 'Project',
id: 'gid://gitlab/Project/20',
pipeline: {
__typename: 'Pipeline',
id: 'gid://gitlab/Ci::Pipeline/97',
jobs: {
__typename: 'CiJobConnection',
pageInfo: {
__typename: 'PageInfo',
hasNextPage: false,
},
nodes: [
{
__typename: 'CiJob',
id: 'gid://gitlab/Ci::Bridge/2502',
duration: null,
detailedStatus: {
__typename: 'DetailedStatus',
id: 'success-2502-2502',
detailsPath: '/root/lots-of-jobs-project/-/pipelines/98',
},
name: 'trigger_job',
stage: {
__typename: 'CiStage',
id: 'gid://gitlab/Ci::Stage/303',
name: 'deploy',
},
startedAt: null,
queuedDuration: 424850.376278,
},
{
__typename: 'CiJob',
id: 'gid://gitlab/Ci::Build/2501',
duration: 10,
detailedStatus: {
__typename: 'DetailedStatus',
id: 'success-2501-2501',
detailsPath: '/root/ci-project/-/jobs/2501',
},
name: 'artifact_job',
stage: {
__typename: 'CiStage',
id: 'gid://gitlab/Ci::Stage/303',
name: 'deploy',
},
startedAt: '2022-07-01T16:31:41Z',
queuedDuration: 2.621553,
},
{
__typename: 'CiJob',
id: 'gid://gitlab/Ci::Build/2500',
duration: 4,
detailedStatus: {
__typename: 'DetailedStatus',
id: 'success-2500-2500',
detailsPath: '/root/ci-project/-/jobs/2500',
},
name: 'coverage_job',
stage: {
__typename: 'CiStage',
id: 'gid://gitlab/Ci::Stage/302',
name: 'test',
},
startedAt: '2022-07-01T16:31:33Z',
queuedDuration: 14.388869,
},
{
__typename: 'CiJob',
id: 'gid://gitlab/Ci::Build/2499',
duration: 4,
detailedStatus: {
__typename: 'DetailedStatus',
id: 'success-2499-2499',
detailsPath: '/root/ci-project/-/jobs/2499',
},
name: 'test_job_two',
stage: {
__typename: 'CiStage',
id: 'gid://gitlab/Ci::Stage/302',
name: 'test',
},
startedAt: '2022-07-01T16:31:28Z',
queuedDuration: 15.792664,
},
{
__typename: 'CiJob',
id: 'gid://gitlab/Ci::Build/2498',
duration: 4,
detailedStatus: {
__typename: 'DetailedStatus',
id: 'success-2498-2498',
detailsPath: '/root/ci-project/-/jobs/2498',
},
name: 'test_job_one',
stage: {
__typename: 'CiStage',
id: 'gid://gitlab/Ci::Stage/302',
name: 'test',
},
startedAt: '2022-07-01T16:31:17Z',
queuedDuration: 8.317072,
},
{
__typename: 'CiJob',
id: 'gid://gitlab/Ci::Build/2497',
duration: 5,
detailedStatus: {
__typename: 'DetailedStatus',
id: 'failed-2497-2497',
detailsPath: '/root/ci-project/-/jobs/2497',
},
name: 'allow_failure_test_job',
stage: {
__typename: 'CiStage',
id: 'gid://gitlab/Ci::Stage/302',
name: 'test',
},
startedAt: '2022-07-01T16:31:22Z',
queuedDuration: 3.547553,
},
{
__typename: 'CiJob',
id: 'gid://gitlab/Ci::Build/2496',
duration: null,
detailedStatus: {
__typename: 'DetailedStatus',
id: 'manual-2496-2496',
detailsPath: '/root/ci-project/-/jobs/2496',
},
name: 'test_manual_job',
stage: {
__typename: 'CiStage',
id: 'gid://gitlab/Ci::Stage/302',
name: 'test',
},
startedAt: null,
queuedDuration: null,
},
{
__typename: 'CiJob',
id: 'gid://gitlab/Ci::Build/2495',
duration: 5,
detailedStatus: {
__typename: 'DetailedStatus',
id: 'success-2495-2495',
detailsPath: '/root/ci-project/-/jobs/2495',
},
name: 'large_log_output',
stage: {
__typename: 'CiStage',
id: 'gid://gitlab/Ci::Stage/301',
name: 'build',
},
startedAt: '2022-07-01T16:31:11Z',
queuedDuration: 79.128625,
},
{
__typename: 'CiJob',
id: 'gid://gitlab/Ci::Build/2494',
duration: 5,
detailedStatus: {
__typename: 'DetailedStatus',
id: 'success-2494-2494',
detailsPath: '/root/ci-project/-/jobs/2494',
},
name: 'build_job',
stage: {
__typename: 'CiStage',
id: 'gid://gitlab/Ci::Stage/301',
name: 'build',
},
startedAt: '2022-07-01T16:31:05Z',
queuedDuration: 73.286895,
},
{
__typename: 'CiJob',
id: 'gid://gitlab/Ci::Build/2493',
duration: 16,
detailedStatus: {
__typename: 'DetailedStatus',
id: 'success-2493-2493',
detailsPath: '/root/ci-project/-/jobs/2493',
},
name: 'wait_job',
stage: {
__typename: 'CiStage',
id: 'gid://gitlab/Ci::Stage/301',
name: 'build',
},
startedAt: '2022-07-01T16:30:48Z',
queuedDuration: 56.258856,
},
],
},
},
},
},
};
export const mockPerformanceInsightsNextPageResponse = {
data: {
project: {
__typename: 'Project',
id: 'gid://gitlab/Project/20',
pipeline: {
__typename: 'Pipeline',
id: 'gid://gitlab/Ci::Pipeline/97',
jobs: {
__typename: 'CiJobConnection',
pageInfo: {
__typename: 'PageInfo',
hasNextPage: true,
},
nodes: [
{
__typename: 'CiJob',
id: 'gid://gitlab/Ci::Bridge/2502',
duration: null,
detailedStatus: {
__typename: 'DetailedStatus',
id: 'success-2502-2502',
detailsPath: '/root/lots-of-jobs-project/-/pipelines/98',
},
name: 'trigger_job',
stage: {
__typename: 'CiStage',
id: 'gid://gitlab/Ci::Stage/303',
name: 'deploy',
},
startedAt: null,
queuedDuration: 424850.376278,
},
],
},
},
},
},
};

View File

@ -1,131 +0,0 @@
import { GlAlert, GlLink, GlModal } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import PerformanceInsightsModal from '~/pipelines/components/performance_insights_modal.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import getPerformanceInsights from '~/pipelines/graphql/queries/get_performance_insights.query.graphql';
import {
mockPerformanceInsightsResponse,
mockPerformanceInsightsNextPageResponse,
} from './graph/mock_data';
Vue.use(VueApollo);
describe('Performance insights modal', () => {
let wrapper;
const findModal = () => wrapper.findComponent(GlModal);
const findAlert = () => wrapper.findComponent(GlAlert);
const findLink = () => wrapper.findComponent(GlLink);
const findLimitText = () => wrapper.findByTestId('limit-alert-text');
const findQueuedCardData = () => wrapper.findByTestId('insights-queued-card-data');
const findQueuedCardLink = () => wrapper.findByTestId('insights-queued-card-link');
const findExecutedCardData = () => wrapper.findByTestId('insights-executed-card-data');
const findExecutedCardLink = () => wrapper.findByTestId('insights-executed-card-link');
const findSlowJobsStage = (index) => wrapper.findAllByTestId('insights-slow-job-stage').at(index);
const findSlowJobsLink = (index) => wrapper.findAllByTestId('insights-slow-job-link').at(index);
const getPerformanceInsightsHandler = jest
.fn()
.mockResolvedValue(mockPerformanceInsightsResponse);
const getPerformanceInsightsNextPageHandler = jest
.fn()
.mockResolvedValue(mockPerformanceInsightsNextPageResponse);
const requestHandlers = [[getPerformanceInsights, getPerformanceInsightsHandler]];
const createComponent = (handlers = requestHandlers) => {
wrapper = shallowMountExtended(PerformanceInsightsModal, {
provide: {
pipelineIid: '1',
pipelineProjectPath: 'root/ci-project',
},
apolloProvider: createMockApollo(handlers),
});
};
afterEach(() => {
wrapper.destroy();
});
describe('without next page', () => {
beforeEach(async () => {
createComponent();
await waitForPromises();
});
it('displays modal', () => {
expect(findModal().exists()).toBe(true);
});
it('displays alert', () => {
expect(findAlert().exists()).toBe(true);
});
it('displays feedback issue link', () => {
expect(findLink().text()).toBe('Feedback issue');
expect(findLink().attributes('href')).toBe(
'https://gitlab.com/gitlab-org/gitlab/-/issues/365902',
);
});
it('does not display limit text', () => {
expect(findLimitText().exists()).toBe(false);
});
describe('queued duration card', () => {
it('displays card data', () => {
expect(trimText(findQueuedCardData().text())).toBe('4.9 days');
});
it('displays card link', () => {
expect(findQueuedCardLink().attributes('href')).toBe(
'/root/lots-of-jobs-project/-/pipelines/98',
);
});
});
describe('executed duration card', () => {
it('displays card data', () => {
expect(trimText(findExecutedCardData().text())).toBe('trigger_job');
});
it('displays card link', () => {
expect(findExecutedCardLink().attributes('href')).toBe(
'/root/lots-of-jobs-project/-/pipelines/98',
);
});
});
describe('slow jobs', () => {
it.each`
index | expectedStage | expectedName | expectedLink
${0} | ${'build'} | ${'wait_job'} | ${'/root/ci-project/-/jobs/2493'}
${1} | ${'deploy'} | ${'artifact_job'} | ${'/root/ci-project/-/jobs/2501'}
${2} | ${'test'} | ${'allow_failure_test_job'} | ${'/root/ci-project/-/jobs/2497'}
${3} | ${'build'} | ${'large_log_output'} | ${'/root/ci-project/-/jobs/2495'}
${4} | ${'build'} | ${'build_job'} | ${'/root/ci-project/-/jobs/2494'}
`(
'should display slow job correctly',
({ index, expectedStage, expectedName, expectedLink }) => {
expect(findSlowJobsStage(index).text()).toBe(expectedStage);
expect(findSlowJobsLink(index).text()).toBe(expectedName);
expect(findSlowJobsLink(index).attributes('href')).toBe(expectedLink);
},
);
});
});
describe('with next page', () => {
it('displays limit text when there is a next page', async () => {
createComponent([[getPerformanceInsights, getPerformanceInsightsNextPageHandler]]);
await waitForPromises();
expect(findLimitText().exists()).toBe(true);
});
});
});

View File

@ -8,14 +8,10 @@ import {
removeOrphanNodes,
getMaxNodes,
} from '~/pipelines/components/parsing_utils';
import { createNodeDict, calculateJobStats, calculateSlowestFiveJobs } from '~/pipelines/utils';
import { createNodeDict } from '~/pipelines/utils';
import { mockParsedGraphQLNodes, missingJob } from './components/dag/mock_data';
import {
generateResponse,
mockPipelineResponse,
mockPerformanceInsightsResponse,
} from './graph/mock_data';
import { generateResponse, mockPipelineResponse } from './graph/mock_data';
describe('DAG visualization parsing utilities', () => {
const nodeDict = createNodeDict(mockParsedGraphQLNodes);
@ -162,40 +158,4 @@ describe('DAG visualization parsing utilities', () => {
expect(columns).toMatchSnapshot();
});
});
describe('performance insights', () => {
const {
data: {
project: {
pipeline: { jobs },
},
},
} = mockPerformanceInsightsResponse;
describe('calculateJobStats', () => {
const expectedJob = jobs.nodes[0];
it('returns the job that spent this longest time queued', () => {
expect(calculateJobStats(jobs, 'queuedDuration')).toEqual(expectedJob);
});
it('returns the job that was executed last', () => {
expect(calculateJobStats(jobs, 'startedAt')).toEqual(expectedJob);
});
});
describe('calculateSlowestFiveJobs', () => {
it('returns the slowest five jobs of the pipeline', () => {
const expectedJobs = [
jobs.nodes[9],
jobs.nodes[1],
jobs.nodes[5],
jobs.nodes[7],
jobs.nodes[8],
];
expect(calculateSlowestFiveJobs(jobs)).toEqual(expectedJobs);
});
});
});
});

View File

@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Types::Ci::JobType do
include GraphqlHelpers
specify { expect(described_class.graphql_name).to eq('CiJob') }
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Ci::Job) }
@ -45,8 +47,21 @@ RSpec.describe Types::Ci::JobType do
tags
triggered
userPermissions
webPath
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
describe '#web_path' do
subject { resolve_field(:web_path, build, current_user: user, object_type: described_class) }
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:build) { create(:ci_build, project: project, user: user) }
it 'returns the web path of the job' do
is_expected.to eq("/#{project.full_path}/-/jobs/#{build.id}")
end
end
end

View File

@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['DeploymentDetails'] do
it 'has the expected fields' do
expected_fields = %w[
id iid ref tag sha created_at updated_at finished_at status
id iid ref tag sha created_at updated_at finished_at status commit job triggerer
]
expect(described_class).to have_graphql_fields(*expected_fields)

View File

@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['Deployment'] do
it 'has the expected fields' do
expected_fields = %w[
id iid ref tag sha created_at updated_at finished_at status
id iid ref tag sha created_at updated_at finished_at status commit job triggerer
]
expect(described_class).to have_graphql_fields(*expected_fields)

View File

@ -46,6 +46,10 @@ RSpec.describe ApplicationSettingsHelper do
expect(helper.visible_attributes).to include(:deactivate_dormant_users)
end
it 'contains :deactivate_dormant_users_period' do
expect(helper.visible_attributes).to include(:deactivate_dormant_users_period)
end
it 'contains rate limit parameters' do
expect(helper.visible_attributes).to include(*%i(
issues_create_limit notes_create_limit project_export_limit
@ -63,6 +67,10 @@ RSpec.describe ApplicationSettingsHelper do
it 'does not contain :deactivate_dormant_users' do
expect(helper.visible_attributes).not_to include(:deactivate_dormant_users)
end
it 'does not contain :deactivate_dormant_users_period' do
expect(helper.visible_attributes).not_to include(:deactivate_dormant_users_period)
end
end
end

View File

@ -98,6 +98,36 @@ RSpec.describe Gitlab::EncodingHelper do
end
end
describe '#encode_utf8_with_escaping!' do
where(:input, :expected) do
"abcd" | "abcd"
"DzDzDz" | "DzDzDz"
"\xC7\xB2\xC7DzDzDz" | "Dz%C7DzDzDz"
"🐤🐤🐤🐤\xF0\x9F\x90" | "🐤🐤🐤🐤%F0%9F%90"
"\xD0\x9F\xD1\x80 \x90" | "Пр %90"
"\x41" | "A"
end
with_them do
it 'escapes invalid UTF-8' do
expect(ext_class.encode_utf8_with_escaping!(input.dup.force_encoding(Encoding::ASCII_8BIT))).to eq(expected)
expect(ext_class.encode_utf8_with_escaping!(input)).to eq(expected)
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(escape_gitaly_refs: false)
end
it 'uses #encode! method' do
expect(ext_class).to receive(:encode!).with('String')
ext_class.encode_utf8_with_escaping!('String')
end
end
end
describe '#encode_utf8' do
[
["nil", nil, nil],

View File

@ -673,6 +673,61 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
end
describe '#search_files_by_name' do
let(:ref) { 'master' }
subject(:result) { mutable_repository.search_files_by_name(query, ref) }
context 'when sending a valid name' do
let(:query) { 'files/ruby/popen.rb' }
it 'returns matched files' do
expect(result).to contain_exactly('files/ruby/popen.rb')
end
end
context 'when sending a name with space' do
let(:query) { 'file with space.md' }
before do
mutable_repository.multi_action(
user,
actions: [{ action: :create, file_path: "file with space.md", content: "Test content" }],
branch_name: ref, message: "Test"
)
end
it 'returns matched files' do
expect(result).to contain_exactly('file with space.md')
end
end
context 'when sending a name with special ASCII characters' do
let(:file_name) { 'Hello !@#$%^&*()' }
let(:query) { file_name }
before do
mutable_repository.multi_action(
user,
actions: [{ action: :create, file_path: file_name, content: "Test content" }],
branch_name: ref, message: "Test"
)
end
it 'returns matched files' do
expect(result).to contain_exactly(file_name)
end
end
context 'when sending a non-existing name' do
let(:query) { 'please do not exist.md' }
it 'raises error' do
expect(result).to eql([])
end
end
end
describe '#find_remote_root_ref' do
it 'gets the remote root ref from GitalyClient' do
expect_any_instance_of(Gitlab::GitalyClient::RemoteService)

View File

@ -7,10 +7,18 @@ RSpec.describe Gitlab::Git do
let(:committer_name) { 'John Doe' }
describe '.ref_name' do
it 'ensure ref is a valid UTF-8 string' do
utf8_invalid_ref = Gitlab::Git::BRANCH_REF_PREFIX + "an_invalid_ref_\xE5"
let(:ref) { Gitlab::Git::BRANCH_REF_PREFIX + "an_invalid_ref_\xE5" }
expect(described_class.ref_name(utf8_invalid_ref)).to eq("an_invalid_ref_å")
it 'ensure ref is a valid UTF-8 string' do
expect(described_class.ref_name(ref)).to eq("an_invalid_ref_%E5")
end
context 'when ref contains characters \x80 - \xFF' do
let(:ref) { Gitlab::Git::BRANCH_REF_PREFIX + "\x90" }
it 'correctly converts it' do
expect(described_class.ref_name(ref)).to eq("%90")
end
end
end

View File

@ -819,3 +819,14 @@ service_desk_setting:
approvals:
- user
- merge_request
resource_milestone_events:
- user
- issue
- merge_request
- milestone
resource_state_events:
- user
- issue
- merge_request
- source_merge_request
- epic

View File

@ -192,10 +192,26 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
expect(Issue.find_by(title: 'Voluptatem').resource_label_events).not_to be_empty
end
it 'restores issue resource milestone events' do
expect(Issue.find_by(title: 'Voluptatem').resource_milestone_events).not_to be_empty
end
it 'restores issue resource state events' do
expect(Issue.find_by(title: 'Voluptatem').resource_state_events).not_to be_empty
end
it 'restores merge requests resource label events' do
expect(MergeRequest.find_by(title: 'MR1').resource_label_events).not_to be_empty
end
it 'restores merge request resource milestone events' do
expect(MergeRequest.find_by(title: 'MR1').resource_milestone_events).not_to be_empty
end
it 'restores merge request resource state events' do
expect(MergeRequest.find_by(title: 'MR1').resource_state_events).not_to be_empty
end
it 'restores suggestion' do
note = Note.find_by("note LIKE 'Saepe asperiores exercitationem non dignissimos laborum reiciendis et ipsum%'")

View File

@ -917,3 +917,15 @@ Approval:
- user_id
- created_at
- updated_at
ResourceMilestoneEvent:
- user_id
- action
- state
- created_at
ResourceStateEvent:
- user_id
- created_at
- state
- source_commit
- close_after_error_tracking_resolve
- close_auto_resolve_prometheus_alert

View File

@ -20,6 +20,7 @@ RSpec.describe Ci::Trigger do
trigger = create(:ci_trigger_without_token, project: project)
expect(trigger.token).not_to be_nil
expect(trigger.token).to start_with(Ci::Trigger::TRIGGER_TOKEN_PREFIX)
end
it 'does not set a random token if one provided' do
@ -30,12 +31,22 @@ RSpec.describe Ci::Trigger do
end
describe '#short_token' do
let(:trigger) { create(:ci_trigger, token: '12345678') }
let(:trigger) { create(:ci_trigger) }
subject { trigger.short_token }
it 'returns shortened token' do
is_expected.to eq('1234')
it 'returns shortened token without prefix' do
is_expected.not_to start_with(Ci::Trigger::TRIGGER_TOKEN_PREFIX)
end
context 'token does not have a prefix' do
before do
trigger.token = '12345678'
end
it 'returns shortened token' do
is_expected.to eq('1234')
end
end
end

View File

@ -897,6 +897,22 @@ RSpec.describe Deployment do
end
end
describe '#build' do
let!(:deployment) { create(:deployment) }
subject { deployment.build }
it 'retrieves build for the deployment' do
is_expected.to eq(deployment.deployable)
end
it 'returns nil when the associated build is not found' do
deployment.update!(deployable_id: nil, deployable_type: nil)
is_expected.to be_nil
end
end
describe '#previous_deployment' do
using RSpec::Parameterized::TableSyntax

View File

@ -3810,8 +3810,8 @@ RSpec.describe User do
describe '#can_be_deactivated?' do
let(:activity) { {} }
let(:user) { create(:user, name: 'John Smith', **activity) }
let(:day_within_minium_inactive_days_threshold) { User::MINIMUM_INACTIVE_DAYS.pred.days.ago }
let(:day_outside_minium_inactive_days_threshold) { User::MINIMUM_INACTIVE_DAYS.next.days.ago }
let(:day_within_minium_inactive_days_threshold) { Gitlab::CurrentSettings.deactivate_dormant_users_period.pred.days.ago }
let(:day_outside_minium_inactive_days_threshold) { Gitlab::CurrentSettings.deactivate_dormant_users_period.next.days.ago }
shared_examples 'not eligible for deactivation' do
it 'returns false' do
@ -7193,8 +7193,8 @@ RSpec.describe User do
describe '.dormant' do
it 'returns dormant users' do
freeze_time do
not_that_long_ago = (described_class::MINIMUM_INACTIVE_DAYS - 1).days.ago.to_date
too_long_ago = described_class::MINIMUM_INACTIVE_DAYS.days.ago.to_date
not_that_long_ago = (Gitlab::CurrentSettings.deactivate_dormant_users_period - 1).days.ago.to_date
too_long_ago = Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_date
create(:user, :deactivated, last_activity_on: too_long_ago)
@ -7214,8 +7214,8 @@ RSpec.describe User do
describe '.with_no_activity' do
it 'returns users with no activity' do
freeze_time do
active_not_that_long_ago = (described_class::MINIMUM_INACTIVE_DAYS - 1).days.ago.to_date
active_too_long_ago = described_class::MINIMUM_INACTIVE_DAYS.days.ago.to_date
active_not_that_long_ago = (Gitlab::CurrentSettings.deactivate_dormant_users_period - 1).days.ago.to_date
active_too_long_ago = Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_date
created_recently = (described_class::MINIMUM_DAYS_CREATED - 1).days.ago.to_date
created_not_recently = described_class::MINIMUM_DAYS_CREATED.days.ago.to_date

View File

@ -295,6 +295,147 @@ RSpec.describe 'Environments Deployments query' do
end
end
shared_examples_for 'avoids N+1 database queries' do
it 'does not increase the query count' do
create_deployments
baseline = ActiveRecord::QueryRecorder.new do
run_with_clean_state(query, context: { current_user: user })
end
create_deployments
multi = ActiveRecord::QueryRecorder.new do
run_with_clean_state(query, context: { current_user: user })
end
expect(multi).not_to exceed_query_limit(baseline)
end
def create_deployments
create_list(:deployment, 3, environment: environment, project: project).each do |deployment|
deployment.user = create(:user).tap { |u| project.add_developer(u) }
deployment.deployable =
create(:ci_build, project: project, environment: environment.name, deployment: deployment,
user: deployment.user)
deployment.save!
end
end
end
context 'when requesting commits of deployments' do
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
environment(name: "#{environment.name}") {
deployments {
nodes {
iid
commit {
author {
avatarUrl
name
webPath
}
fullTitle
webPath
sha
}
}
}
}
}
}
)
end
it_behaves_like 'avoids N+1 database queries'
it 'returns commits of deployments' do
deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
deployments.each do |deployment|
deployment_in_record = project.deployments.find_by_iid(deployment['iid'])
expect(deployment_in_record.sha).to eq(deployment['commit']['sha'])
end
end
end
context 'when requesting triggerers of deployments' do
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
environment(name: "#{environment.name}") {
deployments {
nodes {
iid
triggerer {
id
avatarUrl
name
webPath
}
}
}
}
}
}
)
end
it_behaves_like 'avoids N+1 database queries'
it 'returns triggerers of deployments' do
deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
deployments.each do |deployment|
deployment_in_record = project.deployments.find_by_iid(deployment['iid'])
expect(deployment_in_record.deployed_by.name).to eq(deployment['triggerer']['name'])
end
end
end
context 'when requesting jobs of deployments' do
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
environment(name: "#{environment.name}") {
deployments {
nodes {
iid
job {
id
status
name
webPath
}
}
}
}
}
}
)
end
it_behaves_like 'avoids N+1 database queries'
it 'returns jobs of deployments' do
deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
deployments.each do |deployment|
deployment_in_record = project.deployments.find_by_iid(deployment['iid'])
expect(deployment_in_record.build.to_global_id.to_s).to eq(deployment['job']['id'])
end
end
end
describe 'sorting and pagination' do
let(:data_path) { [:project, :environment, :deployments] }
let(:current_user) { user }

View File

@ -3238,7 +3238,7 @@ RSpec.describe API::Users do
let(:user) { create(:user, **activity) }
context 'with no recent activity' do
let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.next.days.ago } }
let(:activity) { { last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.next.days.ago } }
it 'deactivates an active user' do
deactivate
@ -3249,13 +3249,13 @@ RSpec.describe API::Users do
end
context 'with recent activity' do
let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.pred.days.ago } }
let(:activity) { { last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.pred.days.ago } }
it 'does not deactivate an active user' do
deactivate
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['message']).to eq("403 Forbidden - The user you are trying to deactivate has been active in the past #{::User::MINIMUM_INACTIVE_DAYS} days and cannot be deactivated")
expect(json_response['message']).to eq("403 Forbidden - The user you are trying to deactivate has been active in the past #{Gitlab::CurrentSettings.deactivate_dormant_users_period} days and cannot be deactivated")
expect(user.reload.state).to eq('active')
end
end

View File

@ -6,7 +6,7 @@ RSpec.describe Users::DeactivateDormantUsersWorker do
using RSpec::Parameterized::TableSyntax
describe '#perform' do
let_it_be(:dormant) { create(:user, last_activity_on: User::MINIMUM_INACTIVE_DAYS.days.ago.to_date) }
let_it_be(:dormant) { create(:user, last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_date) }
let_it_be(:inactive) { create(:user, last_activity_on: nil, created_at: User::MINIMUM_DAYS_CREATED.days.ago.to_date) }
let_it_be(:inactive_recently_created) { create(:user, last_activity_on: nil, created_at: (User::MINIMUM_DAYS_CREATED - 1).days.ago.to_date) }
@ -14,7 +14,7 @@ RSpec.describe Users::DeactivateDormantUsersWorker do
it 'does not run for GitLab.com' do
expect(Gitlab).to receive(:com?).and_return(true)
expect(Gitlab::CurrentSettings).not_to receive(:current_application_settings)
# Now makes a call to current settings to determine period of dormancy
worker.perform
@ -48,7 +48,7 @@ RSpec.describe Users::DeactivateDormantUsersWorker do
end
with_them do
it 'deactivates certain user types' do
user = create(:user, user_type: user_type, state: :active, last_activity_on: User::MINIMUM_INACTIVE_DAYS.days.ago.to_date)
user = create(:user, user_type: user_type, state: :active, last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_date)
worker.perform
@ -57,8 +57,8 @@ RSpec.describe Users::DeactivateDormantUsersWorker do
end
it 'does not deactivate non-active users' do
human_user = create(:user, user_type: :human, state: :blocked, last_activity_on: User::MINIMUM_INACTIVE_DAYS.days.ago.to_date)
service_user = create(:user, user_type: :service_user, state: :blocked, last_activity_on: User::MINIMUM_INACTIVE_DAYS.days.ago.to_date)
human_user = create(:user, user_type: :human, state: :blocked, last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_date)
service_user = create(:user, user_type: :service_user, state: :blocked, last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_date)
worker.perform