Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-12-19 18:09:34 +00:00
parent c18599314d
commit d4e22f4ade
80 changed files with 414 additions and 245 deletions

View File

@ -37,11 +37,6 @@ workflow:
- if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^release-tools\/\d+\.\d+\.\d+-rc\d+$/ && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^[\d-]+-stable(-ee)?$/ && $CI_PROJECT_PATH == "gitlab-org/gitlab"'
when: never
# For merge requests running exclusively in Ruby 3.0
- if: '($CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train") && $CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3/'
variables:
RUBY_VERSION: "3.0"
PIPELINE_NAME: 'Ruby 3 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
# For merge requests running exclusively in Ruby 3.0
- if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3/'
variables:
RUBY_VERSION: "3.0"

View File

@ -197,8 +197,7 @@
- "spec/support/gitlab-git-test.git/**/*"
.yaml-lint-patterns: &yaml-lint-patterns
- "*.yml"
- "**/*.yml"
- "**/*.{yml,yaml}{,.*}"
.lint-pipeline-yaml-patterns: &lint-pipeline-yaml-patterns
- ".gitlab-ci.yml"

View File

@ -4,21 +4,3 @@ Security/IoMethods:
Details: grace period
Exclude:
- 'db/migrate/20210301200959_init_schema.rb'
- 'ee/lib/tasks/gitlab/spdx.rake'
- 'ee/spec/factories/spdx_catalogue.rb'
- 'ee/spec/lib/ee/gitlab/import_export/group/legacy_tree_saver_spec.rb'
- 'ee/spec/lib/gitlab/spdx/catalogue_spec.rb'
- 'lib/gitlab/import_export/json/legacy_reader.rb'
- 'lib/gitlab/import_export/lfs_restorer.rb'
- 'lib/tasks/gitlab/assets.rake'
- 'spec/features/projects/import_export/export_file_spec.rb'
- 'spec/lib/backup/gitaly_backup_spec.rb'
- 'spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb'
- 'spec/lib/gitlab/import_export/group/legacy_tree_saver_spec.rb'
- 'spec/lib/gitlab/import_export/group/tree_restorer_spec.rb'
- 'spec/lib/gitlab/import_export/import_test_coverage_spec.rb'
- 'spec/lib/gitlab/import_export/json/legacy_writer_spec.rb'
- 'spec/lib/gitlab/import_export/lfs_saver_spec.rb'
- 'spec/lib/gitlab/import_export/project/relation_saver_spec.rb'
- 'spec/support/helpers/gitaly_setup.rb'
- 'spec/support/import_export/common_util.rb'

View File

@ -2,6 +2,15 @@
extends: default
yaml-files:
# defaults
- '*.yaml'
- '*.yml'
- '.yamllint'
# match more extensions
- '*.yaml.*'
- '*.yml.*'
# Ideally, we should have nothing in this ignore section.
#
# Please consider removing entries below by fixing them.
@ -19,6 +28,16 @@ ignore: |
# Broken on purpose (for testing)
spec/fixtures/lib/gitlab/metrics/dashboard/broken_yml_syntax.yml
# Dynamic YAML files have syntax errors sometimes.
*.erb
# Vim temporary files.
*.sw[pon]
# Zipped files (by e.g. asset pipeline)
*.gz
*.bz2
#### Folders ####
node_modules/
tmp/

View File

@ -1 +1 @@
38812995ea07e43b12b4151c24bf6b960a70f74d
6d9ffab522aae0f2fac5d3ff152064f56b01081d

View File

@ -13,7 +13,7 @@ import {
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import Api, { DEFAULT_PER_PAGE } from '~/api';
import httpStatusCodes from '~/lib/utils/http_status';
import { HTTP_STATUS_PAYLOAD_TOO_LARGE } from '~/lib/utils/http_status';
import { __, s__, sprintf } from '~/locale';
import Tracking from '~/tracking';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@ -145,7 +145,7 @@ export default {
let message = '';
if (error?.response?.data?.message?.name) {
message = this.$options.i18n.uploadErrorMessages.duplicate;
} else if (error.response.status === httpStatusCodes.PAYLOAD_TOO_LARGE) {
} else if (error.response.status === HTTP_STATUS_PAYLOAD_TOO_LARGE) {
message = sprintf(this.$options.i18n.uploadErrorMessages.tooLarge, {
limit: this.fileSizeLimit,
});

View File

@ -1,6 +1,6 @@
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import * as terminalService from '../../../../services/terminals';
import { STARTING, STOPPING, STOPPED } from '../constants';
import * as messages from '../messages';
@ -108,7 +108,7 @@ export const restartSession = ({ state, dispatch, rootState }) => {
// We may have removed the build, in this case we'll just create a new session
if (
responseStatus === httpStatus.NOT_FOUND ||
responseStatus === httpStatus.UNPROCESSABLE_ENTITY
responseStatus === HTTP_STATUS_UNPROCESSABLE_ENTITY
) {
dispatch('startSession');
} else {

View File

@ -1,5 +1,5 @@
import { escape } from 'lodash';
import httpStatus from '~/lib/utils/http_status';
import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import { __, sprintf } from '~/locale';
export const UNEXPECTED_ERROR_CONFIG = __(
@ -28,7 +28,7 @@ export const ERROR_PERMISSION = __(
);
export const configCheckError = (status, helpUrl) => {
if (status === httpStatus.UNPROCESSABLE_ENTITY) {
if (status === HTTP_STATUS_UNPROCESSABLE_ENTITY) {
return sprintf(
ERROR_CONFIG,
{

View File

@ -3,7 +3,7 @@ import _ from 'lodash';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import { HTTP_STATUS_TOO_MANY_REQUESTS } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { visitUrl, objectToQuery } from '~/lib/utils/url_utility';
@ -16,7 +16,7 @@ let eTagPoll;
const hasRedirectInError = (e) => e?.response?.data?.error?.redirect;
const redirectToUrlInError = (e) => visitUrl(e.response.data.error.redirect);
const tooManyRequests = (e) => e.response.status === httpStatusCodes.TOO_MANY_REQUESTS;
const tooManyRequests = (e) => e.response.status === HTTP_STATUS_TOO_MANY_REQUESTS;
const pathWithParams = ({ path, ...params }) => {
const filteredParams = Object.fromEntries(
Object.entries(params).filter(([, value]) => value !== ''),

View File

@ -1,45 +1,43 @@
/**
* exports HTTP status codes
*/
export const HTTP_STATUS_ABORTED = 0;
export const HTTP_STATUS_CREATED = 201;
export const HTTP_STATUS_ACCEPTED = 202;
export const HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION = 203;
export const HTTP_STATUS_NO_CONTENT = 204;
export const HTTP_STATUS_RESET_CONTENT = 205;
export const HTTP_STATUS_PARTIAL_CONTENT = 206;
export const HTTP_STATUS_MULTI_STATUS = 207;
export const HTTP_STATUS_ALREADY_REPORTED = 208;
export const HTTP_STATUS_IM_USED = 226;
export const HTTP_STATUS_METHOD_NOT_ALLOWED = 405;
export const HTTP_STATUS_CONFLICT = 409;
export const HTTP_STATUS_GONE = 410;
export const HTTP_STATUS_PAYLOAD_TOO_LARGE = 413;
export const HTTP_STATUS_UNPROCESSABLE_ENTITY = 422;
export const HTTP_STATUS_TOO_MANY_REQUESTS = 429;
// TODO move the rest of the status codes to primitive constants
// https://docs.gitlab.com/ee/development/fe_guide/style/javascript.html#export-constants-as-primitives
const httpStatusCodes = {
ABORTED: 0,
OK: 200,
CREATED: 201,
ACCEPTED: 202,
NON_AUTHORITATIVE_INFORMATION: 203,
NO_CONTENT: 204,
RESET_CONTENT: 205,
PARTIAL_CONTENT: 206,
MULTI_STATUS: 207,
ALREADY_REPORTED: 208,
IM_USED: 226,
MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
METHOD_NOT_ALLOWED: 405,
CONFLICT: 409,
GONE: 410,
PAYLOAD_TOO_LARGE: 413,
UNPROCESSABLE_ENTITY: 422,
TOO_MANY_REQUESTS: 429,
INTERNAL_SERVER_ERROR: 500,
SERVICE_UNAVAILABLE: 503,
};
export const successCodes = [
httpStatusCodes.OK,
httpStatusCodes.CREATED,
httpStatusCodes.ACCEPTED,
httpStatusCodes.NON_AUTHORITATIVE_INFORMATION,
httpStatusCodes.NO_CONTENT,
httpStatusCodes.RESET_CONTENT,
httpStatusCodes.PARTIAL_CONTENT,
httpStatusCodes.MULTI_STATUS,
httpStatusCodes.ALREADY_REPORTED,
httpStatusCodes.IM_USED,
HTTP_STATUS_CREATED,
HTTP_STATUS_ACCEPTED,
HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION,
HTTP_STATUS_NO_CONTENT,
HTTP_STATUS_RESET_CONTENT,
HTTP_STATUS_PARTIAL_CONTENT,
HTTP_STATUS_MULTI_STATUS,
HTTP_STATUS_ALREADY_REPORTED,
HTTP_STATUS_IM_USED,
];
export default httpStatusCodes;

View File

@ -1,5 +1,5 @@
import { normalizeHeaders } from './common_utils';
import httpStatusCodes, { successCodes } from './http_status';
import { HTTP_STATUS_ABORTED, successCodes } from './http_status';
/**
* Polling utility for handling realtime updates.
@ -108,7 +108,7 @@ export default class Poll {
})
.catch((error) => {
notificationCallback(false);
if (error.status === httpStatusCodes.ABORTED) {
if (error.status === HTTP_STATUS_ABORTED) {
return;
}
errorCallback(error);

View File

@ -1,13 +1,16 @@
import axios from '~/lib/utils/axios_utils';
import { backOff } from '~/lib/utils/common_utils';
import statusCodes from '~/lib/utils/http_status';
import statusCodes, {
HTTP_STATUS_NO_CONTENT,
HTTP_STATUS_UNPROCESSABLE_ENTITY,
} from '~/lib/utils/http_status';
import { PROMETHEUS_TIMEOUT } from '../constants';
const cancellableBackOffRequest = (makeRequestCallback) =>
backOff((next, stop) => {
makeRequestCallback()
.then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
if (resp.status === HTTP_STATUS_NO_CONTENT) {
next();
} else {
stop(resp);
@ -34,7 +37,7 @@ export const getPrometheusQueryData = (prometheusEndpoint, params, opts) =>
const { response = {} } = error;
if (
response.status === statusCodes.BAD_REQUEST ||
response.status === statusCodes.UNPROCESSABLE_ENTITY ||
response.status === HTTP_STATUS_UNPROCESSABLE_ENTITY ||
response.status === statusCodes.SERVICE_UNAVAILABLE
) {
const { data } = response;

View File

@ -7,7 +7,7 @@ import Autosave from '~/autosave';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/flash';
import { badgeState } from '~/issuable/components/status_box.vue';
import httpStatusCodes from '~/lib/utils/http_status';
import { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import {
capitalizeFirstCharacter,
convertToCamelCase,
@ -28,8 +28,6 @@ import CommentTypeDropdown from './comment_type_dropdown.vue';
import DiscussionLockedWidget from './discussion_locked_widget.vue';
import NoteSignedOutWidget from './note_signed_out_widget.vue';
const { UNPROCESSABLE_ENTITY } = httpStatusCodes;
export default {
name: 'CommentForm',
i18n: COMMENT_FORM,
@ -198,7 +196,7 @@ export default {
'toggleIssueLocalState',
]),
handleSaveError({ data, status }) {
if (status === UNPROCESSABLE_ENTITY && data.errors?.commands_only?.length) {
if (status === HTTP_STATUS_UNPROCESSABLE_ENTITY && data.errors?.commands_only?.length) {
this.errors = data.errors.commands_only;
} else {
this.errors = [this.$options.i18n.GENERIC_UNSUBMITTABLE_NETWORK];

View File

@ -7,7 +7,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
import { createAlert } from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import { HTTP_STATUS_GONE } from '~/lib/utils/http_status';
import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
@ -338,7 +338,7 @@ export default {
callback();
})
.catch((response) => {
if (response.status === httpStatusCodes.GONE) {
if (response.status === HTTP_STATUS_GONE) {
this.removeNote(this.note);
this.updateSuccess();
callback();

View File

@ -1,6 +1,6 @@
import axios from '~/lib/utils/axios_utils';
import { backOff } from '~/lib/utils/common_utils';
import statusCodes from '~/lib/utils/http_status';
import statusCodes, { HTTP_STATUS_ACCEPTED } from '~/lib/utils/http_status';
import { __, s__ } from '~/locale';
import * as types from './mutation_types';
@ -10,7 +10,7 @@ function backOffRequest(makeRequestCallback) {
return backOff((next, stop) => {
makeRequestCallback()
.then((resp) => {
if (resp.status === statusCodes.ACCEPTED) {
if (resp.status === HTTP_STATUS_ACCEPTED) {
next();
} else {
stop(resp);
@ -31,7 +31,7 @@ export const requestCreateProject = ({ dispatch, state, commit }) => {
axios
.post(state.createProjectEndpoint)
.then((resp) => {
if (resp.status === statusCodes.ACCEPTED) {
if (resp.status === HTTP_STATUS_ACCEPTED) {
dispatch('requestCreateProjectStatus', resp.data.job_id);
}
})
@ -83,7 +83,7 @@ export const requestDeleteProject = ({ dispatch, state, commit }) => {
axios
.delete(state.deleteProjectEndpoint)
.then((resp) => {
if (resp.status === statusCodes.ACCEPTED) {
if (resp.status === HTTP_STATUS_ACCEPTED) {
dispatch('requestDeleteProjectStatus', resp.data.job_id);
}
})

View File

@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlSprintf, GlLink } from '@gitlab/ui';
import { backOff } from '~/lib/utils/common_utils';
import statusCodes from '~/lib/utils/http_status';
import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import { bytesToMiB } from '~/lib/utils/number_utils';
import { s__ } from '~/locale';
import MemoryGraph from '~/vue_shared/components/memory_graph.vue';
@ -107,7 +107,7 @@ export default {
backOff((next, stop) => {
MRWidgetService.fetchMetrics(this.metricsUrl)
.then((res) => {
if (res.status === statusCodes.NO_CONTENT) {
if (res.status === HTTP_STATUS_NO_CONTENT) {
this.backOffRequestCounter += 1;
/* eslint-disable no-unused-expressions */
this.backOffRequestCounter < 3 ? next() : stop(res);
@ -118,7 +118,7 @@ export default {
.catch(stop);
})
.then((res) => {
if (res.status === statusCodes.NO_CONTENT) {
if (res.status === HTTP_STATUS_NO_CONTENT) {
return res;
}

View File

@ -1,7 +1,7 @@
import axios from '~/lib/utils/axios_utils';
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
import { SEVERITY_ICONS_MR_WIDGET } from '~/ci/reports/codequality_report/constants';
import httpStatusCodes from '~/lib/utils/http_status';
import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/store/utils/codequality_parser';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { i18n } from './constants';
@ -43,7 +43,7 @@ export default {
return {
...response,
data: {
parsingInProgress: status === httpStatusCodes.NO_CONTENT,
parsingInProgress: status === HTTP_STATUS_NO_CONTENT,
resolvedErrors: parseCodeclimateMetrics(data.resolved_errors, this.blobPath.head_path),
newErrors: parseCodeclimateMetrics(data.new_errors, this.blobPath.head_path),
existingErrors: parseCodeclimateMetrics(data.existing_errors, this.blobPath.head_path),

View File

@ -10,6 +10,7 @@ import {
STATE_OPEN,
TASK_TYPE_NAME,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
WIDGET_TYPE_PROGRESS,
WIDGET_TYPE_MILESTONE,
WIDGET_TYPE_HIERARCHY,
WIDGET_TYPE_ASSIGNEES,
@ -113,7 +114,15 @@ export default {
return this.isExpanded ? __('Collapse') : __('Expand');
},
hasMetadata() {
return this.milestone || this.assignees.length > 0 || this.labels.length > 0;
return (
this.progress !== undefined ||
this.milestone !== undefined ||
this.assignees.length > 0 ||
this.labels.length > 0
);
},
progress() {
return this.getWidgetByType(this.childItem, WIDGET_TYPE_PROGRESS)?.progress;
},
milestone() {
return this.getWidgetByType(this.childItem, WIDGET_TYPE_MILESTONE)?.milestone;
@ -231,6 +240,7 @@ export default {
<work-item-link-child-metadata
v-if="hasMetadata"
:allows-scoped-labels="allowsScopedLabels"
:progress="progress"
:milestone="milestone"
:assignees="assignees"
:labels="labels"

View File

@ -1,5 +1,12 @@
<script>
import { GlLabel, GlAvatar, GlAvatarLink, GlAvatarsInline, GlTooltipDirective } from '@gitlab/ui';
import {
GlIcon,
GlLabel,
GlAvatar,
GlAvatarLink,
GlAvatarsInline,
GlTooltipDirective,
} from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import { isScopedLabel } from '~/lib/utils/common_utils';
@ -8,6 +15,7 @@ import ItemMilestone from '~/issuable/components/issue_milestone.vue';
export default {
components: {
GlIcon,
GlLabel,
GlAvatar,
GlAvatarLink,
@ -23,6 +31,11 @@ export default {
required: false,
default: false,
},
progress: {
type: Number,
required: false,
default: 0,
},
milestone: {
type: Object,
required: false,
@ -40,6 +53,9 @@ export default {
},
},
computed: {
hasProgress() {
return !(this.progress === null || this.progress === undefined);
},
assigneesCollapsedTooltip() {
if (this.assignees.length > 2) {
return sprintf(s__('WorkItem|%{count} more assignees'), {
@ -56,12 +72,6 @@ export default {
}
return '';
},
labelsContainerClass() {
if (this.milestone || this.assignees.length) {
return 'gl-sm-ml-5';
}
return '';
},
},
methods: {
showScopedLabel(label) {
@ -73,6 +83,16 @@ export default {
<template>
<div class="gl-display-flex gl-flex-wrap gl-align-items-center">
<div
v-if="hasProgress"
v-gl-tooltip.bottom
:title="__('Progress')"
class="gl-display-flex gl-align-items-center gl-mr-5 gl-cursor-help gl-line-height-normal"
data-testid="item-progress"
>
<gl-icon name="progress" />
<span class="gl-text-primary gl-ml-2">{{ progress }}%</span>
</div>
<item-milestone
v-if="milestone"
:milestone="milestone"
@ -87,6 +107,7 @@ export default {
badge-tooltip-prop="name"
:badge-sr-only-text="assigneesCollapsedTooltip"
:class="assigneesContainerClass"
class="gl-mr-5"
>
<template #avatar="{ avatar }">
<gl-avatar-link v-gl-tooltip target="blank" :href="avatar.webUrl" :title="avatar.name">
@ -94,7 +115,7 @@ export default {
</gl-avatar-link>
</template>
</gl-avatars-inline>
<div v-if="labels.length" class="gl-display-flex gl-flex-wrap" :class="labelsContainerClass">
<div v-if="labels.length" class="gl-display-flex gl-flex-wrap">
<gl-label
v-for="label in labels"
:key="label.id"
@ -102,7 +123,7 @@ export default {
:background-color="label.color"
:description="label.description"
:scoped="showScopedLabel(label)"
class="gl-mt-2 gl-sm-mt-0 gl-mr-2 gl-mb-auto gl-label-sm"
class="gl-mt-3 gl-sm-mt-0 gl-mr-2 gl-mb-auto gl-label-sm"
tooltip-placement="top"
/>
</div>

View File

@ -3,6 +3,10 @@
#import "~/work_items/graphql/milestone.fragment.graphql"
fragment WorkItemMetadataWidgets on WorkItemWidget {
... on WorkItemWidgetProgress {
type
progress
}
... on WorkItemWidgetMilestone {
type
milestone {

View File

@ -236,12 +236,6 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
}
}
// TODO: Remove once https: //gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3198 is merged
.gl-sm-ml-5 {
@include gl-media-breakpoint-up(sm) {
@include gl-ml-5;
}
}
/* End gitlab-ui#1709 */

View File

@ -27,7 +27,9 @@ module TodosHelper
)
when Todo::UNMERGEABLE then s_('Todos|Could not merge')
when Todo::MERGE_TRAIN_REMOVED then s_("Todos|Removed from Merge Train")
when Todo::MEMBER_ACCESS_REQUESTED then s_("Todos|has requested access")
when Todo::MEMBER_ACCESS_REQUESTED then format(
s_("Todos|has requested access to group %{which}"), which: _(todo.target.name)
)
end
end

View File

@ -221,6 +221,8 @@ class Todo < ApplicationRecord
def body
if note.present?
note.note
elsif member_access_requested?
target.full_path
else
target.title
end
@ -258,6 +260,8 @@ class Todo < ApplicationRecord
def target_reference
if for_commit?
target.reference_link_text
elsif member_access_requested?
target.full_path
else
target.to_reference
end

View File

@ -8,7 +8,7 @@
"^[A-Za-z]+[0-9]*(?:[._-][A-Za-z0-9]+)*$": {
"type": "string",
"minLength": 1,
"maxLength": 100
"maxLength": 2048
}
}
}

View File

@ -14,12 +14,11 @@
%span
= todo_parent_path(todo)
- unless todo.member_access_requested?
%span.todo-label
- if todo.target
= link_to todo_target_name(todo), todo_target_path(todo), class: 'todo-target-link gl-text-gray-500! gl-text-decoration-none!', :'aria-describedby' => dom_id(todo) + "_describer", :'aria-label' => todo_target_aria_label(todo)
- else
= _("(removed)")
%span.todo-label
- if todo.target
= link_to todo_target_name(todo), todo_target_path(todo), class: 'todo-target-link gl-text-gray-500! gl-text-decoration-none!', :'aria-describedby' => dom_id(todo) + "_describer", :'aria-label' => todo_target_aria_label(todo)
- else
= _("(removed)")
.todo-body.gl-mb-2.gl-px-2.gl-display-flex.gl-align-items-flex-start.gl-lg-align-items-center
.todo-avatar.gl-display-none.gl-sm-display-inline-block

View File

@ -11,15 +11,16 @@
.col-lg-8.gl-mb-3
- if file_hooks.any?
.card
.card-header
= render Pajamas::CardComponent.new do |c|
- c.header do
= _('File Hooks (%{count})') % { count: file_hooks.count }
%ul.content-list
- file_hooks.each do |file|
%li
.monospace
= File.basename(file)
- c.body do
%ul.content-list
- file_hooks.each do |file|
%li
.monospace
= File.basename(file)
- else
.card.bg-light.text-center
.nothing-here-block= _('No file hooks found.')
= render Pajamas::CardComponent.new do |c|
- c.body do
.nothing-here-block= _('No file hooks found.')

View File

@ -0,0 +1,8 @@
---
name: multiple_environment_approval_rules_fe
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105719
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/384334
milestone: '15.7'
type: development
group: group::release
default_enabled: false

View File

@ -20,14 +20,14 @@ GET /todos
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Attribute | Type | Required | Description |
| --------- | ---- | -------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `action` | string | no | The action to be filtered. Can be `assigned`, `mentioned`, `build_failed`, `marked`, `approval_required`, `unmergeable`, `directly_addressed`, `merge_train_removed` or `member_access_requested`. |
| `author_id` | integer | no | The ID of an author |
| `project_id` | integer | no | The ID of a project |
| `group_id` | integer | no | The ID of a group |
| `state` | string | no | The state of the to-do item. Can be either `pending` or `done` |
| `type` | string | no | The type of to-do item. Can be either `Issue`, `MergeRequest`, `Commit`, `Epic`, `DesignManagement::Design` or `AlertManagement::Alert` |
| `author_id` | integer | no | The ID of an author |
| `project_id` | integer | no | The ID of a project |
| `group_id` | integer | no | The ID of a group |
| `state` | string | no | The state of the to-do item. Can be either `pending` or `done` |
| `type` | string | no | The type of to-do item. Can be either `Issue`, `MergeRequest`, `Commit`, `Epic`, `DesignManagement::Design`, `AlertManagement::Alert` or `Namespace` |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/todos"

View File

@ -124,6 +124,8 @@ page, with these behaviors:
branch name (unless their out-of-office (`OOO`) status changes, as in point 1). It
removes leading `ce-` and `ee-`, and trailing `-ce` and `-ee`, so
that it can be stable for backport branches.
- People whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status) emoji
is Ⓜ `:m:`are only suggested as reviewers on projects they are a maintainer of.
The [Roulette dashboard](https://gitlab-org.gitlab.io/gitlab-roulette/) contains:

View File

@ -47,7 +47,8 @@ This needs to be done for any new, or updated gems.
We do not allow gems that are fetched from Git repositories. All gems have
to be available in the RubyGems index. We want to minimize external build
dependencies and build times.
dependencies and build times. It's enforced by the RuboCop rule
[`Cop/GemFetcher`](https://gitlab.com/gitlab-org/ruby/gems/gitlab-styles/-/blob/master/lib/rubocop/cop/gem_fetcher.rb).
## Review the new dependency for quality

View File

@ -122,10 +122,21 @@ which has a related schema in `/config/metrics/objects_schemas/topology_schema.j
### Metric `time_frame`
- `7d`: The metric data applies to the most recent 7-day interval. For example, the following metric counts the number of users that create epics over a 7-day interval: `ee/config/metrics/counts_7d/20210305145820_g_product_planning_epic_created_weekly.yml`.
- `28d`: The metric data applies to the most recent 28-day interval. For example, the following metric counts the number of unique users that create issues over a 28-day interval: `config/metrics/counts_28d/20210216181139_issues.yml`.
- `all`: The metric data applies for the whole time the metric has been active (all-time interval). For example, the following metric counts all users that create issues: `/config/metrics/counts_all/20210216181115_issues.yml`.
- `none`: The metric collects a type of data that's not tracked over time, such as settings and configuration information. Therefore, a time interval is not applicable. For example, `uuid` has no time interval applicable: `config/metrics/license/20210201124933_uuid.yml`.
A metric's time frame is calculated based on the `time_frame` field and the `data_source` of the metric.
For `redis_hll` metrics, the type of aggregation is also taken into consideration. In this context, the term "aggregation" refers to [chosen events data storage interval](implement.md#add-new-events), and is **NOT** related to the Aggregated Metrics feature.
For more information about the aggregation type of each feature, see the [`common.yml` file](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/known_events/common.yml). Weeks run from Monday to Sunday.
| data_source | time_frame | aggregation | Description |
|------------------------|------------|----------------|-------------------------------------------------|
| any | `none` | not applicable | A type of data thats not tracked over time, such as settings and configuration information |
| `database` | `all` | not applicable | The whole time the metric has been active (all-time interval) |
| `database` | `7d` | not applicable | 9 days ago to 2 days ago |
| `database` | `28d` | not applicable | 30 days ago to 2 days ago |
| `redis` | `all` | not applicable | The whole time the metric has been active (all-time interval) |
| `redis_hll` | `7d` | `daily` | Most recent 7 complete days |
| `redis_hll` | `7d` | `weekly` | Most recent complete week |
| `redis_hll` | `28d` | `daily` | Most recent 28 complete days |
| `redis_hll` | `28d` | `weekly` | Most recent 4 complete weeks |
### Data category

View File

@ -61,6 +61,8 @@ GitLab.
For this association to succeed, each GitHub author and assignee in the repository
must have a [public-facing email address](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address)
on GitHub that matches their GitLab email address (regardless of how the account was created).
If their email address from GitHub is set as their secondary email address in GitLab, it must be
confirmed.
GitLab content imports that use GitHub accounts require that the GitHub public-facing email address is populated. This means
all comments and contributions are properly mapped to the same user in GitLab. GitHub Enterprise does not require this

View File

@ -28,7 +28,7 @@ pre-push:
yamllint:
tags: backend style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: '*.{yml,yaml}'
glob: '*.{yml,yaml}{,.*}'
run: scripts/lint-yaml.sh {files}
stylelint:
tags: stylesheet css style

View File

@ -32,6 +32,7 @@ module API
def todo_target_url(todo)
return design_todo_target_url(todo) if todo.for_design?
return todo.access_request_url if todo.member_access_requested?
target_type = todo.target_type.gsub('::', '_').underscore
target_url = "#{todo.resource_parent.class.to_s.underscore}_#{target_type}_url"

View File

@ -27,7 +27,7 @@ module Gitlab
end
def read_hash
Gitlab::Json.parse(IO.read(@path))
Gitlab::Json.parse(::File.read(@path))
rescue StandardError => e
Gitlab::ErrorTracking.log_exception(e)
raise Gitlab::ImportExport::Error, 'Incorrect JSON format'

View File

@ -71,7 +71,7 @@ module Gitlab
@lfs_json ||=
begin
json = IO.read(lfs_json_path)
json = File.read(lfs_json_path)
Gitlab::Json.parse(json)
rescue StandardError
raise Gitlab::ImportExport::Error, 'Incorrect JSON format'

View File

@ -137,7 +137,7 @@ namespace :gitlab do
File.open(gzip, 'wb+') do |f|
gz = Zlib::GzipWriter.new(f, Zlib::BEST_COMPRESSION)
gz.mtime = mtime
gz.write IO.binread(file)
gz.write File.binread(file)
gz.close
File.utime(mtime, mtime, f.path)

View File

@ -5078,6 +5078,9 @@ msgstr ""
msgid "Approvals are optional."
msgstr ""
msgid "Approvals required"
msgstr ""
msgid "Approvals|Section: %section"
msgstr ""
@ -22161,7 +22164,7 @@ msgstr ""
msgid "Insights|Configure a custom report for insights into your group processes such as amount of issues, bugs, and merge requests per month. %{linkStart}How do I configure an insights report?%{linkEnd}"
msgstr ""
msgid "Insights|Some items are not visible beacuse the project was filtered out in the insights.yml file (see the projects.only config for more information)."
msgid "Insights|Some items are not visible beacuse the project was filtered out in the insights.yml file (see the projects.only config in the YAML file or the enabled project features (issues, merge requests) in the project settings)."
msgstr ""
msgid "Insights|This project is filtered out in the insights.yml file (see the projects.only config for more information)."
@ -33669,6 +33672,24 @@ msgstr ""
msgid "ProtectedBranch|default"
msgstr ""
msgid "ProtectedEnvironments|Allowed to deploy"
msgstr ""
msgid "ProtectedEnvironments|An error occurred while fetching information on the selected approvers."
msgstr ""
msgid "ProtectedEnvironments|Approval rules"
msgstr ""
msgid "ProtectedEnvironments|Number of approvals must be between 1 and 5"
msgstr ""
msgid "ProtectedEnvironments|Set which groups, access levels or users are required to approve."
msgstr ""
msgid "ProtectedEnvironments|Set which groups, access levels or users that are allowed to deploy to this environment"
msgstr ""
msgid "ProtectedEnvironment|%{environment_name} will be writable for developers. Are you sure?"
msgstr ""
@ -33681,6 +33702,9 @@ msgstr ""
msgid "ProtectedEnvironment|Allowed to deploy to %{project} / %{environment}"
msgstr ""
msgid "ProtectedEnvironment|Approvers"
msgstr ""
msgid "ProtectedEnvironment|Environment"
msgstr ""
@ -33717,6 +33741,9 @@ msgstr ""
msgid "ProtectedEnvironment|Select an environment"
msgstr ""
msgid "ProtectedEnvironment|Select environment"
msgstr ""
msgid "ProtectedEnvironment|Select groups"
msgstr ""
@ -43479,7 +43506,7 @@ msgstr ""
msgid "Todos|added a to-do item"
msgstr ""
msgid "Todos|has requested access"
msgid "Todos|has requested access to group %{which}"
msgstr ""
msgid "Todos|mentioned %{who}"

View File

@ -81,7 +81,8 @@ module QA
reload: false,
skip_finished_loading_check_on_refresh: true
) do
is_partial_import = has_css?(:import_status_indicator, text: "Partial import")
status_selector = 'import_status_indicator'
is_partial_import = has_css?(status_selector, text: "Partial import")
# Temporarily adding this for investigation purposes. This makes sure that the details section is
# expanded when the screenshot is taken when the test fails. This can be removed or repurposed later
@ -92,7 +93,7 @@ module QA
end
end
has_element?(:import_status_indicator, text: "Complete")
has_element?(status_selector, text: "Complete")
end
end
end

View File

@ -4,7 +4,7 @@ module QA
# https://github.com/gitlab-qa-github/import-test <- project under test
#
RSpec.describe 'Manage', product_group: :import do
describe 'GitHub import', :reliable do
describe 'GitHub import' do
include_context 'with github import'
context 'when imported via api' do

View File

@ -1,23 +1,11 @@
# frozen_string_literal: true
module QA
# Spec uses real github.com, which means outage of github can actually block deployment
# Keep spec in reliable bucket but don't run in blocking pipelines
RSpec.describe 'Manage', :github, :reliable, :skip_live_env, :requires_admin, product_group: :import do
RSpec.describe 'Manage', product_group: :import do
describe 'GitHub import' do
include QA::Support::Data::Github
include_context 'with github import'
context 'when imported via UI' do
let(:github_repo) { "#{github_username}/import-test" }
let(:api_client) { Runtime::API::Client.as_admin }
let(:group) { Resource::Group.fabricate_via_api! { |resource| resource.api_client = api_client } }
let(:user) do
Resource::User.fabricate_via_api! do |resource|
resource.api_client = api_client
resource.hard_delete_on_api_removal = true
end
end
let(:imported_project) do
Resource::ProjectImportedFromGithub.init do |project|
project.import = true
@ -41,8 +29,6 @@ module QA
end
before do
group.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
Flow::Login.sign_in(as: user)
Page::Main::Menu.perform(&:go_to_create_project)
Page::Project::New.perform do |project_page|
@ -51,10 +37,6 @@ module QA
end
end
after do
user.remove_via_api!
end
it 'imports a project', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347877' do
Page::Project::Import::Github.perform do |import_page|
import_page.add_personal_access_token(Runtime::Env.github_access_token)

View File

@ -1,12 +1,10 @@
# frozen_string_literal: true
module QA
RSpec.shared_context "with github import", :github, :skip_live_env, :requires_admin, quarantine: {
type: :broken,
issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/382166"
} do
RSpec.shared_context "with github import", :github, :import, :requires_admin, :orchestrated do
include QA::Support::Data::Github
let!(:github_repo) { "#{github_username}/import-test" }
let!(:api_client) { Runtime::API::Client.as_admin }
let!(:group) do
@ -30,7 +28,7 @@ module QA
project.name = 'imported-project'
project.group = group
project.github_personal_access_token = Runtime::Env.github_access_token
project.github_repository_path = "#{github_username}/import-test"
project.github_repository_path = github_repo
project.api_client = user_api_client
project.issue_events_import = true
project.full_notes_import = true
@ -40,9 +38,5 @@ module QA
before do
group.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
end
after do
user.remove_via_api!
end
end
end

View File

View File

@ -70,7 +70,7 @@ gitlab:
memory: 920Mi
limits:
cpu: 800m
memory: 1100Mi
memory: 1380Mi
sidekiq:
resources:
@ -99,7 +99,7 @@ gitlab:
cpu: 746m
memory: 2809Mi
limits:
cpu: 1119m
cpu: 1300m
memory: 4214Mi
minReplicas: 1
maxReplicas: 1

View File

@ -41,6 +41,10 @@ FactoryBot.define do
action { Todo::UNMERGEABLE }
end
trait :member_access_requested do
action { Todo::MEMBER_ACCESS_REQUESTED }
end
trait :pending do
state { :pending }
end

View File

@ -445,4 +445,28 @@ RSpec.describe 'Dashboard Todos', feature_category: :team_planning do
expect(page).to have_selector('.todos-list .todo', count: 1)
end
end
context 'User has a todo for an access requested raised for group membership' do
let_it_be(:group) { create(:group, :public) }
let_it_be(:todo) do
create(:todo, :member_access_requested,
user: user,
target: group,
author: author,
group: group)
end
before do
group.add_owner(user)
sign_in(user)
visit dashboard_todos_path
end
it 'has todo present with access request content' do
expect(page).to have_selector('.todos-list .todo', count: 1)
expect(page).to have_content "#{author.name} has requested access to group #{group.name}"
end
end
end

View File

@ -53,7 +53,7 @@ RSpec.describe 'Import/Export - project export integration test', :js, feature_c
project_json_path = File.join(tmpdir, 'project.json')
expect(File).to exist(project_json_path)
project_hash = Gitlab::Json.parse(IO.read(project_json_path))
project_hash = Gitlab::Json.parse(File.read(project_json_path))
sensitive_words.each do |sensitive_word|
found = find_sensitive_attributes(sensitive_word, project_hash)
@ -79,7 +79,7 @@ RSpec.describe 'Import/Export - project export integration test', :js, feature_c
expect(File).to exist(project_json_path)
relations = []
relations << Gitlab::Json.parse(IO.read(project_json_path))
relations << Gitlab::Json.parse(File.read(project_json_path))
Dir.glob(File.join(tmpdir, 'tree/project', '*.ndjson')) do |rb_filename|
File.foreach(rb_filename) do |line|
relations << Gitlab::Json.parse(line)

View File

@ -21,6 +21,12 @@ const useMockLocation = (fn) => {
afterEach(() => {
currentWindowLocation = origWindowLocation;
});
return () => {
beforeEach(() => {
currentWindowLocation = origWindowLocation;
});
};
};
/**

View File

@ -32,7 +32,7 @@ import {
} from '~/alerts_settings/utils/error_messages';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import httpStatusCodes, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import {
createHttpVariables,
updateHttpVariables,
@ -358,7 +358,7 @@ describe('AlertsSettingsWrapper', () => {
});
it('shows an error alert when integration test payload is invalid', async () => {
mock.onPost(/(.*)/).replyOnce(httpStatusCodes.UNPROCESSABLE_ENTITY);
mock.onPost(/(.*)/).replyOnce(HTTP_STATUS_UNPROCESSABLE_ENTITY);
await wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' });
expect(createAlert).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
expect(createAlert).toHaveBeenCalledTimes(1);

View File

@ -1,7 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
import Api, { DEFAULT_PER_PAGE } from '~/api';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import httpStatus, {
HTTP_STATUS_ACCEPTED,
HTTP_STATUS_CREATED,
HTTP_STATUS_NO_CONTENT,
} from '~/lib/utils/http_status';
jest.mock('~/flash');
@ -1069,7 +1073,7 @@ describe('Api', () => {
describe('when the release is successfully created', () => {
it('resolves the Promise', () => {
mock.onPost(expectedUrl, release).replyOnce(httpStatus.CREATED);
mock.onPost(expectedUrl, release).replyOnce(HTTP_STATUS_CREATED);
return Api.createRelease(dummyProjectPath, release).then(() => {
expect(mock.history.post).toHaveLength(1);
@ -1125,7 +1129,7 @@ describe('Api', () => {
describe('when the Release is successfully created', () => {
it('resolves the Promise', () => {
mock.onPost(expectedUrl, expectedLink).replyOnce(httpStatus.CREATED);
mock.onPost(expectedUrl, expectedLink).replyOnce(HTTP_STATUS_CREATED);
return Api.createReleaseLink(dummyProjectPath, dummyTagName, expectedLink).then(() => {
expect(mock.history.post).toHaveLength(1);
@ -1224,7 +1228,7 @@ describe('Api', () => {
describe('when the merge request is successfully created', () => {
it('resolves the Promise', () => {
mock.onPost(expectedUrl, options).replyOnce(httpStatus.CREATED);
mock.onPost(expectedUrl, options).replyOnce(HTTP_STATUS_CREATED);
return Api.createProjectMergeRequest(dummyProjectPath, options).then(() => {
expect(mock.history.post).toHaveLength(1);
@ -1332,7 +1336,7 @@ describe('Api', () => {
describe('when the freeze period is successfully created', () => {
it('resolves the Promise', () => {
mock.onPost(expectedUrl, options).replyOnce(httpStatus.CREATED, expectedResult);
mock.onPost(expectedUrl, options).replyOnce(HTTP_STATUS_CREATED, expectedResult);
return Api.createFreezePeriod(projectId, options).then(({ data }) => {
expect(data).toStrictEqual(expectedResult);
@ -1598,7 +1602,7 @@ describe('Api', () => {
const secureFileId = 2;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/secure_files/${secureFileId}`;
mock.onDelete(expectedUrl).reply(httpStatus.NO_CONTENT, '');
mock.onDelete(expectedUrl).reply(HTTP_STATUS_NO_CONTENT, '');
const { data } = await Api.deleteProjectSecureFile(projectId, secureFileId);
expect(data).toEqual('');
});
@ -1609,10 +1613,10 @@ describe('Api', () => {
const groupId = 1;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/dependency_proxy/cache`;
mock.onDelete(expectedUrl).reply(httpStatus.ACCEPTED);
mock.onDelete(expectedUrl).reply(HTTP_STATUS_ACCEPTED);
const { status } = await Api.deleteDependencyProxyCacheList(groupId, {});
expect(status).toBe(httpStatus.ACCEPTED);
expect(status).toBe(HTTP_STATUS_ACCEPTED);
});
});

View File

@ -4,7 +4,10 @@ import { registerCaptchaModalInterceptor } from '~/captcha/captcha_modal_axios_i
import UnsolvedCaptchaError from '~/captcha/unsolved_captcha_error';
import { waitForCaptchaToBeSolved } from '~/captcha/wait_for_captcha_to_be_solved';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import httpStatusCodes, {
HTTP_STATUS_CONFLICT,
HTTP_STATUS_METHOD_NOT_ALLOWED,
} from '~/lib/utils/http_status';
jest.mock('~/captcha/wait_for_captcha_to_be_solved');
@ -33,7 +36,7 @@ describe('registerCaptchaModalInterceptor', () => {
mock.onAny('/endpoint-with-unrelated-error').reply(404, AXIOS_RESPONSE);
mock.onAny('/endpoint-with-captcha').reply((config) => {
if (!supportedMethods.includes(config.method)) {
return [httpStatusCodes.METHOD_NOT_ALLOWED, { method: config.method }];
return [HTTP_STATUS_METHOD_NOT_ALLOWED, { method: config.method }];
}
const data = JSON.parse(config.data);
@ -46,7 +49,7 @@ describe('registerCaptchaModalInterceptor', () => {
return [httpStatusCodes.OK, { ...data, method: config.method, CAPTCHA_SUCCESS }];
}
return [httpStatusCodes.CONFLICT, NEEDS_CAPTCHA_RESPONSE];
return [HTTP_STATUS_CONFLICT, NEEDS_CAPTCHA_RESPONSE];
});
axios.interceptors.response.handlers = [];
@ -123,7 +126,7 @@ describe('registerCaptchaModalInterceptor', () => {
await expect(() => axios[method]('/endpoint-with-captcha')).rejects.toThrow(
expect.objectContaining({
response: expect.objectContaining({
status: httpStatusCodes.METHOD_NOT_ALLOWED,
status: HTTP_STATUS_METHOD_NOT_ALLOWED,
data: { method },
}),
}),

View File

@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { dismiss } from '~/feature_highlight/feature_highlight_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import httpStatusCodes, { HTTP_STATUS_CREATED } from '~/lib/utils/http_status';
jest.mock('~/flash');
@ -11,7 +11,7 @@ describe('feature highlight helper', () => {
let mockAxios;
const endpoint = '/-/callouts/dismiss';
const highlightId = '123';
const { CREATED, INTERNAL_SERVER_ERROR } = httpStatusCodes;
const { INTERNAL_SERVER_ERROR } = httpStatusCodes;
beforeEach(() => {
mockAxios = new MockAdapter(axios);
@ -22,7 +22,7 @@ describe('feature highlight helper', () => {
});
it('calls persistent dismissal endpoint with highlightId', async () => {
mockAxios.onPost(endpoint, { feature_name: highlightId }).replyOnce(CREATED);
mockAxios.onPost(endpoint, { feature_name: highlightId }).replyOnce(HTTP_STATUS_CREATED);
await expect(dismiss(endpoint, highlightId)).resolves.toEqual(expect.anything());
});

View File

@ -10,7 +10,7 @@ import {
import * as messages from '~/ide/stores/modules/terminal/messages';
import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
const TEST_PROJECT_PATH = 'lorem/root';
const TEST_BRANCH_ID = 'main';
@ -78,7 +78,7 @@ describe('IDE store terminal check actions', () => {
describe('receiveConfigCheckError', () => {
it('handles error response', () => {
const status = httpStatus.UNPROCESSABLE_ENTITY;
const status = HTTP_STATUS_UNPROCESSABLE_ENTITY;
const payload = { response: { status } };
return testAction(

View File

@ -6,7 +6,7 @@ import { STARTING, PENDING, STOPPING, STOPPED } from '~/ide/stores/modules/termi
import * as messages from '~/ide/stores/modules/terminal/messages';
import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
jest.mock('~/flash');
@ -285,7 +285,7 @@ describe('IDE store terminal session controls actions', () => {
);
});
[httpStatus.NOT_FOUND, httpStatus.UNPROCESSABLE_ENTITY].forEach((status) => {
[httpStatus.NOT_FOUND, HTTP_STATUS_UNPROCESSABLE_ENTITY].forEach((status) => {
it(`dispatches request and startSession on ${status}`, () => {
mock
.onPost(state.session.retryPath, { branch: rootState.currentBranchId, format: 'json' })

View File

@ -1,7 +1,7 @@
import { escape } from 'lodash';
import { TEST_HOST } from 'spec/test_constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
import httpStatus from '~/lib/utils/http_status';
import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import { sprintf } from '~/locale';
const TEST_HELP_URL = `${TEST_HOST}/help`;
@ -9,7 +9,7 @@ const TEST_HELP_URL = `${TEST_HOST}/help`;
describe('IDE store terminal messages', () => {
describe('configCheckError', () => {
it('returns job error, with status UNPROCESSABLE_ENTITY', () => {
const result = messages.configCheckError(httpStatus.UNPROCESSABLE_ENTITY, TEST_HELP_URL);
const result = messages.configCheckError(HTTP_STATUS_UNPROCESSABLE_ENTITY, TEST_HELP_URL);
expect(result).toBe(
sprintf(

View File

@ -24,7 +24,7 @@ import {
import eventHub from '~/invite_members/event_hub';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import httpStatus, { HTTP_STATUS_CREATED } from '~/lib/utils/http_status';
import { getParameterValues } from '~/lib/utils/url_utility';
import { GROUPS_INVITATIONS_PATH, invitationsApiResponse } from '../mock_data/api_responses';
import {
@ -467,7 +467,7 @@ describe('InviteMembersModal', () => {
describe('clearing the invalid state and message', () => {
beforeEach(async () => {
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_TAKEN);
mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_TAKEN);
clickInviteButton();
@ -526,7 +526,7 @@ describe('InviteMembersModal', () => {
});
it('displays the restricted user api message for response with bad request', async () => {
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
clickInviteButton();
@ -539,7 +539,7 @@ describe('InviteMembersModal', () => {
});
it('displays all errors when there are multiple existing users that are restricted by email', async () => {
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
clickInviteButton();
@ -636,7 +636,7 @@ describe('InviteMembersModal', () => {
});
it('displays the restricted email error when restricted email is invited', async () => {
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
clickInviteButton();
@ -650,7 +650,7 @@ describe('InviteMembersModal', () => {
});
it('displays all errors when there are multiple emails that return a restricted error message', async () => {
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
clickInviteButton();
@ -701,7 +701,7 @@ describe('InviteMembersModal', () => {
createInviteMembersToGroupWrapper();
await triggerMembersTokenSelect([user3, user4, user5, user6]);
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EXPANDED_RESTRICTED);
mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EXPANDED_RESTRICTED);
clickInviteButton();

View File

@ -1,7 +1,7 @@
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import httpStatusCodes, { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import pollUntilComplete from '~/lib/utils/poll_until_complete';
const endpoint = `${TEST_HOST}/foo`;
@ -37,7 +37,7 @@ describe('pollUntilComplete', () => {
beforeEach(() => {
mock
.onGet(endpoint)
.replyOnce(httpStatusCodes.NO_CONTENT, undefined, pollIntervalHeader)
.replyOnce(HTTP_STATUS_NO_CONTENT, undefined, pollIntervalHeader)
.onGet(endpoint)
.replyOnce(httpStatusCodes.OK, mockData);
});

View File

@ -2,7 +2,10 @@ import MockAdapter from 'axios-mock-adapter';
import { backoffMockImplementation } from 'helpers/backoff_helper';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import statusCodes from '~/lib/utils/http_status';
import statusCodes, {
HTTP_STATUS_NO_CONTENT,
HTTP_STATUS_UNPROCESSABLE_ENTITY,
} from '~/lib/utils/http_status';
import { getDashboard, getPrometheusQueryData } from '~/monitoring/requests';
import { metricsDashboardResponse } from '../fixture_data';
@ -37,8 +40,8 @@ describe('monitoring metrics_requests', () => {
});
it('returns a dashboard response after retrying twice', () => {
mock.onGet(dashboardEndpoint).replyOnce(statusCodes.NO_CONTENT);
mock.onGet(dashboardEndpoint).replyOnce(statusCodes.NO_CONTENT);
mock.onGet(dashboardEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
mock.onGet(dashboardEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
mock.onGet(dashboardEndpoint).reply(statusCodes.OK, response);
return getDashboard(dashboardEndpoint, params).then((data) => {
@ -81,8 +84,8 @@ describe('monitoring metrics_requests', () => {
it('returns a dashboard response after retrying twice', () => {
// Mock multiple attempts while the cache is filling up
mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
mock.onGet(prometheusEndpoint).reply(statusCodes.OK, response); // 3rd attempt
return getPrometheusQueryData(prometheusEndpoint, params).then((data) => {
@ -116,8 +119,8 @@ describe('monitoring metrics_requests', () => {
it('rejects after retrying twice and getting an HTTP 500 error', () => {
// Mock multiple attempts while the cache is filling up and fails
mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
mock.onGet(prometheusEndpoint).reply(500, {
status: 'error',
error: 'An error occurred',
@ -132,7 +135,7 @@ describe('monitoring metrics_requests', () => {
it.each`
code | reason
${statusCodes.BAD_REQUEST} | ${'Parameters are missing or incorrect'}
${statusCodes.UNPROCESSABLE_ENTITY} | ${"Expression can't be executed"}
${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${"Expression can't be executed"}
${statusCodes.SERVICE_UNAVAILABLE} | ${'Query timed out or aborted'}
`('rejects with details: "$reason" after getting an HTTP $code error', ({ code, reason }) => {
mock.onGet(prometheusEndpoint).reply(code, {

View File

@ -4,7 +4,10 @@ import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import statusCodes from '~/lib/utils/http_status';
import statusCodes, {
HTTP_STATUS_CREATED,
HTTP_STATUS_UNPROCESSABLE_ENTITY,
} from '~/lib/utils/http_status';
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
import getAnnotations from '~/monitoring/queries/get_annotations.query.graphql';
@ -944,7 +947,7 @@ describe('Monitoring store actions', () => {
});
it('Succesful POST request resolves', async () => {
mock.onPost(state.dashboardsEndpoint).reply(statusCodes.CREATED, {
mock.onPost(state.dashboardsEndpoint).reply(HTTP_STATUS_CREATED, {
dashboard: dashboardGitResponse[1],
});
@ -969,7 +972,7 @@ describe('Monitoring store actions', () => {
commit_message: 'A new commit message',
});
mock.onPost(state.dashboardsEndpoint).reply(statusCodes.CREATED, {
mock.onPost(state.dashboardsEndpoint).reply(HTTP_STATUS_CREATED, {
dashboard: mockCreatedDashboard,
});
@ -1133,7 +1136,7 @@ describe('Monitoring store actions', () => {
mock
.onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent })
.reply(statusCodes.UNPROCESSABLE_ENTITY, {
.reply(HTTP_STATUS_UNPROCESSABLE_ENTITY, {
message: mockErrorMsg,
});

View File

@ -1,7 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import statusCodes from '~/lib/utils/http_status';
import statusCodes, { HTTP_STATUS_ACCEPTED } from '~/lib/utils/http_status';
import * as actions from '~/self_monitor/store/actions';
import * as types from '~/self_monitor/store/mutation_types';
import createState from '~/self_monitor/store/state';
@ -44,7 +44,7 @@ describe('self-monitor actions', () => {
beforeEach(() => {
state.createProjectEndpoint = '/create';
state.createProjectStatusEndpoint = '/create_status';
mock.onPost(state.createProjectEndpoint).reply(statusCodes.ACCEPTED, {
mock.onPost(state.createProjectEndpoint).reply(HTTP_STATUS_ACCEPTED, {
job_id: '123',
});
mock.onGet(state.createProjectStatusEndpoint).reply(statusCodes.OK, {
@ -151,7 +151,7 @@ describe('self-monitor actions', () => {
beforeEach(() => {
state.deleteProjectEndpoint = '/delete';
state.deleteProjectStatusEndpoint = '/delete-status';
mock.onDelete(state.deleteProjectEndpoint).reply(statusCodes.ACCEPTED, {
mock.onDelete(state.deleteProjectEndpoint).reply(HTTP_STATUS_ACCEPTED, {
job_id: '456',
});
mock.onGet(state.deleteProjectStatusEndpoint).reply(statusCodes.OK, {

View File

@ -8,7 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
import httpStatusCodes from '~/lib/utils/http_status';
import httpStatusCodes, { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
import { failedReport } from 'jest/ci/reports/mock_data/mock_data';
@ -82,7 +82,7 @@ describe('Test report extension', () => {
});
it('with a 204 response, continues to display loading state', async () => {
mockApi(httpStatusCodes.NO_CONTENT, '');
mockApi(HTTP_STATUS_NO_CONTENT, '');
createComponent();
await waitForPromises();

View File

@ -7,7 +7,7 @@ import axios from '~/lib/utils/axios_utils';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
import codeQualityExtension from '~/vue_merge_request_widget/extensions/code_quality';
import httpStatusCodes from '~/lib/utils/http_status';
import httpStatusCodes, { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import { i18n } from '~/vue_merge_request_widget/extensions/code_quality/constants';
import {
codeQualityResponseNewErrors,
@ -63,7 +63,7 @@ describe('Code Quality extension', () => {
});
it('with a 204 response, continues to display loading state', async () => {
mockApi(httpStatusCodes.NO_CONTENT, '');
mockApi(HTTP_STATUS_NO_CONTENT, '');
createComponent();
await waitForPromises();

View File

@ -1,4 +1,4 @@
import { GlLabel, GlAvatarsInline } from '@gitlab/ui';
import { GlIcon, GlLabel, GlAvatarsInline } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@ -12,6 +12,7 @@ describe('WorkItemLinkChildMetadata', () => {
const createComponent = ({
allowsScopedLabels = true,
progress = 10,
milestone = mockMilestone,
assignees = mockAssignees,
labels = mockLabels,
@ -19,6 +20,7 @@ describe('WorkItemLinkChildMetadata', () => {
wrapper = shallowMountExtended(WorkItemLinkChildMetadata, {
propsData: {
allowsScopedLabels,
progress,
milestone,
assignees,
labels,
@ -30,7 +32,16 @@ describe('WorkItemLinkChildMetadata', () => {
createComponent();
});
it('renders milestone link button', () => {
it('renders item progress', () => {
const progressEl = wrapper.findByTestId('item-progress');
expect(progressEl.exists()).toBe(true);
expect(progressEl.attributes('title')).toBe('Progress');
expect(progressEl.text().trim()).toBe('10%');
expect(progressEl.findComponent(GlIcon).props('name')).toBe('progress');
});
it('renders item milestone', () => {
const milestoneLink = wrapper.findComponent(ItemMilestone);
expect(milestoneLink.exists()).toBe(true);

View File

@ -149,6 +149,7 @@ describe('WorkItemLinkChild', () => {
expect(metadataEl.exists()).toBe(true);
expect(metadataEl.props()).toMatchObject({
allowsScopedLabels: true,
progress: 10,
milestone: mockMilestone,
assignees: mockAssignees,
labels: mockLabels,

View File

@ -968,6 +968,11 @@ export const workItemObjectiveWithChild = {
},
__typename: 'WorkItemWidgetHierarchy',
},
{
type: 'PROGRESS',
__typename: 'WorkItemWidgetProgress',
progress: 10,
},
{
type: 'MILESTONE',
__typename: 'WorkItemWidgetMilestone',

View File

@ -14,6 +14,8 @@ RSpec.describe TodosHelper do
note: 'I am note, hear me roar')
end
let_it_be(:group) { create(:group, :public, name: 'Group 1') }
let_it_be(:design_todo) do
create(:todo, :mentioned,
user: user,
@ -37,6 +39,10 @@ RSpec.describe TodosHelper do
create(:todo, target: issue)
end
let_it_be(:group_todo) do
create(:todo, target: group)
end
describe '#todos_count_format' do
it 'shows fuzzy count for 100 or more items' do
expect(helper.todos_count_format(100)).to eq '99+'
@ -155,9 +161,7 @@ RSpec.describe TodosHelper do
end
context 'when a user requests access to group' do
let(:group) { create(:group, :public) }
let(:group_access_request_todo) do
let_it_be(:group_access_request_todo) do
create(:todo,
target_id: group.id,
target_type: group.class.polymorphic_name,
@ -358,7 +362,6 @@ RSpec.describe TodosHelper do
Todo::APPROVAL_REQUIRED | false | format(s_("Todos|set %{who} as an approver"), who: _('you'))
Todo::UNMERGEABLE | true | s_('Todos|Could not merge')
Todo::MERGE_TRAIN_REMOVED | true | s_("Todos|Removed from Merge Train")
Todo::MEMBER_ACCESS_REQUESTED | true | s_("Todos|has requested access")
end
with_them do
@ -369,6 +372,18 @@ RSpec.describe TodosHelper do
it { expect(helper.todo_action_name(alert_todo)).to eq(expected_action_name) }
end
context 'member access requested' do
context 'when source is group' do
it 'returns group access message' do
group_todo.action = Todo::MEMBER_ACCESS_REQUESTED
expect(helper.todo_action_name(group_todo)).to eq(
format(s_("Todos|has requested access to group %{which}"), which: _(group.name))
)
end
end
end
end
describe '#todo_due_date' do

View File

@ -61,7 +61,7 @@ RSpec.describe Backup::GitalyBackup do
it 'erases any existing repository backups' do
existing_file = File.join(destination, 'some_existing_file')
IO.write(existing_file, "Some existing file.\n")
File.write(existing_file, "Some existing file.\n")
subject.start(:create, destination, backup_id: backup_id)
subject.finish!

View File

@ -77,7 +77,7 @@ RSpec.describe Gitlab::ImportExport::Group::LegacyTreeRestorer do
let(:group) { create(:group) }
let(:shared) { Gitlab::ImportExport::Shared.new(group) }
let(:group_tree_restorer) { described_class.new(user: importer_user, shared: shared, group: group, group_hash: nil) }
let(:group_json) { Gitlab::Json.parse(IO.read(File.join(shared.export_path, 'group.json'))) }
let(:group_json) { Gitlab::Json.parse(File.read(File.join(shared.export_path, 'group.json'))) }
shared_examples 'excluded attributes' do
excluded_attributes = %w[

View File

@ -154,6 +154,6 @@ RSpec.describe Gitlab::ImportExport::Group::LegacyTreeSaver do
end
def group_json(filename)
::JSON.parse(IO.read(filename))
::JSON.parse(File.read(filename))
end
end

View File

@ -112,7 +112,7 @@ RSpec.describe Gitlab::ImportExport::Group::TreeRestorer, feature: :subgroups do
let(:shared) { Gitlab::ImportExport::Shared.new(group) }
let(:group_tree_restorer) { described_class.new(user: importer_user, shared: shared, group: group) }
let(:exported_file) { File.join(shared.export_path, 'tree/groups/4352.json') }
let(:group_json) { Gitlab::Json.parse(IO.read(exported_file)) }
let(:group_json) { Gitlab::Json.parse(File.read(exported_file)) }
shared_examples 'excluded attributes' do
excluded_attributes = %w[

View File

@ -86,7 +86,7 @@ RSpec.describe 'Test coverage of the Project Import' do
end
def relations_from_json(json_file)
json = Gitlab::Json.parse(IO.read(json_file))
json = Gitlab::Json.parse(File.read(json_file))
[].tap { |res| gather_relations({ project: json }, res, []) }
.map { |relation_names| relation_names.join('.') }

View File

@ -96,6 +96,6 @@ RSpec.describe Gitlab::ImportExport::Json::LegacyWriter do
def subject_json
subject.close
::JSON.parse(IO.read(subject.path))
::JSON.parse(File.read(subject.path))
end
end

View File

@ -26,7 +26,7 @@ RSpec.describe Gitlab::ImportExport::LfsSaver do
let(:lfs_json_file) { File.join(shared.export_path, Gitlab::ImportExport.lfs_objects_filename) }
def lfs_json
Gitlab::Json.parse(IO.read(lfs_json_file))
Gitlab::Json.parse(File.read(lfs_json_file))
end
before do

View File

@ -111,7 +111,7 @@ RSpec.describe Gitlab::ImportExport::Project::RelationSaver do
end
def read_json(path)
Gitlab::Json.parse(IO.read(path))
Gitlab::Json.parse(File.read(path))
end
def read_ndjson(path)

View File

@ -2816,7 +2816,7 @@ RSpec.describe Group do
end
describe 'has_project_with_service_desk_enabled?' do
let_it_be(:group) { create(:group, :private) }
let_it_be_with_refind(:group) { create(:group, :private) }
subject { group.has_project_with_service_desk_enabled? }

View File

@ -31,7 +31,7 @@ RSpec.describe WebHook do
it { is_expected.to allow_value({ 'MY_TOKEN' => 'bar' }).for(:url_variables) }
it { is_expected.to allow_value({ 'foo2' => 'bar' }).for(:url_variables) }
it { is_expected.to allow_value({ 'x' => 'y' }).for(:url_variables) }
it { is_expected.to allow_value({ 'x' => ('a' * 100) }).for(:url_variables) }
it { is_expected.to allow_value({ 'x' => ('a' * 2048) }).for(:url_variables) }
it { is_expected.to allow_value({ 'foo' => 'bar', 'bar' => 'baz' }).for(:url_variables) }
it { is_expected.to allow_value((1..20).to_h { ["k#{_1}", 'value'] }).for(:url_variables) }
it { is_expected.to allow_value({ 'MY-TOKEN' => 'bar' }).for(:url_variables) }
@ -45,7 +45,7 @@ RSpec.describe WebHook do
it { is_expected.not_to allow_value({ 'bar' => :baz }).for(:url_variables) }
it { is_expected.not_to allow_value({ 'bar' => nil }).for(:url_variables) }
it { is_expected.not_to allow_value({ 'foo' => '' }).for(:url_variables) }
it { is_expected.not_to allow_value({ 'foo' => ('a' * 101) }).for(:url_variables) }
it { is_expected.not_to allow_value({ 'foo' => ('a' * 2049) }).for(:url_variables) }
it { is_expected.not_to allow_value({ 'has spaces' => 'foo' }).for(:url_variables) }
it { is_expected.not_to allow_value({ '' => 'foo' }).for(:url_variables) }
it { is_expected.not_to allow_value({ '1foo' => 'foo' }).for(:url_variables) }

View File

@ -56,6 +56,15 @@ RSpec.describe Todo do
expect(subject.body).to eq 'quick fix'
end
it 'returns full path of target when action is member_access_requested' do
group = create(:group)
subject.target = group
subject.action = Todo::MEMBER_ACCESS_REQUESTED
expect(subject.body).to eq group.full_path
end
end
describe '#done' do
@ -182,6 +191,17 @@ RSpec.describe Todo do
expect(subject.target_reference).to eq issue.to_reference(full: false)
end
context 'when target is member access requested' do
it 'returns group full path' do
group = create(:group)
subject.target = group
subject.action = Todo::MEMBER_ACCESS_REQUESTED
expect(subject.target_reference).to eq group.full_path
end
end
end
describe '#self_added?' do

View File

@ -15,6 +15,7 @@ RSpec.describe API::Todos, feature_category: :source_code_management do
let_it_be(:work_item) { create(:work_item, :task, project: project_1) }
let_it_be(:merge_request) { create(:merge_request, source_project: project_1) }
let_it_be(:alert) { create(:alert_management_alert, project: project_1) }
let_it_be(:group_request_todo) { create(:todo, author: author_1, user: john_doe, target: group, action: Todo::MEMBER_ACCESS_REQUESTED) }
let_it_be(:alert_todo) { create(:todo, project: project_1, author: john_doe, user: john_doe, target: alert) }
let_it_be(:merge_request_todo) { create(:todo, project: project_1, author: author_2, user: john_doe, target: merge_request) }
let_it_be(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe, target: issue) }
@ -71,7 +72,7 @@ RSpec.describe API::Todos, feature_category: :source_code_management do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(6)
expect(json_response.length).to eq(7)
expect(json_response[0]).to include(
'id' => pending_5.id,
@ -127,6 +128,17 @@ RSpec.describe API::Todos, feature_category: :source_code_management do
'title' => alert.title
)
)
expect(json_response[6]).to include(
'target_type' => 'Namespace',
'action_name' => 'member_access_requested',
'target' => hash_including(
'id' => group.id,
'name' => group.name,
'full_path' => group.full_path
),
'target_url' => Gitlab::Routing.url_helpers.group_group_members_url(group, tab: 'access_requests')
)
end
context "when current user does not have access to one of the TODO's target" do
@ -137,7 +149,7 @@ RSpec.describe API::Todos, feature_category: :source_code_management do
get api('/todos', john_doe)
expect(json_response.count).to eq(6)
expect(json_response.count).to eq(7)
expect(json_response.map { |t| t['id'] }).not_to include(no_access_todo.id, pending_4.id)
end
end
@ -231,7 +243,7 @@ RSpec.describe API::Todos, feature_category: :source_code_management do
create(:on_commit_todo, project: new_todo.project, author: author_1, user: john_doe, target: merge_request_3)
create(:todo, project: new_todo.project, author: author_2, user: john_doe, target: merge_request_3)
expect { get api('/todos', john_doe) }.not_to exceed_query_limit(control1).with_threshold(5)
expect { get api('/todos', john_doe) }.not_to exceed_query_limit(control1).with_threshold(6)
control2 = ActiveRecord::QueryRecorder.new { get api('/todos', john_doe) }
create_issue_todo_for(john_doe)

View File

@ -205,7 +205,7 @@ module GitalySetup
# This code needs to work in an environment where we cannot use bundler,
# so we cannot easily use the toml-rb gem. This ad-hoc parser should be
# good enough.
config_text = IO.read(toml)
config_text = File.read(toml)
config_text.lines.each do |line|
match_data = line.match(/^\s*(socket_path|listen_addr)\s*=\s*"([^"]*)"$/)

View File

@ -83,7 +83,7 @@ module ImportExport
path = File.join(dir_path, "#{exportable_path}.json")
return unless File.exist?(path)
Gitlab::Json.parse(IO.read(path))
Gitlab::Json.parse(File.read(path))
end
def consume_relations(dir_path, exportable_path, key)
@ -101,7 +101,7 @@ module ImportExport
end
def project_json(filename)
Gitlab::Json.parse(IO.read(filename))
Gitlab::Json.parse(File.read(filename))
end
end
end