Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6a38034714
commit
05db4ead6d
|
@ -534,6 +534,50 @@ rspec:feature-flags:
|
||||||
run_timed_command "bundle exec scripts/used-feature-flags";
|
run_timed_command "bundle exec scripts/used-feature-flags";
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
rspec:skipped-flaky-tests-report:
|
||||||
|
extends:
|
||||||
|
- .default-retry
|
||||||
|
- .rails:rules:skipped-flaky-tests-report
|
||||||
|
image: ruby:2.7-alpine
|
||||||
|
stage: post-test
|
||||||
|
# We cannot use needs since it would mean needing 84 jobs (since most are parallelized)
|
||||||
|
# so we use `dependencies` here.
|
||||||
|
dependencies:
|
||||||
|
# FOSS/EE jobs
|
||||||
|
- rspec migration pg12
|
||||||
|
- rspec unit pg12
|
||||||
|
- rspec integration pg12
|
||||||
|
- rspec system pg12
|
||||||
|
# FOSS/EE minimal jobs
|
||||||
|
- rspec migration pg12 minimal
|
||||||
|
- rspec unit pg12 minimal
|
||||||
|
- rspec integration pg12 minimal
|
||||||
|
- rspec system pg12 minimal
|
||||||
|
# EE jobs
|
||||||
|
- rspec-ee migration pg12
|
||||||
|
- rspec-ee unit pg12
|
||||||
|
- rspec-ee integration pg12
|
||||||
|
- rspec-ee system pg12
|
||||||
|
# EE minimal jobs
|
||||||
|
- rspec-ee migration pg12 minimal
|
||||||
|
- rspec-ee unit pg12 minimal
|
||||||
|
- rspec-ee integration pg12 minimal
|
||||||
|
- rspec-ee system pg12 minimal
|
||||||
|
# Geo jobs
|
||||||
|
- rspec-ee unit pg12 geo
|
||||||
|
- rspec-ee integration pg12 geo
|
||||||
|
- rspec-ee system pg12 geo
|
||||||
|
# Geo minimal jobs
|
||||||
|
- rspec-ee unit pg12 geo minimal
|
||||||
|
- rspec-ee integration pg12 geo minimal
|
||||||
|
- rspec-ee system pg12 geo minimal
|
||||||
|
script:
|
||||||
|
- cat rspec_flaky/skipped_flaky_tests_*_report.txt >> skipped_flaky_tests_report.txt
|
||||||
|
artifacts:
|
||||||
|
expire_in: 31d
|
||||||
|
paths:
|
||||||
|
- skipped_flaky_tests_report.txt
|
||||||
|
|
||||||
# EE/FOSS: default refs (MRs, default branch, schedules) jobs #
|
# EE/FOSS: default refs (MRs, default branch, schedules) jobs #
|
||||||
#######################################################
|
#######################################################
|
||||||
|
|
||||||
|
|
|
@ -1352,6 +1352,13 @@
|
||||||
when: never
|
when: never
|
||||||
- changes: *code-backstage-patterns
|
- changes: *code-backstage-patterns
|
||||||
|
|
||||||
|
.rails:rules:skipped-flaky-tests-report:
|
||||||
|
rules:
|
||||||
|
- <<: *if-not-ee
|
||||||
|
when: never
|
||||||
|
- if: '$SKIP_FLAKY_TESTS_AUTOMATICALLY == "true"'
|
||||||
|
changes: *code-backstage-patterns
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
# Static analysis rules #
|
# Static analysis rules #
|
||||||
#########################
|
#########################
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
7b9cd199b0851fd1b6615e0798f2aafddafd63cb
|
460a880c6993ab5f76cac951fccc02efd5cbd444
|
||||||
|
|
2
Gemfile
2
Gemfile
|
@ -342,7 +342,7 @@ group :development do
|
||||||
gem 'lefthook', '~> 0.7.0', require: false
|
gem 'lefthook', '~> 0.7.0', require: false
|
||||||
gem 'solargraph', '~> 0.43', require: false
|
gem 'solargraph', '~> 0.43', require: false
|
||||||
|
|
||||||
gem 'letter_opener_web', '~> 1.4.1'
|
gem 'letter_opener_web', '~> 2.0.0'
|
||||||
|
|
||||||
# Better errors handler
|
# Better errors handler
|
||||||
gem 'better_errors', '~> 2.9.0'
|
gem 'better_errors', '~> 2.9.0'
|
||||||
|
|
11
Gemfile.lock
11
Gemfile.lock
|
@ -700,10 +700,11 @@ GEM
|
||||||
lefthook (0.7.5)
|
lefthook (0.7.5)
|
||||||
letter_opener (1.7.0)
|
letter_opener (1.7.0)
|
||||||
launchy (~> 2.2)
|
launchy (~> 2.2)
|
||||||
letter_opener_web (1.4.1)
|
letter_opener_web (2.0.0)
|
||||||
actionmailer (>= 3.2)
|
actionmailer (>= 5.2)
|
||||||
letter_opener (~> 1.0)
|
letter_opener (~> 1.7)
|
||||||
railties (>= 3.2)
|
railties (>= 5.2)
|
||||||
|
rexml
|
||||||
libyajl2 (1.2.0)
|
libyajl2 (1.2.0)
|
||||||
license_finder (6.0.0)
|
license_finder (6.0.0)
|
||||||
bundler
|
bundler
|
||||||
|
@ -1516,7 +1517,7 @@ DEPENDENCIES
|
||||||
kramdown (~> 2.3.1)
|
kramdown (~> 2.3.1)
|
||||||
kubeclient (~> 4.9.2)
|
kubeclient (~> 4.9.2)
|
||||||
lefthook (~> 0.7.0)
|
lefthook (~> 0.7.0)
|
||||||
letter_opener_web (~> 1.4.1)
|
letter_opener_web (~> 2.0.0)
|
||||||
license_finder (~> 6.0)
|
license_finder (~> 6.0)
|
||||||
licensee (~> 9.14.1)
|
licensee (~> 9.14.1)
|
||||||
lockbox (~> 0.6.2)
|
lockbox (~> 0.6.2)
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
query GroupBoardIterations($fullPath: ID!, $title: String) {
|
|
||||||
group(fullPath: $fullPath) {
|
|
||||||
iterations(includeAncestors: true, title: $title) {
|
|
||||||
nodes {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
query ProjectBoardIterations($fullPath: ID!, $title: String) {
|
|
||||||
project(fullPath: $fullPath) {
|
|
||||||
iterations(includeAncestors: true, title: $title) {
|
|
||||||
nodes {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -36,13 +36,11 @@ import {
|
||||||
} from '../boards_util';
|
} from '../boards_util';
|
||||||
import { gqlClient } from '../graphql';
|
import { gqlClient } from '../graphql';
|
||||||
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
|
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
|
||||||
import groupBoardIterationsQuery from '../graphql/group_board_iterations.query.graphql';
|
|
||||||
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
|
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
|
||||||
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
|
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
|
||||||
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
|
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
|
||||||
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
|
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
|
||||||
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
|
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
|
||||||
import projectBoardIterationsQuery from '../graphql/project_board_iterations.query.graphql';
|
|
||||||
import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
|
import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
|
||||||
|
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
|
@ -203,52 +201,6 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchIterations({ state, commit }, title) {
|
|
||||||
commit(types.RECEIVE_ITERATIONS_REQUEST);
|
|
||||||
|
|
||||||
const { fullPath, boardType } = state;
|
|
||||||
|
|
||||||
const variables = {
|
|
||||||
fullPath,
|
|
||||||
title,
|
|
||||||
};
|
|
||||||
|
|
||||||
let query;
|
|
||||||
if (boardType === BoardType.project) {
|
|
||||||
query = projectBoardIterationsQuery;
|
|
||||||
}
|
|
||||||
if (boardType === BoardType.group) {
|
|
||||||
query = groupBoardIterationsQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!query) {
|
|
||||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
|
||||||
throw new Error('Unknown board type');
|
|
||||||
}
|
|
||||||
|
|
||||||
return gqlClient
|
|
||||||
.query({
|
|
||||||
query,
|
|
||||||
variables,
|
|
||||||
})
|
|
||||||
.then(({ data }) => {
|
|
||||||
const errors = data[boardType]?.errors;
|
|
||||||
const iterations = data[boardType]?.iterations.nodes;
|
|
||||||
|
|
||||||
if (errors?.[0]) {
|
|
||||||
throw new Error(errors[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
commit(types.RECEIVE_ITERATIONS_SUCCESS, iterations);
|
|
||||||
|
|
||||||
return iterations;
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
commit(types.RECEIVE_ITERATIONS_FAILURE);
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchMilestones({ state, commit }, searchTerm) {
|
fetchMilestones({ state, commit }, searchTerm) {
|
||||||
commit(types.RECEIVE_MILESTONES_REQUEST);
|
commit(types.RECEIVE_MILESTONES_REQUEST);
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,3 @@ export const ADD_LIST_TO_HIGHLIGHTED_LISTS = 'ADD_LIST_TO_HIGHLIGHTED_LISTS';
|
||||||
export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS';
|
export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS';
|
||||||
export const RESET_BOARD_ITEM_SELECTION = 'RESET_BOARD_ITEM_SELECTION';
|
export const RESET_BOARD_ITEM_SELECTION = 'RESET_BOARD_ITEM_SELECTION';
|
||||||
export const SET_ERROR = 'SET_ERROR';
|
export const SET_ERROR = 'SET_ERROR';
|
||||||
|
|
||||||
export const RECEIVE_ITERATIONS_REQUEST = 'RECEIVE_ITERATIONS_REQUEST';
|
|
||||||
export const RECEIVE_ITERATIONS_SUCCESS = 'RECEIVE_ITERATIONS_SUCCESS';
|
|
||||||
export const RECEIVE_ITERATIONS_FAILURE = 'RECEIVE_ITERATIONS_FAILURE';
|
|
||||||
|
|
|
@ -64,20 +64,6 @@ export default {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
[mutationTypes.RECEIVE_ITERATIONS_REQUEST](state) {
|
|
||||||
state.iterationsLoading = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
[mutationTypes.RECEIVE_ITERATIONS_SUCCESS](state, iterations) {
|
|
||||||
state.iterations = iterations;
|
|
||||||
state.iterationsLoading = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
[mutationTypes.RECEIVE_ITERATIONS_FAILURE](state) {
|
|
||||||
state.iterationsLoading = false;
|
|
||||||
state.error = __('Failed to load iterations.');
|
|
||||||
},
|
|
||||||
|
|
||||||
[mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) {
|
[mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) {
|
||||||
state.activeId = id;
|
state.activeId = id;
|
||||||
state.sidebarType = sidebarType;
|
state.sidebarType = sidebarType;
|
||||||
|
|
|
@ -44,6 +44,7 @@ import {
|
||||||
TRACKING_MULTIPLE_FILES_MODE,
|
TRACKING_MULTIPLE_FILES_MODE,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
|
|
||||||
|
import { discussionIntersectionObserverHandlerFactory } from '../utils/discussions';
|
||||||
import diffsEventHub from '../event_hub';
|
import diffsEventHub from '../event_hub';
|
||||||
import { reviewStatuses } from '../utils/file_reviews';
|
import { reviewStatuses } from '../utils/file_reviews';
|
||||||
import { diffsApp } from '../utils/performance';
|
import { diffsApp } from '../utils/performance';
|
||||||
|
@ -86,6 +87,9 @@ export default {
|
||||||
ALERT_MERGE_CONFLICT,
|
ALERT_MERGE_CONFLICT,
|
||||||
ALERT_COLLAPSED_FILES,
|
ALERT_COLLAPSED_FILES,
|
||||||
},
|
},
|
||||||
|
provide: {
|
||||||
|
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
endpoint: {
|
endpoint: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
function normalize(processable) {
|
||||||
|
const { entry } = processable;
|
||||||
|
const offset = entry.rootBounds.bottom - entry.boundingClientRect.top;
|
||||||
|
const direction =
|
||||||
|
offset < 0 ? 'Up' : 'Down'; /* eslint-disable-line @gitlab/require-i18n-strings */
|
||||||
|
|
||||||
|
return {
|
||||||
|
...processable,
|
||||||
|
entry: {
|
||||||
|
time: entry.time,
|
||||||
|
type: entry.isIntersecting ? 'intersection' : `scroll${direction}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sort({ entry: alpha }, { entry: beta }) {
|
||||||
|
const diff = alpha.time - beta.time;
|
||||||
|
let order = 0;
|
||||||
|
|
||||||
|
if (diff < 0) {
|
||||||
|
order = -1;
|
||||||
|
} else if (diff > 0) {
|
||||||
|
order = 1;
|
||||||
|
} else if (alpha.type === 'intersection' && beta.type === 'scrollUp') {
|
||||||
|
order = 2;
|
||||||
|
} else if (alpha.type === 'scrollUp' && beta.type === 'intersection') {
|
||||||
|
order = -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filter(entry) {
|
||||||
|
return entry.type !== 'scrollDown';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function discussionIntersectionObserverHandlerFactory() {
|
||||||
|
let unprocessed = [];
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
return (processable) => {
|
||||||
|
unprocessed.push(processable);
|
||||||
|
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
unprocessed
|
||||||
|
.map(normalize)
|
||||||
|
.filter(filter)
|
||||||
|
.sort(sort)
|
||||||
|
.forEach((discussionObservationContainer) => {
|
||||||
|
const {
|
||||||
|
entry: { type },
|
||||||
|
currentDiscussion,
|
||||||
|
isFirstUnresolved,
|
||||||
|
isDiffsPage,
|
||||||
|
functions: { setCurrentDiscussionId, getPreviousUnresolvedDiscussionId },
|
||||||
|
} = discussionObservationContainer;
|
||||||
|
|
||||||
|
if (type === 'intersection') {
|
||||||
|
setCurrentDiscussionId(currentDiscussion.id);
|
||||||
|
} else if (type === 'scrollUp') {
|
||||||
|
setCurrentDiscussionId(
|
||||||
|
isFirstUnresolved
|
||||||
|
? null
|
||||||
|
: getPreviousUnresolvedDiscussionId(currentDiscussion.id, isDiffsPage),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
unprocessed = [];
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters, mapActions } from 'vuex';
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
|
import { GlIntersectionObserver } from '@gitlab/ui';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
|
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
|
||||||
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
|
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
|
||||||
|
@ -16,7 +17,9 @@ export default {
|
||||||
ToggleRepliesWidget,
|
ToggleRepliesWidget,
|
||||||
NoteEditedText,
|
NoteEditedText,
|
||||||
DiscussionNotesRepliesWrapper,
|
DiscussionNotesRepliesWrapper,
|
||||||
|
GlIntersectionObserver,
|
||||||
},
|
},
|
||||||
|
inject: ['discussionObserverHandler'],
|
||||||
props: {
|
props: {
|
||||||
discussion: {
|
discussion: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -54,7 +57,11 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['userCanReply']),
|
...mapGetters([
|
||||||
|
'userCanReply',
|
||||||
|
'previousUnresolvedDiscussionId',
|
||||||
|
'firstUnresolvedDiscussionId',
|
||||||
|
]),
|
||||||
hasReplies() {
|
hasReplies() {
|
||||||
return Boolean(this.replies.length);
|
return Boolean(this.replies.length);
|
||||||
},
|
},
|
||||||
|
@ -77,9 +84,20 @@ export default {
|
||||||
url: this.discussion.discussion_path,
|
url: this.discussion.discussion_path,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
isFirstUnresolved() {
|
||||||
|
return this.firstUnresolvedDiscussionId === this.discussion.id;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
observerOptions: {
|
||||||
|
threshold: 0,
|
||||||
|
rootMargin: '0px 0px -50% 0px',
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['toggleDiscussion', 'setSelectedCommentPositionHover']),
|
...mapActions([
|
||||||
|
'toggleDiscussion',
|
||||||
|
'setSelectedCommentPositionHover',
|
||||||
|
'setCurrentDiscussionId',
|
||||||
|
]),
|
||||||
componentName(note) {
|
componentName(note) {
|
||||||
if (note.isPlaceholderNote) {
|
if (note.isPlaceholderNote) {
|
||||||
if (note.placeholderType === SYSTEM_NOTE) {
|
if (note.placeholderType === SYSTEM_NOTE) {
|
||||||
|
@ -110,6 +128,18 @@ export default {
|
||||||
this.setSelectedCommentPositionHover();
|
this.setSelectedCommentPositionHover();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
observerTriggered(entry) {
|
||||||
|
this.discussionObserverHandler({
|
||||||
|
entry,
|
||||||
|
isFirstUnresolved: this.isFirstUnresolved,
|
||||||
|
currentDiscussion: { ...this.discussion },
|
||||||
|
isDiffsPage: !this.isOverviewTab,
|
||||||
|
functions: {
|
||||||
|
setCurrentDiscussionId: this.setCurrentDiscussionId,
|
||||||
|
getPreviousUnresolvedDiscussionId: this.previousUnresolvedDiscussionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -122,33 +152,35 @@ export default {
|
||||||
@mouseleave="handleMouseLeave(discussion)"
|
@mouseleave="handleMouseLeave(discussion)"
|
||||||
>
|
>
|
||||||
<template v-if="shouldGroupReplies">
|
<template v-if="shouldGroupReplies">
|
||||||
<component
|
<gl-intersection-observer :options="$options.observerOptions" @update="observerTriggered">
|
||||||
:is="componentName(firstNote)"
|
<component
|
||||||
:note="componentData(firstNote)"
|
:is="componentName(firstNote)"
|
||||||
:line="line || diffLine"
|
:note="componentData(firstNote)"
|
||||||
:discussion-file="discussion.diff_file"
|
:line="line || diffLine"
|
||||||
:commit="commit"
|
:discussion-file="discussion.diff_file"
|
||||||
:help-page-path="helpPagePath"
|
:commit="commit"
|
||||||
:show-reply-button="userCanReply"
|
:help-page-path="helpPagePath"
|
||||||
:discussion-root="true"
|
:show-reply-button="userCanReply"
|
||||||
:discussion-resolve-path="discussion.resolve_path"
|
:discussion-root="true"
|
||||||
:is-overview-tab="isOverviewTab"
|
:discussion-resolve-path="discussion.resolve_path"
|
||||||
@handleDeleteNote="$emit('deleteNote')"
|
:is-overview-tab="isOverviewTab"
|
||||||
@startReplying="$emit('startReplying')"
|
@handleDeleteNote="$emit('deleteNote')"
|
||||||
>
|
@startReplying="$emit('startReplying')"
|
||||||
<template #discussion-resolved-text>
|
>
|
||||||
<note-edited-text
|
<template #discussion-resolved-text>
|
||||||
v-if="discussion.resolved"
|
<note-edited-text
|
||||||
:edited-at="discussion.resolved_at"
|
v-if="discussion.resolved"
|
||||||
:edited-by="discussion.resolved_by"
|
:edited-at="discussion.resolved_at"
|
||||||
:action-text="resolvedText"
|
:edited-by="discussion.resolved_by"
|
||||||
class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
|
:action-text="resolvedText"
|
||||||
/>
|
class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
|
||||||
</template>
|
/>
|
||||||
<template #avatar-badge>
|
</template>
|
||||||
<slot name="avatar-badge"></slot>
|
<template #avatar-badge>
|
||||||
</template>
|
<slot name="avatar-badge"></slot>
|
||||||
</component>
|
</template>
|
||||||
|
</component>
|
||||||
|
</gl-intersection-observer>
|
||||||
<discussion-notes-replies-wrapper :is-diff-discussion="discussion.diff_discussion">
|
<discussion-notes-replies-wrapper :is-diff-discussion="discussion.diff_discussion">
|
||||||
<toggle-replies-widget
|
<toggle-replies-widget
|
||||||
v-if="hasReplies"
|
v-if="hasReplies"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import initDevOpsScore from '~/analytics/devops_report/devops_score';
|
import initDevOpsScore from '~/analytics/devops_reports/devops_score';
|
||||||
import initDevOpsScoreDisabledServicePing from '~/analytics/devops_report/devops_score_disabled_service_ping';
|
import initDevOpsScoreDisabledServicePing from '~/analytics/devops_reports/devops_score_disabled_service_ping';
|
||||||
|
|
||||||
initDevOpsScoreDisabledServicePing();
|
initDevOpsScoreDisabledServicePing();
|
||||||
initDevOpsScore();
|
initDevOpsScore();
|
||||||
|
|
|
@ -227,7 +227,7 @@
|
||||||
// IMPORTANT PERFORMANCE OPTIMIZATION
|
// IMPORTANT PERFORMANCE OPTIMIZATION
|
||||||
//
|
//
|
||||||
// When viewinng a blame with many commits a lot of content is rendered on the page.
|
// When viewinng a blame with many commits a lot of content is rendered on the page.
|
||||||
// Two selectors below ensure that we only render what is visible to the user, thus reducing TBT in the browser.
|
// content-visibility rules below ensure that we only render what is visible to the user, thus reducing TBT in the browser.
|
||||||
.commit {
|
.commit {
|
||||||
content-visibility: auto;
|
content-visibility: auto;
|
||||||
contain-intrinsic-size: 1px 3em;
|
contain-intrinsic-size: 1px 3em;
|
||||||
|
@ -237,6 +237,10 @@
|
||||||
content-visibility: auto;
|
content-visibility: auto;
|
||||||
contain-intrinsic-size: 1px 1.1875rem;
|
contain-intrinsic-size: 1px 1.1875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.line-numbers {
|
||||||
|
content-visibility: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.logs {
|
&.logs {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module IssuesHelper
|
module IssuesHelper
|
||||||
|
include Issues::IssueTypeHelpers
|
||||||
|
|
||||||
def issue_css_classes(issue)
|
def issue_css_classes(issue)
|
||||||
classes = ["issue"]
|
classes = ["issue"]
|
||||||
classes << "closed" if issue.closed?
|
classes << "closed" if issue.closed?
|
||||||
|
|
|
@ -41,8 +41,6 @@ module AlertEventLifecycle
|
||||||
scope :firing, -> { where(status: status_value_for(:firing)) }
|
scope :firing, -> { where(status: status_value_for(:firing)) }
|
||||||
scope :resolved, -> { where(status: status_value_for(:resolved)) }
|
scope :resolved, -> { where(status: status_value_for(:resolved)) }
|
||||||
|
|
||||||
scope :count_by_project_id, -> { group(:project_id).count }
|
|
||||||
|
|
||||||
def self.status_value_for(name)
|
def self.status_value_for(name)
|
||||||
state_machines[:status].states[name].value
|
state_machines[:status].states[name].value
|
||||||
end
|
end
|
||||||
|
|
|
@ -92,7 +92,6 @@ module Issuable
|
||||||
scope :recent, -> { reorder(id: :desc) }
|
scope :recent, -> { reorder(id: :desc) }
|
||||||
scope :of_projects, ->(ids) { where(project_id: ids) }
|
scope :of_projects, ->(ids) { where(project_id: ids) }
|
||||||
scope :opened, -> { with_state(:opened) }
|
scope :opened, -> { with_state(:opened) }
|
||||||
scope :only_opened, -> { with_state(:opened) }
|
|
||||||
scope :closed, -> { with_state(:closed) }
|
scope :closed, -> { with_state(:closed) }
|
||||||
|
|
||||||
# rubocop:disable GitlabSecurity/SqlInjection
|
# rubocop:disable GitlabSecurity/SqlInjection
|
||||||
|
|
|
@ -14,7 +14,6 @@ module Milestoneable
|
||||||
|
|
||||||
validate :milestone_is_valid
|
validate :milestone_is_valid
|
||||||
|
|
||||||
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
|
|
||||||
scope :any_milestone, -> { where.not(milestone_id: nil) }
|
scope :any_milestone, -> { where.not(milestone_id: nil) }
|
||||||
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
|
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
|
||||||
scope :without_particular_milestone, ->(title) { left_outer_joins(:milestone).where("milestones.title != ? OR milestone_id IS NULL", title) }
|
scope :without_particular_milestone, ->(title) { left_outer_joins(:milestone).where("milestones.title != ? OR milestone_id IS NULL", title) }
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module AuthorizedProjectUpdate
|
||||||
|
class ProjectAccessChangedService
|
||||||
|
def initialize(project_ids)
|
||||||
|
@project_ids = Array.wrap(project_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute(blocking: true)
|
||||||
|
bulk_args = @project_ids.map { |id| [id] }
|
||||||
|
|
||||||
|
if blocking
|
||||||
|
AuthorizedProjectUpdate::ProjectRecalculateWorker.bulk_perform_and_wait(bulk_args)
|
||||||
|
else
|
||||||
|
AuthorizedProjectUpdate::ProjectRecalculateWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Issues
|
||||||
|
module IssueTypeHelpers
|
||||||
|
# @param object [Issue, Project]
|
||||||
|
# @param issue_type [String, Symbol]
|
||||||
|
def create_issue_type_allowed?(object, issue_type)
|
||||||
|
WorkItem::Type.base_types.key?(issue_type.to_s) &&
|
||||||
|
can?(current_user, :"create_#{issue_type}", object)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -175,21 +175,18 @@ module Groups
|
||||||
end
|
end
|
||||||
|
|
||||||
def refresh_project_authorizations
|
def refresh_project_authorizations
|
||||||
ProjectAuthorization.where(project_id: @group.all_projects.select(:id)).delete_all # rubocop: disable CodeReuse/ActiveRecord
|
projects_to_update = Set.new
|
||||||
|
|
||||||
# refresh authorized projects for current_user immediately
|
# All projects in this hierarchy need to have their project authorizations recalculated
|
||||||
current_user.refresh_authorized_projects
|
@group.all_projects.each_batch { |prjs| projects_to_update.merge(prjs.ids) } # rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
# schedule refreshing projects for all the members of the group
|
|
||||||
@group.refresh_members_authorized_projects
|
|
||||||
|
|
||||||
# When a group is transferred, it also affects who gets access to the projects shared to
|
# When a group is transferred, it also affects who gets access to the projects shared to
|
||||||
# the subgroups within its hierarchy, so we also schedule jobs that refresh authorizations for all such shared projects.
|
# the subgroups within its hierarchy, so we also schedule jobs that refresh authorizations for all such shared projects.
|
||||||
project_group_shares_within_the_hierarchy = ProjectGroupLink.in_group(group.self_and_descendants.select(:id))
|
ProjectGroupLink.in_group(@group.self_and_descendants.select(:id)).each_batch do |project_group_links|
|
||||||
|
projects_to_update.merge(project_group_links.pluck(:project_id)) # rubocop: disable CodeReuse/ActiveRecord
|
||||||
project_group_shares_within_the_hierarchy.find_each do |project_group_link|
|
|
||||||
AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project_group_link.project_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
AuthorizedProjectUpdate::ProjectAccessChangedService.new(projects_to_update.to_a).execute unless projects_to_update.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def raise_transfer_error(message)
|
def raise_transfer_error(message)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
module Issues
|
module Issues
|
||||||
class BaseService < ::IssuableBaseService
|
class BaseService < ::IssuableBaseService
|
||||||
include IncidentManagement::UsageData
|
include IncidentManagement::UsageData
|
||||||
|
include IssueTypeHelpers
|
||||||
|
|
||||||
def hook_data(issue, action, old_associations: {})
|
def hook_data(issue, action, old_associations: {})
|
||||||
hook_data = issue.to_hook_data(current_user, old_associations: old_associations)
|
hook_data = issue.to_hook_data(current_user, old_associations: old_associations)
|
||||||
|
@ -44,7 +45,7 @@ module Issues
|
||||||
def filter_params(issue)
|
def filter_params(issue)
|
||||||
super
|
super
|
||||||
|
|
||||||
params.delete(:issue_type) unless issue_type_allowed?(issue)
|
params.delete(:issue_type) unless create_issue_type_allowed?(issue, params[:issue_type])
|
||||||
filter_incident_label(issue) if params[:issue_type]
|
filter_incident_label(issue) if params[:issue_type]
|
||||||
|
|
||||||
moved_issue = params.delete(:moved_issue)
|
moved_issue = params.delete(:moved_issue)
|
||||||
|
@ -89,12 +90,6 @@ module Issues
|
||||||
Milestones::IssuesCountService.new(milestone).delete_cache
|
Milestones::IssuesCountService.new(milestone).delete_cache
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param object [Issue, Project]
|
|
||||||
def issue_type_allowed?(object)
|
|
||||||
WorkItem::Type.base_types.key?(params[:issue_type]) &&
|
|
||||||
can?(current_user, :"create_#{params[:issue_type]}", object)
|
|
||||||
end
|
|
||||||
|
|
||||||
# @param issue [Issue]
|
# @param issue [Issue]
|
||||||
def filter_incident_label(issue)
|
def filter_incident_label(issue)
|
||||||
return unless add_incident_label?(issue) || remove_incident_label?(issue)
|
return unless add_incident_label?(issue) || remove_incident_label?(issue)
|
||||||
|
|
|
@ -80,7 +80,7 @@ module Issues
|
||||||
]
|
]
|
||||||
|
|
||||||
allowed_params << :milestone_id if can?(current_user, :admin_issue, project)
|
allowed_params << :milestone_id if can?(current_user, :admin_issue, project)
|
||||||
allowed_params << :issue_type if issue_type_allowed?(project)
|
allowed_params << :issue_type if create_issue_type_allowed?(project, params[:issue_type])
|
||||||
|
|
||||||
params.slice(*allowed_params)
|
params.slice(*allowed_params)
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
- if show_adoption?
|
- if show_adoption?
|
||||||
= render_if_exists 'admin/dev_ops_report/devops_tabs'
|
= render_if_exists 'admin/dev_ops_report/devops_tabs'
|
||||||
- else
|
- else
|
||||||
= render 'report'
|
= render 'score'
|
||||||
|
|
||||||
|
|
|
@ -18,17 +18,19 @@
|
||||||
= sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon')
|
= sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon')
|
||||||
.dropdown-content{ data: { testid: 'issue-type-select-dropdown' } }
|
.dropdown-content{ data: { testid: 'issue-type-select-dropdown' } }
|
||||||
%ul
|
%ul
|
||||||
%li.js-filter-issuable-type
|
- if create_issue_type_allowed?(@project, :issue)
|
||||||
= link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do
|
%li.js-filter-issuable-type
|
||||||
#{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_("Issue")}
|
= link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do
|
||||||
%li.js-filter-issuable-type{ data: { track: { action: "select_issue_type_incident", label: "select_issue_type_incident_dropdown_option" } } }
|
#{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_('Issue')}
|
||||||
= link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if issuable.incident?) do
|
- if create_issue_type_allowed?(@project, :incident)
|
||||||
#{sprite_icon(work_item_type_icon(:incident), css_class: 'gl-icon')} #{_("Incident")}
|
%li.js-filter-issuable-type{ data: { track: { action: "select_issue_type_incident", label: "select_issue_type_incident_dropdown_option" } } }
|
||||||
|
= link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if issuable.incident?) do
|
||||||
|
#{sprite_icon(work_item_type_icon(:incident), css_class: 'gl-icon')} #{_('Incident')}
|
||||||
|
|
||||||
#js-type-popover
|
#js-type-popover
|
||||||
|
|
||||||
- if issuable.incident?
|
- if issuable.incident?
|
||||||
%p.form-text.text-muted
|
%p.form-text.text-muted
|
||||||
- incident_docs_url = help_page_path('operations/incident_management/incidents.md')
|
- incident_docs_url = help_page_path('operations/incident_management/incidents.md')
|
||||||
- incident_docs_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: incident_docs_url }
|
- incident_docs_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">', url: incident_docs_url)
|
||||||
= _('A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents.').html_safe % { incident_docs_start: incident_docs_start, incident_docs_end: '</a>'.html_safe }
|
= format(_('A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents.'), incident_docs_start: incident_docs_start, incident_docs_end: '</a>').html_safe
|
||||||
|
|
|
@ -7,6 +7,8 @@ module AuthorizedProjectUpdate
|
||||||
data_consistency :always
|
data_consistency :always
|
||||||
include Gitlab::ExclusiveLeaseHelpers
|
include Gitlab::ExclusiveLeaseHelpers
|
||||||
|
|
||||||
|
prepend WaitableWorker
|
||||||
|
|
||||||
feature_category :authentication_and_authorization
|
feature_category :authentication_and_authorization
|
||||||
urgency :high
|
urgency :high
|
||||||
queue_namespace :authorized_project_update
|
queue_namespace :authorized_project_update
|
||||||
|
|
|
@ -159,7 +159,10 @@ module ContainerExpirationPolicies
|
||||||
|
|
||||||
return unless tags_count && cached_tags_count && tags_count != 0
|
return unless tags_count && cached_tags_count && tags_count != 0
|
||||||
|
|
||||||
log_extra_metadata_on_done(:cleanup_tags_service_cache_hit_ratio, cached_tags_count / tags_count.to_f)
|
ratio = cached_tags_count / tags_count.to_f
|
||||||
|
ratio_as_percentage = (ratio * 100).round(2)
|
||||||
|
|
||||||
|
log_extra_metadata_on_done(:cleanup_tags_service_cache_hit_ratio, ratio_as_percentage)
|
||||||
end
|
end
|
||||||
|
|
||||||
def log_truncate(result)
|
def log_truncate(result)
|
||||||
|
|
|
@ -16,6 +16,8 @@ Bundler.require(*Rails.groups)
|
||||||
|
|
||||||
module Gitlab
|
module Gitlab
|
||||||
class Application < Rails::Application
|
class Application < Rails::Application
|
||||||
|
config.load_defaults 6.1
|
||||||
|
|
||||||
require_dependency Rails.root.join('lib/gitlab')
|
require_dependency Rails.root.join('lib/gitlab')
|
||||||
require_dependency Rails.root.join('lib/gitlab/utils')
|
require_dependency Rails.root.join('lib/gitlab/utils')
|
||||||
require_dependency Rails.root.join('lib/gitlab/action_cable/config')
|
require_dependency Rails.root.join('lib/gitlab/action_cable/config')
|
||||||
|
@ -37,8 +39,6 @@ module Gitlab
|
||||||
require_dependency Rails.root.join('lib/gitlab/runtime')
|
require_dependency Rails.root.join('lib/gitlab/runtime')
|
||||||
require_dependency Rails.root.join('lib/gitlab/patch/legacy_database_config')
|
require_dependency Rails.root.join('lib/gitlab/patch/legacy_database_config')
|
||||||
|
|
||||||
config.autoloader = :zeitwerk
|
|
||||||
|
|
||||||
# To be removed in 15.0
|
# To be removed in 15.0
|
||||||
# This preload is needed to convert legacy `database.yml`
|
# This preload is needed to convert legacy `database.yml`
|
||||||
# from `production: adapter: postgresql`
|
# from `production: adapter: postgresql`
|
||||||
|
@ -190,11 +190,12 @@ module Gitlab
|
||||||
# regardless if schema_search_path is set, or not.
|
# regardless if schema_search_path is set, or not.
|
||||||
config.active_record.dump_schemas = :all
|
config.active_record.dump_schemas = :all
|
||||||
|
|
||||||
# Use new connection handling so that we can use Rails 6.1+ multiple
|
# Override default Active Record settings
|
||||||
# database support.
|
# We cannot do this in an initializer because some models are already loaded by then
|
||||||
config.active_record.legacy_connection_handling = false
|
config.active_record.cache_versioning = false
|
||||||
|
config.active_record.collection_cache_versioning = false
|
||||||
config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"
|
config.active_record.has_many_inversing = false
|
||||||
|
config.active_record.belongs_to_required_by_default = false
|
||||||
|
|
||||||
# Enable the asset pipeline
|
# Enable the asset pipeline
|
||||||
config.assets.enabled = true
|
config.assets.enabled = true
|
||||||
|
@ -380,6 +381,7 @@ module Gitlab
|
||||||
config.cache_store = :redis_cache_store, Gitlab::Redis::Cache.active_support_config
|
config.cache_store = :redis_cache_store, Gitlab::Redis::Cache.active_support_config
|
||||||
|
|
||||||
config.active_job.queue_adapter = :sidekiq
|
config.active_job.queue_adapter = :sidekiq
|
||||||
|
config.action_mailer.deliver_later_queue_name = :mailers
|
||||||
|
|
||||||
# This is needed for gitlab-shell
|
# This is needed for gitlab-shell
|
||||||
ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH']
|
ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH']
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# This file was introduced during upgrading Rails from 5.2 to 6.0.
|
|
||||||
# This file can be removed when `config.load_defaults 6.0` is introduced.
|
|
||||||
|
|
||||||
# Don't force requests from old versions of IE to be UTF-8 encoded.
|
|
||||||
Rails.application.config.action_view.default_enforce_utf8 = false
|
|
|
@ -2,6 +2,5 @@
|
||||||
|
|
||||||
# Be sure to restart your server when you modify this file.
|
# Be sure to restart your server when you modify this file.
|
||||||
|
|
||||||
Rails.application.config.action_dispatch.use_cookies_with_metadata = true
|
|
||||||
Rails.application.config.action_dispatch.cookies_serializer =
|
Rails.application.config.action_dispatch.cookies_serializer =
|
||||||
Gitlab::Utils.to_boolean(ENV['USE_UNSAFE_HYBRID_COOKIES']) ? :hybrid : :json
|
Gitlab::Utils.to_boolean(ENV['USE_UNSAFE_HYBRID_COOKIES']) ? :hybrid : :json
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# Remove this `if` condition when upgraded to rails 5.0.
|
|
||||||
# The body must be kept.
|
|
||||||
# Be sure to restart your server when you modify this file.
|
|
||||||
#
|
|
||||||
# This file contains migration options to ease your Rails 5.0 upgrade.
|
|
||||||
#
|
|
||||||
# Once upgraded flip defaults one by one to migrate to the new default.
|
|
||||||
#
|
|
||||||
# Read the Guide for Upgrading Ruby on Rails for more info on each option.
|
|
||||||
|
|
||||||
# Enable per-form CSRF tokens. Previous versions had false.
|
|
||||||
Rails.application.config.action_controller.per_form_csrf_tokens = false
|
|
||||||
|
|
||||||
# Enable origin-checking CSRF mitigation. Previous versions had false.
|
|
||||||
Rails.application.config.action_controller.forgery_protection_origin_check = false
|
|
||||||
|
|
||||||
# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`.
|
|
||||||
# Previous versions had false.
|
|
||||||
ActiveSupport.to_time_preserves_timezone = false
|
|
||||||
|
|
||||||
# Require `belongs_to` associations by default. Previous versions had false.
|
|
||||||
Rails.application.config.active_record.belongs_to_required_by_default = false
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# This contains configuration from Rails upgrades to override the new defaults so that we
|
||||||
|
# keep existing behavior.
|
||||||
|
#
|
||||||
|
# For boolean values, the new default is the opposite of the value being set in this file.
|
||||||
|
# For other types, the new default is noted in the comments. These are also documented in
|
||||||
|
# https://guides.rubyonrails.org/configuring.html#results-of-config-load-defaults
|
||||||
|
#
|
||||||
|
# To switch a setting to the new default value, we just need to delete the specific line here.
|
||||||
|
|
||||||
|
Rails.application.configure do
|
||||||
|
# Rails 6.1
|
||||||
|
config.action_dispatch.cookies_same_site_protection = nil # New default is :lax
|
||||||
|
config.action_dispatch.ssl_default_redirect_status = nil # New default is 308
|
||||||
|
ActiveSupport.utc_to_local_returns_utc_offset_times = false
|
||||||
|
config.action_controller.urlsafe_csrf_tokens = false
|
||||||
|
config.action_view.preload_links_header = false
|
||||||
|
|
||||||
|
# Rails 5.2
|
||||||
|
config.action_dispatch.use_authenticated_cookie_encryption = false
|
||||||
|
config.active_support.use_authenticated_message_encryption = false
|
||||||
|
config.active_support.hash_digest_class = ::Digest::MD5 # New default is ::Digest::SHA1
|
||||||
|
config.action_controller.default_protect_from_forgery = false
|
||||||
|
config.action_view.form_with_generates_ids = false
|
||||||
|
|
||||||
|
# Rails 5.1
|
||||||
|
config.assets.unknown_asset_fallback = true
|
||||||
|
|
||||||
|
# Rails 5.0
|
||||||
|
config.action_controller.per_form_csrf_tokens = false
|
||||||
|
config.action_controller.forgery_protection_origin_check = false
|
||||||
|
ActiveSupport.to_time_preserves_timezone = false
|
||||||
|
config.ssl_options = {} # New default is { hsts: { subdomains: true } }
|
||||||
|
end
|
|
@ -0,0 +1,112 @@
|
||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
|
||||||
|
const PLUGIN_NAME = 'GraphqlKnownOperationsPlugin';
|
||||||
|
const GRAPHQL_PATH_REGEX = /(query|mutation)\.graphql$/;
|
||||||
|
const OPERATION_NAME_SOURCE_REGEX = /^\s*module\.exports.*oneQuery.*"(\w+)"/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a given webpack module is a "graphql" module
|
||||||
|
*/
|
||||||
|
const isGraphqlModule = (module) => {
|
||||||
|
return GRAPHQL_PATH_REGEX.test(module.resource);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns graphql operation names we can parse from the given module
|
||||||
|
*
|
||||||
|
* Since webpack gives us the source **after** the graphql-tag/loader runs,
|
||||||
|
* we can look for specific lines we're guaranteed to have from the
|
||||||
|
* graphql-tag/loader.
|
||||||
|
*/
|
||||||
|
const getOperationNames = (module) => {
|
||||||
|
const originalSource = module.originalSource();
|
||||||
|
|
||||||
|
if (!originalSource) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = originalSource.source().toString().matchAll(OPERATION_NAME_SOURCE_REGEX);
|
||||||
|
|
||||||
|
return Array.from(matches).map((match) => match[1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFileContents = (knownOperations) => {
|
||||||
|
const sourceData = Array.from(knownOperations.values()).sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
|
return yaml.dump(sourceData);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a webpack4 compatible "RawSource"
|
||||||
|
*
|
||||||
|
* Inspired from https://sourcegraph.com/github.com/FormidableLabs/webpack-stats-plugin@e050ff8c362d5ddd45c66ade724d4a397ace3e5c/-/blob/lib/stats-writer-plugin.js?L144
|
||||||
|
*/
|
||||||
|
const createWebpackRawSource = (source) => {
|
||||||
|
const buff = Buffer.from(source, 'utf-8');
|
||||||
|
|
||||||
|
return {
|
||||||
|
source() {
|
||||||
|
return buff;
|
||||||
|
},
|
||||||
|
size() {
|
||||||
|
return buff.length;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSucceedModule = ({ module, knownOperations }) => {
|
||||||
|
if (!isGraphqlModule(module)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOperationNames(module).forEach((x) => knownOperations.add(x));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCompilerEmit = ({ compilation, knownOperations, filename }) => {
|
||||||
|
const contents = createFileContents(knownOperations);
|
||||||
|
const source = createWebpackRawSource(contents);
|
||||||
|
|
||||||
|
const asset = compilation.getAsset(filename);
|
||||||
|
if (asset) {
|
||||||
|
compilation.updateAsset(filename, source);
|
||||||
|
} else {
|
||||||
|
compilation.emitAsset(filename, source);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Webpack plugin that outputs a file containing known graphql operations.
|
||||||
|
*
|
||||||
|
* A lot of the mechanices was expired from [this example][1].
|
||||||
|
*
|
||||||
|
* [1]: https://sourcegraph.com/github.com/FormidableLabs/webpack-stats-plugin@e050ff8c362d5ddd45c66ade724d4a397ace3e5c/-/blob/lib/stats-writer-plugin.js?L136
|
||||||
|
*/
|
||||||
|
class GraphqlKnownOperationsPlugin {
|
||||||
|
constructor({ filename }) {
|
||||||
|
this._filename = filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(compiler) {
|
||||||
|
const knownOperations = new Set();
|
||||||
|
|
||||||
|
compiler.hooks.emit.tap(PLUGIN_NAME, (compilation) => {
|
||||||
|
onCompilerEmit({
|
||||||
|
compilation,
|
||||||
|
knownOperations,
|
||||||
|
filename: this._filename,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
|
||||||
|
compilation.hooks.succeedModule.tap(PLUGIN_NAME, (module) => {
|
||||||
|
onSucceedModule({
|
||||||
|
module,
|
||||||
|
knownOperations,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GraphqlKnownOperationsPlugin;
|
|
@ -24,6 +24,7 @@ const IS_JH = require('./helpers/is_jh_env');
|
||||||
const vendorDllHash = require('./helpers/vendor_dll_hash');
|
const vendorDllHash = require('./helpers/vendor_dll_hash');
|
||||||
|
|
||||||
const MonacoWebpackPlugin = require('./plugins/monaco_webpack');
|
const MonacoWebpackPlugin = require('./plugins/monaco_webpack');
|
||||||
|
const GraphqlKnownOperationsPlugin = require('./plugins/graphql_known_operations_plugin');
|
||||||
|
|
||||||
const ROOT_PATH = path.resolve(__dirname, '..');
|
const ROOT_PATH = path.resolve(__dirname, '..');
|
||||||
const SUPPORTED_BROWSERS = fs.readFileSync(path.join(ROOT_PATH, '.browserslistrc'), 'utf-8');
|
const SUPPORTED_BROWSERS = fs.readFileSync(path.join(ROOT_PATH, '.browserslistrc'), 'utf-8');
|
||||||
|
@ -456,6 +457,8 @@ module.exports = {
|
||||||
globalAPI: true,
|
globalAPI: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
new GraphqlKnownOperationsPlugin({ filename: 'graphql_known_operations.yml' }),
|
||||||
|
|
||||||
// fix legacy jQuery plugins which depend on globals
|
// fix legacy jQuery plugins which depend on globals
|
||||||
new webpack.ProvidePlugin({
|
new webpack.ProvidePlugin({
|
||||||
$: 'jquery',
|
$: 'jquery',
|
||||||
|
|
|
@ -1298,6 +1298,7 @@ docker build:
|
||||||
|
|
||||||
- `rules: changes` works the same way as [`only: changes` and `except: changes`](#onlychanges--exceptchanges).
|
- `rules: changes` works the same way as [`only: changes` and `except: changes`](#onlychanges--exceptchanges).
|
||||||
- You can use `when: never` to implement a rule similar to [`except:changes`](#onlychanges--exceptchanges).
|
- You can use `when: never` to implement a rule similar to [`except:changes`](#onlychanges--exceptchanges).
|
||||||
|
- `changes` resolves to `true` if any of the matching files are changed (an `OR` operation).
|
||||||
|
|
||||||
#### `rules:exists`
|
#### `rules:exists`
|
||||||
|
|
||||||
|
@ -1330,6 +1331,7 @@ job:
|
||||||
file paths. After the 10,000th check, rules with patterned globs always match.
|
file paths. After the 10,000th check, rules with patterned globs always match.
|
||||||
In other words, the `exists` rule always assumes a match in projects with more
|
In other words, the `exists` rule always assumes a match in projects with more
|
||||||
than 10,000 files.
|
than 10,000 files.
|
||||||
|
- `exists` resolves to `true` if any of the listed files are found (an `OR` operation).
|
||||||
|
|
||||||
#### `rules:allow_failure`
|
#### `rules:allow_failure`
|
||||||
|
|
||||||
|
@ -1567,7 +1569,7 @@ docker build:
|
||||||
|
|
||||||
**Additional details**:
|
**Additional details**:
|
||||||
|
|
||||||
- If any of the matching files are changed (an `OR` operation), `changes` resolves to `true`.
|
- `changes` resolves to `true` if any of the matching files are changed (an `OR` operation).
|
||||||
- If you use refs other than `branches`, `external_pull_requests`, or `merge_requests`,
|
- If you use refs other than `branches`, `external_pull_requests`, or `merge_requests`,
|
||||||
`changes` can't determine if a given file is new or old and always returns `true`.
|
`changes` can't determine if a given file is new or old and always returns `true`.
|
||||||
- If you use `only: changes` with other refs, jobs ignore the changes and always run.
|
- If you use `only: changes` with other refs, jobs ignore the changes and always run.
|
||||||
|
|
|
@ -166,6 +166,13 @@ Our current RSpec tests parallelization setup is as follows:
|
||||||
|
|
||||||
After that, the next pipeline uses the up-to-date `knapsack/report-master.json` file.
|
After that, the next pipeline uses the up-to-date `knapsack/report-master.json` file.
|
||||||
|
|
||||||
|
### Flaky tests
|
||||||
|
|
||||||
|
Tests that are [known to be flaky](testing_guide/flaky_tests.md#automatic-retries-and-flaky-tests-detection) are:
|
||||||
|
|
||||||
|
- skipped if the `$SKIP_FLAKY_TESTS_AUTOMATICALLY` variable is set to `true` (`false` by default)
|
||||||
|
- run if `$SKIP_FLAKY_TESTS_AUTOMATICALLY` variable is not set to `true` or if the `~"pipeline:run-flaky-tests"` label is set on the MR
|
||||||
|
|
||||||
### Monitoring
|
### Monitoring
|
||||||
|
|
||||||
The GitLab test suite is [monitored](performance.md#rspec-profiling) for the `main` branch, and any branch
|
The GitLab test suite is [monitored](performance.md#rspec-profiling) for the `main` branch, and any branch
|
||||||
|
|
|
@ -85,6 +85,7 @@ the Agent in subsequent steps.
|
||||||
|
|
||||||
In GitLab:
|
In GitLab:
|
||||||
|
|
||||||
|
1. Ensure that [GitLab CI/CD is enabled in your project](../../../../ci/enable_or_disable_ci.md#enable-cicd-in-a-project).
|
||||||
1. From your project's sidebar, select **Infrastructure > Kubernetes clusters**.
|
1. From your project's sidebar, select **Infrastructure > Kubernetes clusters**.
|
||||||
1. Select the **GitLab Agent managed clusters** tab.
|
1. Select the **GitLab Agent managed clusters** tab.
|
||||||
1. Select **Integrate with the GitLab Agent**.
|
1. Select **Integrate with the GitLab Agent**.
|
||||||
|
|
|
@ -29,13 +29,12 @@ Learn more about how GitLab can help you run [Infrastructure as Code](iac/index.
|
||||||
|
|
||||||
## Integrated Kubernetes management
|
## Integrated Kubernetes management
|
||||||
|
|
||||||
GitLab has special integrations with Kubernetes to help you deploy, manage and troubleshoot
|
The GitLab integration with Kubernetes helps you to install, configure, manage, deploy, and troubleshoot
|
||||||
third-party or custom applications in Kubernetes clusters. Auto DevOps provides a full
|
cluster applications. With the GitLab Kubernetes Agent, you can connect clusters behind a firewall,
|
||||||
DevSecOps pipeline by default targeted at Kubernetes based deployments. To support
|
have real-time access to API endpoints, perform pull-beased or push-based deployments for production
|
||||||
all the GitLab features, GitLab offers a cluster management project for easy onboarding.
|
and non-production environments, and much more.
|
||||||
The deploy boards provide quick insights into your cluster, including pod logs tailing.
|
|
||||||
|
|
||||||
Learn more about the [GitLab integration with Kubernetes](clusters/index.md).
|
Learn more about the [GitLab Kubernetes Agent](../clusters/agent/index.md).
|
||||||
|
|
||||||
## Runbooks in GitLab
|
## Runbooks in GitLab
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Graphql
|
||||||
|
class KnownOperations
|
||||||
|
Operation = Struct.new(:name) do
|
||||||
|
def to_caller_id
|
||||||
|
"graphql:#{name}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ANONYMOUS = Operation.new("anonymous").freeze
|
||||||
|
UNKNOWN = Operation.new("unknown").freeze
|
||||||
|
|
||||||
|
def self.default
|
||||||
|
@default ||= self.new(Gitlab::Webpack::GraphqlKnownOperations.load)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(operation_names)
|
||||||
|
@operation_hash = operation_names
|
||||||
|
.map { |name| Operation.new(name).freeze }
|
||||||
|
.concat([ANONYMOUS, UNKNOWN])
|
||||||
|
.index_by(&:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the known operation from the given ::GraphQL::Query object
|
||||||
|
def from_query(query)
|
||||||
|
operation_name = query.selected_operation_name
|
||||||
|
|
||||||
|
return ANONYMOUS unless operation_name
|
||||||
|
|
||||||
|
@operation_hash[operation_name] || UNKNOWN
|
||||||
|
end
|
||||||
|
|
||||||
|
def operations
|
||||||
|
@operation_hash.values
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -6,11 +6,13 @@ module Gitlab
|
||||||
module SidekiqConfig
|
module SidekiqConfig
|
||||||
FOSS_QUEUE_CONFIG_PATH = 'app/workers/all_queues.yml'
|
FOSS_QUEUE_CONFIG_PATH = 'app/workers/all_queues.yml'
|
||||||
EE_QUEUE_CONFIG_PATH = 'ee/app/workers/all_queues.yml'
|
EE_QUEUE_CONFIG_PATH = 'ee/app/workers/all_queues.yml'
|
||||||
|
JH_QUEUE_CONFIG_PATH = 'jh/app/workers/all_queues.yml'
|
||||||
SIDEKIQ_QUEUES_PATH = 'config/sidekiq_queues.yml'
|
SIDEKIQ_QUEUES_PATH = 'config/sidekiq_queues.yml'
|
||||||
|
|
||||||
QUEUE_CONFIG_PATHS = [
|
QUEUE_CONFIG_PATHS = [
|
||||||
FOSS_QUEUE_CONFIG_PATH,
|
FOSS_QUEUE_CONFIG_PATH,
|
||||||
(EE_QUEUE_CONFIG_PATH if Gitlab.ee?)
|
(EE_QUEUE_CONFIG_PATH if Gitlab.ee?),
|
||||||
|
(JH_QUEUE_CONFIG_PATH if Gitlab.jh?)
|
||||||
].compact.freeze
|
].compact.freeze
|
||||||
|
|
||||||
# This maps workers not in our application code to queues. We need
|
# This maps workers not in our application code to queues. We need
|
||||||
|
@ -33,7 +35,7 @@ module Gitlab
|
||||||
weight: 2,
|
weight: 2,
|
||||||
tags: []
|
tags: []
|
||||||
)
|
)
|
||||||
}.transform_values { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: false) }.freeze
|
}.transform_values { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: false, jh: false) }.freeze
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
include Gitlab::SidekiqConfig::CliMethods
|
include Gitlab::SidekiqConfig::CliMethods
|
||||||
|
@ -58,10 +60,14 @@ module Gitlab
|
||||||
@workers ||= begin
|
@workers ||= begin
|
||||||
result = []
|
result = []
|
||||||
result.concat(DEFAULT_WORKERS.values)
|
result.concat(DEFAULT_WORKERS.values)
|
||||||
result.concat(find_workers(Rails.root.join('app', 'workers'), ee: false))
|
result.concat(find_workers(Rails.root.join('app', 'workers'), ee: false, jh: false))
|
||||||
|
|
||||||
if Gitlab.ee?
|
if Gitlab.ee?
|
||||||
result.concat(find_workers(Rails.root.join('ee', 'app', 'workers'), ee: true))
|
result.concat(find_workers(Rails.root.join('ee', 'app', 'workers'), ee: true, jh: false))
|
||||||
|
end
|
||||||
|
|
||||||
|
if Gitlab.jh?
|
||||||
|
result.concat(find_workers(Rails.root.join('jh', 'app', 'workers'), ee: false, jh: true))
|
||||||
end
|
end
|
||||||
|
|
||||||
result
|
result
|
||||||
|
@ -69,16 +75,26 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def workers_for_all_queues_yml
|
def workers_for_all_queues_yml
|
||||||
workers.partition(&:ee?).reverse.map(&:sort)
|
workers.each_with_object([[], [], []]) do |worker, array|
|
||||||
|
if worker.jh?
|
||||||
|
array[2].push(worker)
|
||||||
|
elsif worker.ee?
|
||||||
|
array[1].push(worker)
|
||||||
|
else
|
||||||
|
array[0].push(worker)
|
||||||
|
end
|
||||||
|
end.map(&:sort)
|
||||||
end
|
end
|
||||||
|
|
||||||
# YAML.load_file is OK here as we control the file contents
|
# YAML.load_file is OK here as we control the file contents
|
||||||
def all_queues_yml_outdated?
|
def all_queues_yml_outdated?
|
||||||
foss_workers, ee_workers = workers_for_all_queues_yml
|
foss_workers, ee_workers, jh_workers = workers_for_all_queues_yml
|
||||||
|
|
||||||
return true if foss_workers != YAML.load_file(FOSS_QUEUE_CONFIG_PATH)
|
return true if foss_workers != YAML.load_file(FOSS_QUEUE_CONFIG_PATH)
|
||||||
|
|
||||||
Gitlab.ee? && ee_workers != YAML.load_file(EE_QUEUE_CONFIG_PATH)
|
return true if Gitlab.ee? && ee_workers != YAML.load_file(EE_QUEUE_CONFIG_PATH)
|
||||||
|
|
||||||
|
Gitlab.jh? && File.exist?(JH_QUEUE_CONFIG_PATH) && jh_workers != YAML.load_file(JH_QUEUE_CONFIG_PATH)
|
||||||
end
|
end
|
||||||
|
|
||||||
def queues_for_sidekiq_queues_yml
|
def queues_for_sidekiq_queues_yml
|
||||||
|
@ -120,14 +136,14 @@ module Gitlab
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def find_workers(root, ee:)
|
def find_workers(root, ee:, jh:)
|
||||||
concerns = root.join('concerns').to_s
|
concerns = root.join('concerns').to_s
|
||||||
|
|
||||||
Dir[root.join('**', '*.rb')]
|
Dir[root.join('**', '*.rb')]
|
||||||
.reject { |path| path.start_with?(concerns) }
|
.reject { |path| path.start_with?(concerns) }
|
||||||
.map { |path| worker_from_path(path, root) }
|
.map { |path| worker_from_path(path, root) }
|
||||||
.select { |worker| worker < Sidekiq::Worker }
|
.select { |worker| worker < Sidekiq::Worker }
|
||||||
.map { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: ee) }
|
.map { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: ee, jh: jh) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def worker_from_path(path, root)
|
def worker_from_path(path, root)
|
||||||
|
|
|
@ -18,6 +18,7 @@ module Gitlab
|
||||||
QUEUE_CONFIG_PATHS = begin
|
QUEUE_CONFIG_PATHS = begin
|
||||||
result = %w[app/workers/all_queues.yml]
|
result = %w[app/workers/all_queues.yml]
|
||||||
result << 'ee/app/workers/all_queues.yml' if Gitlab.ee?
|
result << 'ee/app/workers/all_queues.yml' if Gitlab.ee?
|
||||||
|
result << 'jh/app/workers/all_queues.yml' if Gitlab.jh?
|
||||||
result
|
result
|
||||||
end.freeze
|
end.freeze
|
||||||
|
|
||||||
|
|
|
@ -13,15 +13,20 @@ module Gitlab
|
||||||
:worker_has_external_dependencies?,
|
:worker_has_external_dependencies?,
|
||||||
to: :klass
|
to: :klass
|
||||||
|
|
||||||
def initialize(klass, ee:)
|
def initialize(klass, ee:, jh: false)
|
||||||
@klass = klass
|
@klass = klass
|
||||||
@ee = ee
|
@ee = ee
|
||||||
|
@jh = jh
|
||||||
end
|
end
|
||||||
|
|
||||||
def ee?
|
def ee?
|
||||||
@ee
|
@ee
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def jh?
|
||||||
|
@jh
|
||||||
|
end
|
||||||
|
|
||||||
def ==(other)
|
def ==(other)
|
||||||
to_yaml == case other
|
to_yaml == case other
|
||||||
when self.class
|
when self.class
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'net/http'
|
||||||
|
require 'uri'
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Webpack
|
||||||
|
class FileLoader
|
||||||
|
class BaseError < StandardError
|
||||||
|
attr_reader :original_error, :uri
|
||||||
|
|
||||||
|
def initialize(uri, orig)
|
||||||
|
super orig.message
|
||||||
|
@uri = uri.to_s
|
||||||
|
@original_error = orig
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
StaticLoadError = Class.new(BaseError)
|
||||||
|
DevServerLoadError = Class.new(BaseError)
|
||||||
|
DevServerSSLError = Class.new(BaseError)
|
||||||
|
|
||||||
|
def self.load(path)
|
||||||
|
if Gitlab.config.webpack.dev_server.enabled
|
||||||
|
self.load_from_dev_server(path)
|
||||||
|
else
|
||||||
|
self.load_from_static(path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.load_from_dev_server(path)
|
||||||
|
host = Gitlab.config.webpack.dev_server.host
|
||||||
|
port = Gitlab.config.webpack.dev_server.port
|
||||||
|
scheme = Gitlab.config.webpack.dev_server.https ? 'https' : 'http'
|
||||||
|
uri = Addressable::URI.new(scheme: scheme, host: host, port: port, path: self.dev_server_path(path))
|
||||||
|
|
||||||
|
# localhost could be blocked via Gitlab::HTTP
|
||||||
|
response = HTTParty.get(uri.to_s, verify: false) # rubocop:disable Gitlab/HTTParty
|
||||||
|
|
||||||
|
return response.body if response.code == 200
|
||||||
|
|
||||||
|
raise "HTTP error #{response.code}"
|
||||||
|
rescue OpenSSL::SSL::SSLError, EOFError => e
|
||||||
|
raise DevServerSSLError.new(uri, e)
|
||||||
|
rescue StandardError => e
|
||||||
|
raise DevServerLoadError.new(uri, e)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.load_from_static(path)
|
||||||
|
file_uri = ::Rails.root.join(
|
||||||
|
Gitlab.config.webpack.output_dir,
|
||||||
|
path
|
||||||
|
)
|
||||||
|
|
||||||
|
File.read(file_uri)
|
||||||
|
rescue StandardError => e
|
||||||
|
raise StaticLoadError.new(file_uri, e)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.dev_server_path(path)
|
||||||
|
"/#{Gitlab.config.webpack.public_path}/#{path}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Webpack
|
||||||
|
class GraphqlKnownOperations
|
||||||
|
class << self
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
|
def clear_memoization!
|
||||||
|
clear_memoization(:graphql_known_operations)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load
|
||||||
|
strong_memoize(:graphql_known_operations) do
|
||||||
|
data = ::Gitlab::Webpack::FileLoader.load("graphql_known_operations.yml")
|
||||||
|
|
||||||
|
YAML.safe_load(data)
|
||||||
|
rescue StandardError
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,8 +1,5 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'net/http'
|
|
||||||
require 'uri'
|
|
||||||
|
|
||||||
module Gitlab
|
module Gitlab
|
||||||
module Webpack
|
module Webpack
|
||||||
class Manifest
|
class Manifest
|
||||||
|
@ -78,49 +75,16 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_manifest
|
def load_manifest
|
||||||
data = if Gitlab.config.webpack.dev_server.enabled
|
data = Gitlab::Webpack::FileLoader.load(Gitlab.config.webpack.manifest_filename)
|
||||||
load_dev_server_manifest
|
|
||||||
else
|
|
||||||
load_static_manifest
|
|
||||||
end
|
|
||||||
|
|
||||||
Gitlab::Json.parse(data)
|
Gitlab::Json.parse(data)
|
||||||
end
|
rescue Gitlab::Webpack::FileLoader::StaticLoadError => e
|
||||||
|
raise ManifestLoadError.new("Could not load compiled manifest from #{e.uri}.\n\nHave you run `rake gitlab:assets:compile`?", e.original_error)
|
||||||
def load_dev_server_manifest
|
rescue Gitlab::Webpack::FileLoader::DevServerSSLError => e
|
||||||
host = Gitlab.config.webpack.dev_server.host
|
|
||||||
port = Gitlab.config.webpack.dev_server.port
|
|
||||||
scheme = Gitlab.config.webpack.dev_server.https ? 'https' : 'http'
|
|
||||||
uri = Addressable::URI.new(scheme: scheme, host: host, port: port, path: dev_server_path)
|
|
||||||
|
|
||||||
# localhost could be blocked via Gitlab::HTTP
|
|
||||||
response = HTTParty.get(uri.to_s, verify: false) # rubocop:disable Gitlab/HTTParty
|
|
||||||
|
|
||||||
return response.body if response.code == 200
|
|
||||||
|
|
||||||
raise "HTTP error #{response.code}"
|
|
||||||
rescue OpenSSL::SSL::SSLError, EOFError => e
|
|
||||||
ssl_status = Gitlab.config.webpack.dev_server.https ? ' over SSL' : ''
|
ssl_status = Gitlab.config.webpack.dev_server.https ? ' over SSL' : ''
|
||||||
raise ManifestLoadError.new("Could not connect to webpack-dev-server at #{uri}#{ssl_status}.\n\nIs SSL enabled? Check that settings in `gitlab.yml` and webpack-dev-server match.", e)
|
raise ManifestLoadError.new("Could not connect to webpack-dev-server at #{e.uri}#{ssl_status}.\n\nIs SSL enabled? Check that settings in `gitlab.yml` and webpack-dev-server match.", e.original_error)
|
||||||
rescue StandardError => e
|
rescue Gitlab::Webpack::FileLoader::DevServerLoadError => e
|
||||||
raise ManifestLoadError.new("Could not load manifest from webpack-dev-server at #{uri}.\n\nIs webpack-dev-server running? Try running `gdk status webpack` or `gdk tail webpack`.", e)
|
raise ManifestLoadError.new("Could not load manifest from webpack-dev-server at #{e.uri}.\n\nIs webpack-dev-server running? Try running `gdk status webpack` or `gdk tail webpack`.", e.original_error)
|
||||||
end
|
|
||||||
|
|
||||||
def load_static_manifest
|
|
||||||
File.read(static_manifest_path)
|
|
||||||
rescue StandardError => e
|
|
||||||
raise ManifestLoadError.new("Could not load compiled manifest from #{static_manifest_path}.\n\nHave you run `rake gitlab:assets:compile`?", e)
|
|
||||||
end
|
|
||||||
|
|
||||||
def static_manifest_path
|
|
||||||
::Rails.root.join(
|
|
||||||
Gitlab.config.webpack.output_dir,
|
|
||||||
Gitlab.config.webpack.manifest_filename
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def dev_server_path
|
|
||||||
"/#{Gitlab.config.webpack.public_path}/#{Gitlab.config.webpack.manifest_filename}"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -100,7 +100,7 @@ module Sidebars
|
||||||
::Sidebars::MenuItem.new(
|
::Sidebars::MenuItem.new(
|
||||||
title: _('Google Cloud'),
|
title: _('Google Cloud'),
|
||||||
link: project_google_cloud_index_path(context.project),
|
link: project_google_cloud_index_path(context.project),
|
||||||
active_routes: {},
|
active_routes: { controller: :google_cloud },
|
||||||
item_id: :google_cloud
|
item_id: :google_cloud
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -36,13 +36,17 @@ namespace :gitlab do
|
||||||
# Do not edit it manually!
|
# Do not edit it manually!
|
||||||
BANNER
|
BANNER
|
||||||
|
|
||||||
foss_workers, ee_workers = Gitlab::SidekiqConfig.workers_for_all_queues_yml
|
foss_workers, ee_workers, jh_workers = Gitlab::SidekiqConfig.workers_for_all_queues_yml
|
||||||
|
|
||||||
write_yaml(Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH, banner, foss_workers)
|
write_yaml(Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH, banner, foss_workers)
|
||||||
|
|
||||||
if Gitlab.ee?
|
if Gitlab.ee?
|
||||||
write_yaml(Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH, banner, ee_workers)
|
write_yaml(Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH, banner, ee_workers)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if Gitlab.jh?
|
||||||
|
write_yaml(Gitlab::SidekiqConfig::JH_QUEUE_CONFIG_PATH, banner, jh_workers)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'GitLab | Sidekiq | Validate that all_queues.yml matches worker definitions'
|
desc 'GitLab | Sidekiq | Validate that all_queues.yml matches worker definitions'
|
||||||
|
@ -57,6 +61,7 @@ namespace :gitlab do
|
||||||
|
|
||||||
- #{Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH}
|
- #{Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH}
|
||||||
- #{Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH}
|
- #{Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH}
|
||||||
|
#{"- " + Gitlab::SidekiqConfig::JH_QUEUE_CONFIG_PATH if Gitlab.jh?}
|
||||||
|
|
||||||
MSG
|
MSG
|
||||||
end
|
end
|
||||||
|
|
|
@ -4037,6 +4037,9 @@ msgid_plural "ApplicationSettings|Approve %d users"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
|
msgid "ApplicationSettings|Approve users"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "ApplicationSettings|Approve users in the pending approval status?"
|
msgid "ApplicationSettings|Approve users in the pending approval status?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -4045,6 +4048,9 @@ msgid_plural "ApplicationSettings|By making this change, you will automatically
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
|
msgid "ApplicationSettings|By making this change, you will automatically approve all users in pending approval status."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "ApplicationSettings|Denied domains for sign-ups"
|
msgid "ApplicationSettings|Denied domains for sign-ups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -227,10 +227,10 @@ GEM
|
||||||
watir (6.19.1)
|
watir (6.19.1)
|
||||||
regexp_parser (>= 1.2, < 3)
|
regexp_parser (>= 1.2, < 3)
|
||||||
selenium-webdriver (>= 3.142.7)
|
selenium-webdriver (>= 3.142.7)
|
||||||
webdrivers (4.7.0)
|
webdrivers (5.0.0)
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.6)
|
||||||
rubyzip (>= 1.3.0)
|
rubyzip (>= 1.3.0)
|
||||||
selenium-webdriver (> 3.141, < 5.0)
|
selenium-webdriver (~> 4.0)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.4.2)
|
zeitwerk (2.4.2)
|
||||||
|
@ -263,10 +263,10 @@ DEPENDENCIES
|
||||||
rspec-retry (~> 0.6.1)
|
rspec-retry (~> 0.6.1)
|
||||||
rspec_junit_formatter (~> 0.4.1)
|
rspec_junit_formatter (~> 0.4.1)
|
||||||
ruby-debug-ide (~> 0.7.0)
|
ruby-debug-ide (~> 0.7.0)
|
||||||
selenium-webdriver (~> 4.0.0.rc1)
|
selenium-webdriver (~> 4.0)
|
||||||
timecop (~> 0.9.1)
|
timecop (~> 0.9.1)
|
||||||
webdrivers (~> 4.6)
|
webdrivers (~> 5.0)
|
||||||
zeitwerk (~> 2.4)
|
zeitwerk (~> 2.4)
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.2.29
|
2.2.30
|
||||||
|
|
|
@ -166,6 +166,7 @@ function rspec_paralellized_job() {
|
||||||
export SUITE_FLAKY_RSPEC_REPORT_PATH="${FLAKY_RSPEC_SUITE_REPORT_PATH}"
|
export SUITE_FLAKY_RSPEC_REPORT_PATH="${FLAKY_RSPEC_SUITE_REPORT_PATH}"
|
||||||
export FLAKY_RSPEC_REPORT_PATH="rspec_flaky/all_${report_name}_report.json"
|
export FLAKY_RSPEC_REPORT_PATH="rspec_flaky/all_${report_name}_report.json"
|
||||||
export NEW_FLAKY_RSPEC_REPORT_PATH="rspec_flaky/new_${report_name}_report.json"
|
export NEW_FLAKY_RSPEC_REPORT_PATH="rspec_flaky/new_${report_name}_report.json"
|
||||||
|
export SKIPPED_FLAKY_TESTS_REPORT_PATH="rspec_flaky/skipped_flaky_tests_${report_name}_report.txt"
|
||||||
|
|
||||||
if [[ ! -f $FLAKY_RSPEC_REPORT_PATH ]]; then
|
if [[ ! -f $FLAKY_RSPEC_REPORT_PATH ]]; then
|
||||||
echo "{}" > "${FLAKY_RSPEC_REPORT_PATH}"
|
echo "{}" > "${FLAKY_RSPEC_REPORT_PATH}"
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
# We need to distinguish between known and unknown GraphQL operations. This spec
|
||||||
|
# tests that we set up Gitlab::Graphql::KnownOperations.default which requires
|
||||||
|
# integration of FE queries, webpack plugin, and BE.
|
||||||
|
RSpec.describe 'Graphql known operations', :js do
|
||||||
|
around do |example|
|
||||||
|
# Let's make sure we aren't receiving or leaving behind any side-effects
|
||||||
|
# https://gitlab.com/gitlab-org/gitlab/-/jobs/1743294100
|
||||||
|
::Gitlab::Graphql::KnownOperations.instance_variable_set(:@default, nil)
|
||||||
|
::Gitlab::Webpack::GraphqlKnownOperations.clear_memoization!
|
||||||
|
|
||||||
|
example.run
|
||||||
|
|
||||||
|
::Gitlab::Graphql::KnownOperations.instance_variable_set(:@default, nil)
|
||||||
|
::Gitlab::Webpack::GraphqlKnownOperations.clear_memoization!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'collects known Graphql operations from the code', :aggregate_failures do
|
||||||
|
# Check that we include some arbitrary operation name we expect
|
||||||
|
known_operations = Gitlab::Graphql::KnownOperations.default.operations.map(&:name)
|
||||||
|
|
||||||
|
expect(known_operations).to include("searchProjects")
|
||||||
|
expect(known_operations.length).to be > 20
|
||||||
|
expect(known_operations).to all( match(%r{^[a-z]+}i) )
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,25 +4,29 @@ require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe 'New/edit issue', :js do
|
RSpec.describe 'New/edit issue', :js do
|
||||||
include ActionView::Helpers::JavaScriptHelper
|
include ActionView::Helpers::JavaScriptHelper
|
||||||
include FormHelper
|
|
||||||
|
|
||||||
let_it_be(:project) { create(:project) }
|
let_it_be(:project) { create(:project) }
|
||||||
let_it_be(:user) { create(:user)}
|
let_it_be(:user) { create(:user) }
|
||||||
let_it_be(:user2) { create(:user)}
|
let_it_be(:user2) { create(:user) }
|
||||||
let_it_be(:milestone) { create(:milestone, project: project) }
|
let_it_be(:milestone) { create(:milestone, project: project) }
|
||||||
let_it_be(:label) { create(:label, project: project) }
|
let_it_be(:label) { create(:label, project: project) }
|
||||||
let_it_be(:label2) { create(:label, project: project) }
|
let_it_be(:label2) { create(:label, project: project) }
|
||||||
let_it_be(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
|
let_it_be(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
|
||||||
|
|
||||||
|
let(:current_user) { user }
|
||||||
|
|
||||||
|
before_all do
|
||||||
|
project.add_maintainer(user)
|
||||||
|
project.add_maintainer(user2)
|
||||||
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
|
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
|
||||||
|
|
||||||
project.add_maintainer(user)
|
sign_in(current_user)
|
||||||
project.add_maintainer(user2)
|
|
||||||
sign_in(user)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'new issue' do
|
describe 'new issue' do
|
||||||
before do
|
before do
|
||||||
visit new_project_issue_path(project)
|
visit new_project_issue_path(project)
|
||||||
end
|
end
|
||||||
|
@ -235,29 +239,42 @@ RSpec.describe 'New/edit issue', :js do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'displays issue type options in the dropdown' do
|
describe 'displays issue type options in the dropdown' do
|
||||||
|
shared_examples 'type option is visible' do |label:, identifier:|
|
||||||
|
it "shows #{identifier} option", :aggregate_failures do
|
||||||
|
page.within('[data-testid="issue-type-select-dropdown"]') do
|
||||||
|
expect(page).to have_selector(%([data-testid="issue-type-#{identifier}-icon"]))
|
||||||
|
expect(page).to have_content(label)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
page.within('.issue-form') do
|
page.within('.issue-form') do
|
||||||
click_button 'Issue'
|
click_button 'Issue'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'correctly displays the Issue type option with an icon', :aggregate_failures do
|
it_behaves_like 'type option is visible', label: 'Issue', identifier: :issue
|
||||||
page.within('[data-testid="issue-type-select-dropdown"]') do
|
it_behaves_like 'type option is visible', label: 'Incident', identifier: :incident
|
||||||
expect(page).to have_selector('[data-testid="issue-type-issue-icon"]')
|
|
||||||
expect(page).to have_content('Issue')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'correctly displays the Incident type option with an icon', :aggregate_failures do
|
context 'when user is guest' do
|
||||||
page.within('[data-testid="issue-type-select-dropdown"]') do
|
let_it_be(:guest) { create(:user) }
|
||||||
expect(page).to have_selector('[data-testid="issue-type-incident-icon"]')
|
|
||||||
expect(page).to have_content('Incident')
|
let(:current_user) { guest }
|
||||||
|
|
||||||
|
before_all do
|
||||||
|
project.add_guest(guest)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'type option is visible', label: 'Issue', identifier: :issue
|
||||||
|
it_behaves_like 'type option is visible', label: 'Incident', identifier: :incident
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'milestone' do
|
describe 'milestone' do
|
||||||
let!(:milestone) { create(:milestone, title: '"><img src=x onerror=alert(document.domain)>', project: project) }
|
let!(:milestone) do
|
||||||
|
create(:milestone, title: '"><img src=x onerror=alert(document.domain)>', project: project)
|
||||||
|
end
|
||||||
|
|
||||||
it 'escapes milestone' do
|
it 'escapes milestone' do
|
||||||
click_button 'Milestone'
|
click_button 'Milestone'
|
||||||
|
@ -274,7 +291,7 @@ RSpec.describe 'New/edit issue', :js do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'edit issue' do
|
describe 'edit issue' do
|
||||||
before do
|
before do
|
||||||
visit edit_project_issue_path(project, issue)
|
visit edit_project_issue_path(project, issue)
|
||||||
end
|
end
|
||||||
|
@ -329,7 +346,7 @@ RSpec.describe 'New/edit issue', :js do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'inline edit' do
|
describe 'inline edit' do
|
||||||
before do
|
before do
|
||||||
visit project_issue_path(project, issue)
|
visit project_issue_path(project, issue)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { GlBanner } from '@gitlab/ui';
|
import { GlBanner } from '@gitlab/ui';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import DevopsScoreCallout from '~/analytics/devops_report/components/devops_score_callout.vue';
|
import DevopsScoreCallout from '~/analytics/devops_reports/components/devops_score_callout.vue';
|
||||||
import { INTRO_COOKIE_KEY } from '~/analytics/devops_report/constants';
|
import { INTRO_COOKIE_KEY } from '~/analytics/devops_reports/constants';
|
||||||
import * as utils from '~/lib/utils/common_utils';
|
import * as utils from '~/lib/utils/common_utils';
|
||||||
import { devopsReportDocsPath, devopsScoreIntroImagePath } from '../mock_data';
|
import { devopsReportDocsPath, devopsScoreIntroImagePath } from '../mock_data';
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { GlTable, GlBadge, GlEmptyState } from '@gitlab/ui';
|
||||||
import { GlSingleStat } from '@gitlab/ui/dist/charts';
|
import { GlSingleStat } from '@gitlab/ui/dist/charts';
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||||
import DevopsScore from '~/analytics/devops_report/components/devops_score.vue';
|
import DevopsScore from '~/analytics/devops_reports/components/devops_score.vue';
|
||||||
import DevopsScoreCallout from '~/analytics/devops_report/components/devops_score_callout.vue';
|
import DevopsScoreCallout from '~/analytics/devops_reports/components/devops_score_callout.vue';
|
||||||
import { devopsScoreMetricsData, noDataImagePath, devopsScoreTableHeaders } from '../mock_data';
|
import { devopsScoreMetricsData, noDataImagePath, devopsScoreTableHeaders } from '../mock_data';
|
||||||
|
|
||||||
describe('DevopsScore', () => {
|
describe('DevopsScore', () => {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { GlEmptyState, GlSprintf } from '@gitlab/ui';
|
import { GlEmptyState, GlSprintf } from '@gitlab/ui';
|
||||||
import { TEST_HOST } from 'helpers/test_constants';
|
import { TEST_HOST } from 'helpers/test_constants';
|
||||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
import ServicePingDisabled from '~/analytics/devops_report/components/service_ping_disabled.vue';
|
import ServicePingDisabled from '~/analytics/devops_reports/components/service_ping_disabled.vue';
|
||||||
|
|
||||||
describe('~/analytics/devops_report/components/service_ping_disabled.vue', () => {
|
describe('~/analytics/devops_reports/components/service_ping_disabled.vue', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
|
@ -1,6 +1,7 @@
|
||||||
import { GlIcon } from '@gitlab/ui';
|
import { GlIcon } from '@gitlab/ui';
|
||||||
import { mount, createLocalVue } from '@vue/test-utils';
|
import { mount, createLocalVue } from '@vue/test-utils';
|
||||||
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
|
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
|
||||||
|
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
|
||||||
import { createStore } from '~/mr_notes/stores';
|
import { createStore } from '~/mr_notes/stores';
|
||||||
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
|
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
|
||||||
import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
|
import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
|
||||||
|
@ -19,6 +20,9 @@ describe('DiffDiscussions', () => {
|
||||||
store = createStore();
|
store = createStore();
|
||||||
wrapper = mount(localVue.extend(DiffDiscussions), {
|
wrapper = mount(localVue.extend(DiffDiscussions), {
|
||||||
store,
|
store,
|
||||||
|
provide: {
|
||||||
|
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
|
||||||
|
},
|
||||||
propsData: {
|
propsData: {
|
||||||
discussions: getDiscussionsMockData(),
|
discussions: getDiscussionsMockData(),
|
||||||
...props,
|
...props,
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
|
||||||
|
|
||||||
|
describe('Diff Discussions Utils', () => {
|
||||||
|
describe('discussionIntersectionObserverHandlerFactory', () => {
|
||||||
|
it('creates a handler function', () => {
|
||||||
|
expect(discussionIntersectionObserverHandlerFactory()).toBeInstanceOf(Function);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('intersection observer handler', () => {
|
||||||
|
const functions = {
|
||||||
|
setCurrentDiscussionId: jest.fn(),
|
||||||
|
getPreviousUnresolvedDiscussionId: jest.fn().mockImplementation((id) => {
|
||||||
|
return Number(id) - 1;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const defaultProcessableWrapper = {
|
||||||
|
entry: {
|
||||||
|
time: 0,
|
||||||
|
isIntersecting: true,
|
||||||
|
rootBounds: {
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
boundingClientRect: {
|
||||||
|
top: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentDiscussion: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
isFirstUnresolved: false,
|
||||||
|
isDiffsPage: true,
|
||||||
|
};
|
||||||
|
let handler;
|
||||||
|
let getMock;
|
||||||
|
let setMock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
functions.setCurrentDiscussionId.mockClear();
|
||||||
|
functions.getPreviousUnresolvedDiscussionId.mockClear();
|
||||||
|
|
||||||
|
defaultProcessableWrapper.functions = functions;
|
||||||
|
|
||||||
|
setMock = functions.setCurrentDiscussionId.mock;
|
||||||
|
getMock = functions.getPreviousUnresolvedDiscussionId.mock;
|
||||||
|
handler = discussionIntersectionObserverHandlerFactory();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('debounces multiple simultaneous requests into one queue', () => {
|
||||||
|
handler(defaultProcessableWrapper);
|
||||||
|
handler(defaultProcessableWrapper);
|
||||||
|
handler(defaultProcessableWrapper);
|
||||||
|
handler(defaultProcessableWrapper);
|
||||||
|
|
||||||
|
expect(setTimeout).toHaveBeenCalledTimes(4);
|
||||||
|
expect(clearTimeout).toHaveBeenCalledTimes(3);
|
||||||
|
|
||||||
|
// By only advancing to one timer, we ensure it's all being batched into one queue
|
||||||
|
jest.advanceTimersToNextTimer();
|
||||||
|
|
||||||
|
expect(functions.setCurrentDiscussionId).toHaveBeenCalledTimes(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('properly processes, sorts and executes the correct actions for a set of observed intersections', () => {
|
||||||
|
handler(defaultProcessableWrapper);
|
||||||
|
handler({
|
||||||
|
// This observation is here to be filtered out because it's a scrollDown
|
||||||
|
...defaultProcessableWrapper,
|
||||||
|
entry: {
|
||||||
|
...defaultProcessableWrapper.entry,
|
||||||
|
isIntersecting: false,
|
||||||
|
boundingClientRect: { top: 10 },
|
||||||
|
rootBounds: { bottom: 100 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
handler({
|
||||||
|
...defaultProcessableWrapper,
|
||||||
|
entry: {
|
||||||
|
...defaultProcessableWrapper.entry,
|
||||||
|
time: 101,
|
||||||
|
isIntersecting: false,
|
||||||
|
rootBounds: { bottom: -100 },
|
||||||
|
},
|
||||||
|
currentDiscussion: { id: 20 },
|
||||||
|
});
|
||||||
|
handler({
|
||||||
|
...defaultProcessableWrapper,
|
||||||
|
entry: {
|
||||||
|
...defaultProcessableWrapper.entry,
|
||||||
|
time: 100,
|
||||||
|
isIntersecting: false,
|
||||||
|
boundingClientRect: { top: 100 },
|
||||||
|
},
|
||||||
|
currentDiscussion: { id: 30 },
|
||||||
|
isDiffsPage: false,
|
||||||
|
});
|
||||||
|
handler({
|
||||||
|
...defaultProcessableWrapper,
|
||||||
|
isFirstUnresolved: true,
|
||||||
|
entry: {
|
||||||
|
...defaultProcessableWrapper.entry,
|
||||||
|
time: 100,
|
||||||
|
isIntersecting: false,
|
||||||
|
boundingClientRect: { top: 200 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.advanceTimersToNextTimer();
|
||||||
|
|
||||||
|
expect(setMock.calls.length).toBe(4);
|
||||||
|
expect(setMock.calls[0]).toEqual([1]);
|
||||||
|
expect(setMock.calls[1]).toEqual([29]);
|
||||||
|
expect(setMock.calls[2]).toEqual([null]);
|
||||||
|
expect(setMock.calls[3]).toEqual([19]);
|
||||||
|
|
||||||
|
expect(getMock.calls.length).toBe(2);
|
||||||
|
expect(getMock.calls[0]).toEqual([30, false]);
|
||||||
|
expect(getMock.calls[1]).toEqual([20, true]);
|
||||||
|
|
||||||
|
[
|
||||||
|
setMock.invocationCallOrder[0],
|
||||||
|
getMock.invocationCallOrder[0],
|
||||||
|
setMock.invocationCallOrder[1],
|
||||||
|
setMock.invocationCallOrder[2],
|
||||||
|
getMock.invocationCallOrder[1],
|
||||||
|
setMock.invocationCallOrder[3],
|
||||||
|
].forEach((order, idx, list) => {
|
||||||
|
// Compare each invocation sequence to the one before it (except the first one)
|
||||||
|
expect(list[idx - 1] || -1).toBeLessThan(order);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,6 +1,7 @@
|
||||||
import { getByRole } from '@testing-library/dom';
|
import { getByRole } from '@testing-library/dom';
|
||||||
import { shallowMount, mount } from '@vue/test-utils';
|
import { shallowMount, mount } from '@vue/test-utils';
|
||||||
import '~/behaviors/markdown/render_gfm';
|
import '~/behaviors/markdown/render_gfm';
|
||||||
|
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
|
||||||
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
|
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
|
||||||
import NoteableNote from '~/notes/components/noteable_note.vue';
|
import NoteableNote from '~/notes/components/noteable_note.vue';
|
||||||
import { SYSTEM_NOTE } from '~/notes/constants';
|
import { SYSTEM_NOTE } from '~/notes/constants';
|
||||||
|
@ -26,6 +27,9 @@ describe('DiscussionNotes', () => {
|
||||||
const createComponent = (props, mountingMethod = shallowMount) => {
|
const createComponent = (props, mountingMethod = shallowMount) => {
|
||||||
wrapper = mountingMethod(DiscussionNotes, {
|
wrapper = mountingMethod(DiscussionNotes, {
|
||||||
store,
|
store,
|
||||||
|
provide: {
|
||||||
|
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
|
||||||
|
},
|
||||||
propsData: {
|
propsData: {
|
||||||
discussion: discussionMock,
|
discussion: discussionMock,
|
||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { nextTick } from 'vue';
|
||||||
import discussionWithTwoUnresolvedNotes from 'test_fixtures/merge_requests/resolved_diff_discussion.json';
|
import discussionWithTwoUnresolvedNotes from 'test_fixtures/merge_requests/resolved_diff_discussion.json';
|
||||||
import { trimText } from 'helpers/text_helper';
|
import { trimText } from 'helpers/text_helper';
|
||||||
import mockDiffFile from 'jest/diffs/mock_data/diff_file';
|
import mockDiffFile from 'jest/diffs/mock_data/diff_file';
|
||||||
|
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
|
||||||
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
|
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
|
||||||
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
|
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
|
||||||
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
|
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
|
||||||
|
@ -31,6 +32,9 @@ describe('noteable_discussion component', () => {
|
||||||
|
|
||||||
wrapper = mount(NoteableDiscussion, {
|
wrapper = mount(NoteableDiscussion, {
|
||||||
store,
|
store,
|
||||||
|
provide: {
|
||||||
|
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
|
||||||
|
},
|
||||||
propsData: { discussion: discussionMock },
|
propsData: { discussion: discussionMock },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -167,6 +171,9 @@ describe('noteable_discussion component', () => {
|
||||||
|
|
||||||
wrapper = mount(NoteableDiscussion, {
|
wrapper = mount(NoteableDiscussion, {
|
||||||
store,
|
store,
|
||||||
|
provide: {
|
||||||
|
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
|
||||||
|
},
|
||||||
propsData: { discussion: discussionMock },
|
propsData: { discussion: discussionMock },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -185,6 +192,9 @@ describe('noteable_discussion component', () => {
|
||||||
|
|
||||||
wrapper = mount(NoteableDiscussion, {
|
wrapper = mount(NoteableDiscussion, {
|
||||||
store,
|
store,
|
||||||
|
provide: {
|
||||||
|
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
|
||||||
|
},
|
||||||
propsData: { discussion: discussionMock },
|
propsData: { discussion: discussionMock },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,7 @@ import DraftNote from '~/batch_comments/components/draft_note.vue';
|
||||||
import batchComments from '~/batch_comments/stores/modules/batch_comments';
|
import batchComments from '~/batch_comments/stores/modules/batch_comments';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import * as urlUtility from '~/lib/utils/url_utility';
|
import * as urlUtility from '~/lib/utils/url_utility';
|
||||||
|
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
|
||||||
import CommentForm from '~/notes/components/comment_form.vue';
|
import CommentForm from '~/notes/components/comment_form.vue';
|
||||||
import NotesApp from '~/notes/components/notes_app.vue';
|
import NotesApp from '~/notes/components/notes_app.vue';
|
||||||
import * as constants from '~/notes/constants';
|
import * as constants from '~/notes/constants';
|
||||||
|
@ -78,6 +79,9 @@ describe('note_app', () => {
|
||||||
</div>`,
|
</div>`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
provide: {
|
||||||
|
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
|
||||||
|
},
|
||||||
propsData,
|
propsData,
|
||||||
store,
|
store,
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'fast_spec_helper'
|
||||||
|
require 'rspec-parameterized'
|
||||||
|
require "support/graphql/fake_query_type"
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::Graphql::KnownOperations do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
# Include duplicated operation names to test that we are unique-ifying them
|
||||||
|
let(:fake_operations) { %w(foo foo bar bar) }
|
||||||
|
let(:fake_schema) do
|
||||||
|
Class.new(GraphQL::Schema) do
|
||||||
|
query Graphql::FakeQueryType
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { described_class.new(fake_operations) }
|
||||||
|
|
||||||
|
describe "#from_query" do
|
||||||
|
where(:query_string, :expected) do
|
||||||
|
"query { helloWorld }" | described_class::ANONYMOUS
|
||||||
|
"query fuzzyyy { helloWorld }" | described_class::UNKNOWN
|
||||||
|
"query foo { helloWorld }" | described_class::Operation.new("foo")
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it "returns known operation name from GraphQL Query" do
|
||||||
|
query = ::GraphQL::Query.new(fake_schema, query_string)
|
||||||
|
|
||||||
|
expect(subject.from_query(query)).to eq(expected)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#operations" do
|
||||||
|
it "returns array of known operations" do
|
||||||
|
expect(subject.operations.map(&:name)).to match_array(%w(anonymous unknown foo bar))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Operation#to_caller_id" do
|
||||||
|
where(:query_string, :expected) do
|
||||||
|
"query { helloWorld }" | "graphql:#{described_class::ANONYMOUS.name}"
|
||||||
|
"query foo { helloWorld }" | "graphql:foo"
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it "formats operation name for caller_id metric property" do
|
||||||
|
query = ::GraphQL::Query.new(fake_schema, query_string)
|
||||||
|
|
||||||
|
expect(subject.from_query(query).to_caller_id).to eq(expected)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".default" do
|
||||||
|
it "returns a memoization of values from webpack", :aggregate_failures do
|
||||||
|
# .default could have been referenced in another spec, so we need to clean it up here
|
||||||
|
described_class.instance_variable_set(:@default, nil)
|
||||||
|
|
||||||
|
expect(Gitlab::Webpack::GraphqlKnownOperations).to receive(:load).once.and_return(fake_operations)
|
||||||
|
|
||||||
|
2.times { described_class.default }
|
||||||
|
|
||||||
|
# Uses reference equality to verify memoization
|
||||||
|
expect(described_class.default).to equal(described_class.default)
|
||||||
|
expect(described_class.default).to be_a(described_class)
|
||||||
|
expect(described_class.default.operations.map(&:name)).to include(*fake_operations)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,12 +11,12 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_exists(exists: true)
|
def stub_exists(exists: true)
|
||||||
['app/workers/all_queues.yml', 'ee/app/workers/all_queues.yml'].each do |path|
|
['app/workers/all_queues.yml', 'ee/app/workers/all_queues.yml', 'jh/app/workers/all_queues.yml'].each do |path|
|
||||||
allow(File).to receive(:exist?).with(expand_path(path)).and_return(exists)
|
allow(File).to receive(:exist?).with(expand_path(path)).and_return(exists)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_contents(foss_queues, ee_queues)
|
def stub_contents(foss_queues, ee_queues, jh_queues)
|
||||||
allow(YAML).to receive(:load_file)
|
allow(YAML).to receive(:load_file)
|
||||||
.with(expand_path('app/workers/all_queues.yml'))
|
.with(expand_path('app/workers/all_queues.yml'))
|
||||||
.and_return(foss_queues)
|
.and_return(foss_queues)
|
||||||
|
@ -24,6 +24,10 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
|
||||||
allow(YAML).to receive(:load_file)
|
allow(YAML).to receive(:load_file)
|
||||||
.with(expand_path('ee/app/workers/all_queues.yml'))
|
.with(expand_path('ee/app/workers/all_queues.yml'))
|
||||||
.and_return(ee_queues)
|
.and_return(ee_queues)
|
||||||
|
|
||||||
|
allow(YAML).to receive(:load_file)
|
||||||
|
.with(expand_path('jh/app/workers/all_queues.yml'))
|
||||||
|
.and_return(jh_queues)
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
@ -45,8 +49,9 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'flattens and joins the contents' do
|
it 'flattens and joins the contents' do
|
||||||
expected_queues = %w[queue_a queue_b]
|
expected_queues = %w[queue_a]
|
||||||
expected_queues = expected_queues.first(1) unless Gitlab.ee?
|
expected_queues << 'queue_b' if Gitlab.ee?
|
||||||
|
expected_queues << 'queue_c' if Gitlab.jh?
|
||||||
|
|
||||||
expect(described_class.worker_queues(dummy_root))
|
expect(described_class.worker_queues(dummy_root))
|
||||||
.to match_array(expected_queues)
|
.to match_array(expected_queues)
|
||||||
|
@ -55,7 +60,7 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
|
||||||
|
|
||||||
context 'when the file contains an array of hashes' do
|
context 'when the file contains an array of hashes' do
|
||||||
before do
|
before do
|
||||||
stub_contents([{ name: 'queue_a' }], [{ name: 'queue_b' }])
|
stub_contents([{ name: 'queue_a' }], [{ name: 'queue_b' }], [{ name: 'queue_c' }])
|
||||||
end
|
end
|
||||||
|
|
||||||
include_examples 'valid file contents'
|
include_examples 'valid file contents'
|
||||||
|
|
|
@ -18,19 +18,26 @@ RSpec.describe Gitlab::SidekiqConfig::Worker do
|
||||||
get_tags: attributes[:tags]
|
get_tags: attributes[:tags]
|
||||||
)
|
)
|
||||||
|
|
||||||
described_class.new(inner_worker, ee: false)
|
described_class.new(inner_worker, ee: false, jh: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#ee?' do
|
describe '#ee?' do
|
||||||
it 'returns the EE status set on creation' do
|
it 'returns the EE status set on creation' do
|
||||||
expect(described_class.new(double, ee: true)).to be_ee
|
expect(described_class.new(double, ee: true, jh: false)).to be_ee
|
||||||
expect(described_class.new(double, ee: false)).not_to be_ee
|
expect(described_class.new(double, ee: false, jh: false)).not_to be_ee
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#jh?' do
|
||||||
|
it 'returns the JH status set on creation' do
|
||||||
|
expect(described_class.new(double, ee: false, jh: true)).to be_jh
|
||||||
|
expect(described_class.new(double, ee: false, jh: false)).not_to be_jh
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#==' do
|
describe '#==' do
|
||||||
def worker_with_yaml(yaml)
|
def worker_with_yaml(yaml)
|
||||||
described_class.new(double, ee: false).tap do |worker|
|
described_class.new(double, ee: false, jh: false).tap do |worker|
|
||||||
allow(worker).to receive(:to_yaml).and_return(yaml)
|
allow(worker).to receive(:to_yaml).and_return(yaml)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -57,7 +64,7 @@ RSpec.describe Gitlab::SidekiqConfig::Worker do
|
||||||
|
|
||||||
expect(worker).to receive(meth)
|
expect(worker).to receive(meth)
|
||||||
|
|
||||||
described_class.new(worker, ee: false).send(meth)
|
described_class.new(worker, ee: false, jh: false).send(meth)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'fast_spec_helper'
|
||||||
|
require 'support/helpers/file_read_helpers'
|
||||||
|
require 'support/webmock'
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::Webpack::FileLoader do
|
||||||
|
include FileReadHelpers
|
||||||
|
include WebMock::API
|
||||||
|
|
||||||
|
let(:error_file_path) { "error.yml" }
|
||||||
|
let(:file_path) { "my_test_file.yml" }
|
||||||
|
let(:file_contents) do
|
||||||
|
<<-EOF
|
||||||
|
- hello
|
||||||
|
- world
|
||||||
|
- test
|
||||||
|
EOF
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Gitlab.config.webpack.dev_server).to receive_messages(host: 'hostname', port: 2000, https: false)
|
||||||
|
allow(Gitlab.config.webpack).to receive(:public_path).and_return('public_path')
|
||||||
|
allow(Gitlab.config.webpack).to receive(:output_dir).and_return('webpack_output')
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with dev server enabled" do
|
||||||
|
before do
|
||||||
|
allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(true)
|
||||||
|
|
||||||
|
stub_request(:get, "http://hostname:2000/public_path/not_found").to_return(status: 404)
|
||||||
|
stub_request(:get, "http://hostname:2000/public_path/#{file_path}").to_return(body: file_contents, status: 200)
|
||||||
|
stub_request(:get, "http://hostname:2000/public_path/#{error_file_path}").to_raise(StandardError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns content when respondes succesfully" do
|
||||||
|
expect(Gitlab::Webpack::FileLoader.load(file_path)).to be(file_contents)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises error when 404" do
|
||||||
|
expect { Gitlab::Webpack::FileLoader.load("not_found") }.to raise_error("HTTP error 404")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises error when errors out" do
|
||||||
|
expect { Gitlab::Webpack::FileLoader.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::DevServerLoadError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with dev server enabled and https" do
|
||||||
|
before do
|
||||||
|
allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(true)
|
||||||
|
allow(Gitlab.config.webpack.dev_server).to receive(:https).and_return(true)
|
||||||
|
|
||||||
|
stub_request(:get, "https://hostname:2000/public_path/#{error_file_path}").to_raise(EOFError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises error if catches SSLError" do
|
||||||
|
expect { Gitlab::Webpack::FileLoader.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::DevServerSSLError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with dev server disabled" do
|
||||||
|
before do
|
||||||
|
allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(false)
|
||||||
|
stub_file_read(::Rails.root.join("webpack_output/#{file_path}"), content: file_contents)
|
||||||
|
stub_file_read(::Rails.root.join("webpack_output/#{error_file_path}"), error: Errno::ENOENT)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".load" do
|
||||||
|
it "returns file content from file path" do
|
||||||
|
expect(Gitlab::Webpack::FileLoader.load(file_path)).to be(file_contents)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "throws error if file cannot be read" do
|
||||||
|
expect { Gitlab::Webpack::FileLoader.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::StaticLoadError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,47 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'fast_spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::Webpack::GraphqlKnownOperations do
|
||||||
|
let(:content) do
|
||||||
|
<<-EOF
|
||||||
|
- hello
|
||||||
|
- world
|
||||||
|
- test
|
||||||
|
EOF
|
||||||
|
end
|
||||||
|
|
||||||
|
around do |example|
|
||||||
|
described_class.clear_memoization!
|
||||||
|
|
||||||
|
example.run
|
||||||
|
|
||||||
|
described_class.clear_memoization!
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".load" do
|
||||||
|
context "when file loader returns" do
|
||||||
|
before do
|
||||||
|
allow(::Gitlab::Webpack::FileLoader).to receive(:load).with("graphql_known_operations.yml").and_return(content)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns memoized value" do
|
||||||
|
expect(::Gitlab::Webpack::FileLoader).to receive(:load).once
|
||||||
|
|
||||||
|
2.times { ::Gitlab::Webpack::GraphqlKnownOperations.load }
|
||||||
|
|
||||||
|
expect(::Gitlab::Webpack::GraphqlKnownOperations.load).to eq(%w(hello world test))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when file loader errors" do
|
||||||
|
before do
|
||||||
|
allow(::Gitlab::Webpack::FileLoader).to receive(:load).and_raise(StandardError.new("test"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns empty array" do
|
||||||
|
expect(::Gitlab::Webpack::GraphqlKnownOperations.load).to eq([])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -51,6 +51,16 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
|
||||||
it 'menu link points to Terraform page' do
|
it 'menu link points to Terraform page' do
|
||||||
expect(subject.link).to eq find_menu_item(:terraform).link
|
expect(subject.link).to eq find_menu_item(:terraform).link
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when Terraform menu is not visible' do
|
||||||
|
before do
|
||||||
|
subject.renderable_items.delete(find_menu_item(:terraform))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'menu link points to Google Cloud page' do
|
||||||
|
expect(subject.link).to eq find_menu_item(:google_cloud).link
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -89,5 +99,11 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
|
||||||
|
|
||||||
it_behaves_like 'access rights checks'
|
it_behaves_like 'access rights checks'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'Google Cloud' do
|
||||||
|
let(:item_id) { :google_cloud }
|
||||||
|
|
||||||
|
it_behaves_like 'access rights checks'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe AuthorizedProjectUpdate::ProjectAccessChangedService do
|
||||||
|
describe '#execute' do
|
||||||
|
it 'schedules the project IDs' do
|
||||||
|
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_and_wait)
|
||||||
|
.with([[1], [2]])
|
||||||
|
|
||||||
|
described_class.new([1, 2]).execute
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'permits non-blocking operation' do
|
||||||
|
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async)
|
||||||
|
.with([[1], [2]])
|
||||||
|
|
||||||
|
described_class.new([1, 2]).execute(blocking: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -593,11 +593,16 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
|
||||||
let_it_be_with_reload(:group) { create(:group, :private, parent: old_parent_group) }
|
let_it_be_with_reload(:group) { create(:group, :private, parent: old_parent_group) }
|
||||||
let_it_be(:new_group_member) { create(:user) }
|
let_it_be(:new_group_member) { create(:user) }
|
||||||
let_it_be(:old_group_member) { create(:user) }
|
let_it_be(:old_group_member) { create(:user) }
|
||||||
|
let_it_be(:unique_subgroup_member) { create(:user) }
|
||||||
|
let_it_be(:direct_project_member) { create(:user) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
new_parent_group.add_maintainer(new_group_member)
|
new_parent_group.add_maintainer(new_group_member)
|
||||||
old_parent_group.add_maintainer(old_group_member)
|
old_parent_group.add_maintainer(old_group_member)
|
||||||
|
subgroup1.add_developer(unique_subgroup_member)
|
||||||
|
nested_project.add_developer(direct_project_member)
|
||||||
group.refresh_members_authorized_projects
|
group.refresh_members_authorized_projects
|
||||||
|
subgroup1.refresh_members_authorized_projects
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'removes old project authorizations' do
|
it 'removes old project authorizations' do
|
||||||
|
@ -613,7 +618,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'performs authorizations job immediately' do
|
it 'performs authorizations job immediately' do
|
||||||
expect(AuthorizedProjectsWorker).to receive(:bulk_perform_inline)
|
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_inline)
|
||||||
|
|
||||||
transfer_service.execute(new_parent_group)
|
transfer_service.execute(new_parent_group)
|
||||||
end
|
end
|
||||||
|
@ -630,14 +635,24 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
|
||||||
ProjectAuthorization.where(project_id: nested_project.id, user_id: new_group_member.id).size
|
ProjectAuthorization.where(project_id: nested_project.id, user_id: new_group_member.id).size
|
||||||
}.from(0).to(1)
|
}.from(0).to(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'preserves existing project authorizations for direct project members' do
|
||||||
|
expect { transfer_service.execute(new_parent_group) }.not_to change {
|
||||||
|
ProjectAuthorization.where(project_id: nested_project.id, user_id: direct_project_member.id).count
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for groups with many members' do
|
context 'for nested groups with unique members' do
|
||||||
before do
|
it 'preserves existing project authorizations' do
|
||||||
11.times do
|
expect { transfer_service.execute(new_parent_group) }.not_to change {
|
||||||
new_parent_group.add_maintainer(create(:user))
|
ProjectAuthorization.where(project_id: nested_project.id, user_id: unique_subgroup_member.id).count
|
||||||
end
|
}
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for groups with many projects' do
|
||||||
|
let_it_be(:project_list) { create_list(:project, 11, :repository, :private, namespace: group) }
|
||||||
|
|
||||||
it 'adds new project authorizations for the user which makes a transfer' do
|
it 'adds new project authorizations for the user which makes a transfer' do
|
||||||
transfer_service.execute(new_parent_group)
|
transfer_service.execute(new_parent_group)
|
||||||
|
@ -646,9 +661,21 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
|
||||||
expect(ProjectAuthorization.where(project_id: nested_project.id, user_id: user.id).size).to eq(1)
|
expect(ProjectAuthorization.where(project_id: nested_project.id, user_id: user.id).size).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'adds project authorizations for users in the new hierarchy' do
|
||||||
|
expect { transfer_service.execute(new_parent_group) }.to change {
|
||||||
|
ProjectAuthorization.where(project_id: project_list.map { |project| project.id }, user_id: new_group_member.id).size
|
||||||
|
}.from(0).to(project_list.count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes project authorizations for users in the old hierarchy' do
|
||||||
|
expect { transfer_service.execute(new_parent_group) }.to change {
|
||||||
|
ProjectAuthorization.where(project_id: project_list.map { |project| project.id }, user_id: old_group_member.id).size
|
||||||
|
}.from(project_list.count).to(0)
|
||||||
|
end
|
||||||
|
|
||||||
it 'schedules authorizations job' do
|
it 'schedules authorizations job' do
|
||||||
expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async)
|
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async)
|
||||||
.with(array_including(new_parent_group.members_with_parents.pluck(:user_id).map {|id| [id, anything] }))
|
.with(array_including(group.all_projects.ids.map { |id| [id, anything] }))
|
||||||
|
|
||||||
transfer_service.execute(new_parent_group)
|
transfer_service.execute(new_parent_group)
|
||||||
end
|
end
|
||||||
|
|
|
@ -107,9 +107,7 @@ RSpec.configure do |config|
|
||||||
warn `curl -s -o log/goroutines.log http://localhost:9236/debug/pprof/goroutine?debug=2`
|
warn `curl -s -o log/goroutines.log http://localhost:9236/debug/pprof/goroutine?debug=2`
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
else
|
||||||
|
|
||||||
unless ENV['CI']
|
|
||||||
# Allow running `:focus` examples locally,
|
# Allow running `:focus` examples locally,
|
||||||
# falling back to all tests when there is no `:focus` example.
|
# falling back to all tests when there is no `:focus` example.
|
||||||
config.filter_run focus: true
|
config.filter_run focus: true
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
return unless ENV['CI']
|
||||||
|
return unless ENV['SKIP_FLAKY_TESTS_AUTOMATICALLY'] == "true"
|
||||||
|
return if ENV['CI_MERGE_REQUEST_LABELS'].include?(/pipeline:run-flaky-tests/)
|
||||||
|
|
||||||
|
require_relative '../tooling/rspec_flaky/report'
|
||||||
|
|
||||||
|
RSpec.configure do |config|
|
||||||
|
$flaky_test_example_ids = begin # rubocop:disable Style/GlobalVars
|
||||||
|
raise "$SUITE_FLAKY_RSPEC_REPORT_PATH is empty." if ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'].to_s.empty?
|
||||||
|
raise "#{ENV['SUITE_FLAKY_RSPEC_REPORT_PATH']} doesn't exist" unless File.exist?(ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'])
|
||||||
|
|
||||||
|
RspecFlaky::Report.load(ENV['SUITE_FLAKY_RSPEC_REPORT_PATH']).map { |_, flaky_test_data| flaky_test_data["example_id"] }
|
||||||
|
rescue => e # rubocop:disable Style/RescueStandardError
|
||||||
|
puts e
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
$skipped_flaky_tests_report = [] # rubocop:disable Style/GlobalVars
|
||||||
|
|
||||||
|
config.around do |example|
|
||||||
|
# Skip flaky tests automatically
|
||||||
|
if $flaky_test_example_ids.include?(example.id) # rubocop:disable Style/GlobalVars
|
||||||
|
puts "Skipping #{example.id} '#{example.full_description}' because it's flaky."
|
||||||
|
$skipped_flaky_tests_report << example.id # rubocop:disable Style/GlobalVars
|
||||||
|
else
|
||||||
|
example.run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
config.after(:suite) do
|
||||||
|
next unless ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH']
|
||||||
|
|
||||||
|
File.write(ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH'], "#{$skipped_flaky_tests_report.join("\n")}\n") # rubocop:disable Style/GlobalVars
|
||||||
|
end
|
||||||
|
end
|
|
@ -82,8 +82,9 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
|
||||||
nil | 10 | nil
|
nil | 10 | nil
|
||||||
0 | 5 | nil
|
0 | 5 | nil
|
||||||
10 | 0 | 0
|
10 | 0 | 0
|
||||||
10 | 5 | 0.5
|
10 | 5 | 50.0
|
||||||
3 | 10 | (10 / 3.to_f)
|
17 | 3 | 17.65
|
||||||
|
3 | 10 | 333.33
|
||||||
end
|
end
|
||||||
|
|
||||||
with_them do
|
with_them do
|
||||||
|
|
Loading…
Reference in New Issue