Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-04-13 18:11:28 +00:00
parent 536d72ba7e
commit a5605d87fb
121 changed files with 1851 additions and 534 deletions

View File

@ -46,8 +46,6 @@ Rails/SaveBang:
Exclude:
- 'ee/spec/controllers/projects/merge_requests_controller_spec.rb'
- 'ee/spec/controllers/subscriptions_controller_spec.rb'
- 'ee/spec/frontend/fixtures/analytics.rb'
- 'ee/spec/graphql/resolvers/vulnerabilities_resolver_spec.rb'
- 'ee/spec/initializers/fog_google_https_private_urls_spec.rb'
- 'ee/spec/lib/analytics/merge_request_metrics_calculator_spec.rb'
- 'ee/spec/lib/ee/gitlab/auth/ldap/sync/group_spec.rb'
@ -182,8 +180,6 @@ Rails/SaveBang:
- 'spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb'
- 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb'
- 'spec/lib/gitlab/git_access_spec.rb'
- 'spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb'
- 'spec/lib/gitlab/gitaly_client/repository_service_spec.rb'
- 'spec/lib/gitlab/import_export/avatar_saver_spec.rb'
- 'spec/lib/gitlab/import_export/base/relation_factory_spec.rb'
- 'spec/lib/gitlab/import_export/design_repo_restorer_spec.rb'
@ -424,7 +420,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
Exclude:
- ee/spec/controllers/admin/geo/projects_controller_spec.rb
- ee/spec/controllers/admin/projects_controller_spec.rb
- ee/spec/controllers/ee/projects/jobs_controller_spec.rb
- ee/spec/controllers/groups/analytics/cycle_analytics/stages_controller_spec.rb
- ee/spec/controllers/groups/analytics/cycle_analytics/summary_controller_spec.rb
- ee/spec/controllers/groups/analytics/cycle_analytics/value_streams_controller_spec.rb

View File

@ -108,7 +108,7 @@ gem 'hashie-forbidden_attributes'
gem 'kaminari', '~> 1.0'
# HAML
gem 'hamlit', '~> 2.14.4'
gem 'hamlit', '~> 2.15.0'
# Files attachments
gem 'carrierwave', '~> 1.3'
@ -294,7 +294,7 @@ gem 'terser', '1.0.2'
gem 'addressable', '~> 2.7'
gem 'gemojione', '~> 3.3'
gem 'gon', '~> 6.2'
gem 'gon', '~> 6.4.0'
gem 'request_store', '~> 1.5'
gem 'base32', '~> 0.3.0'
@ -366,7 +366,7 @@ group :development, :test do
gem 'database_cleaner', '~> 1.7.0'
gem 'factory_bot_rails', '~> 6.1.0'
gem 'rspec-rails', '~> 4.1.2'
gem 'rspec-rails', '~> 5.0.1'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.11.0'

View File

@ -499,8 +499,9 @@ GEM
rubyntlm (~> 0.5)
globalid (0.4.2)
activesupport (>= 4.2.0)
gon (6.2.0)
actionpack (>= 3.0)
gon (6.4.0)
actionpack (>= 3.0.20)
i18n (>= 0.7)
multi_json
request_store (>= 1.0)
google-api-client (0.50.0)
@ -591,7 +592,7 @@ GEM
rainbow
rubocop (>= 0.50.0)
sysexits (~> 1.1)
hamlit (2.14.4)
hamlit (2.15.0)
temple (>= 0.8.2)
thor
tilt
@ -1075,10 +1076,10 @@ GEM
proc_to_ast
rspec (>= 2.13, < 4)
unparser
rspec-rails (4.1.2)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
rspec-rails (5.0.1)
actionpack (>= 5.2)
activesupport (>= 5.2)
railties (>= 5.2)
rspec-core (~> 3.10)
rspec-expectations (~> 3.10)
rspec-mocks (~> 3.10)
@ -1444,7 +1445,7 @@ DEPENDENCIES
gitlab-styles (~> 6.2.0)
gitlab_chronic_duration (~> 0.10.6.2)
gitlab_omniauth-ldap (~> 2.1.1)
gon (~> 6.2)
gon (~> 6.4.0)
google-api-client (~> 0.33)
google-protobuf (~> 3.14.0)
gpgme (~> 2.0.19)
@ -1460,7 +1461,7 @@ DEPENDENCIES
gssapi
guard-rspec
haml_lint (~> 0.36.0)
hamlit (~> 2.14.4)
hamlit (~> 2.15.0)
hangouts-chat (~> 0.0.5)
hashie
hashie-forbidden_attributes
@ -1563,7 +1564,7 @@ DEPENDENCIES
rouge (~> 3.26.0)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 4.1.2)
rspec-rails (~> 5.0.1)
rspec-retry (~> 0.6.1)
rspec_junit_formatter
rspec_profiling (~> 0.0.6)

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -184,7 +184,12 @@ export default {
'viewDiffsFileByFile',
'mrReviews',
]),
...mapGetters('diffs', ['whichCollapsedTypes', 'isParallelView', 'currentDiffIndex']),
...mapGetters('diffs', [
'whichCollapsedTypes',
'isParallelView',
'currentDiffIndex',
'fileCodequalityDiff',
]),
...mapGetters(['isNotesFetched', 'getNoteableData']),
diffs() {
if (!this.viewDiffsFileByFile) {
@ -282,6 +287,7 @@ export default {
endpointMetadata: this.endpointMetadata,
endpointBatch: this.endpointBatch,
endpointCoverage: this.endpointCoverage,
endpointCodequality: this.endpointCodequality,
endpointUpdateUser: this.endpointUpdateUser,
projectPath: this.projectPath,
dismissEndpoint: this.dismissEndpoint,
@ -291,10 +297,6 @@ export default {
mrReviews: this.rehydratedMrReviews,
});
if (this.endpointCodequality) {
this.setCodequalityEndpoint(this.endpointCodequality);
}
if (this.shouldShow) {
this.fetchData();
}
@ -339,7 +341,6 @@ export default {
...mapActions('diffs', [
'moveToNeighboringCommit',
'setBaseConfig',
'setCodequalityEndpoint',
'fetchDiffFilesMeta',
'fetchDiffFilesBatch',
'fetchCoverageFiles',
@ -531,6 +532,7 @@ export default {
:help-page-path="helpPagePath"
:can-current-user-fork="canCurrentUserFork"
:view-diffs-file-by-file="viewDiffsFileByFile"
:codequality-diff="fileCodequalityDiff(file.file_path)"
/>
<div
v-if="showFileByFileNavigation"

View File

@ -67,6 +67,11 @@ export default {
type: Boolean,
required: true,
},
codequalityDiff: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
@ -80,7 +85,7 @@ export default {
genericError: GENERIC_ERROR,
},
computed: {
...mapState('diffs', ['currentDiffFileId', 'codequalityDiff']),
...mapState('diffs', ['currentDiffFileId']),
...mapGetters(['isNotesFetched']),
...mapGetters('diffs', ['getDiffFileDiscussions']),
viewBlobHref() {
@ -149,9 +154,7 @@ export default {
return loggedIn && featureOn;
},
hasCodequalityChanges() {
return (
this.codequalityDiff?.files && this.codequalityDiff?.files[this.file.file_path]?.length > 0
);
return this.codequalityDiff.length > 0;
},
},
watch: {

View File

@ -13,6 +13,11 @@ import {
CONFLICT_THEIR,
CONFLICT_MARKER,
} from '../constants';
import {
getInteropInlineAttributes,
getInteropOldSideAttributes,
getInteropNewSideAttributes,
} from '../utils/interoperability';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
@ -116,6 +121,16 @@ export default {
isLeftConflictMarker() {
return [CONFLICT_MARKER_OUR, CONFLICT_MARKER_THEIR].includes(this.line.left?.type);
},
interopLeftAttributes() {
if (this.inline) {
return getInteropInlineAttributes(this.line.left);
}
return getInteropOldSideAttributes(this.line.left);
},
interopRightAttributes() {
return getInteropNewSideAttributes(this.line.right);
},
},
mounted() {
this.scrollToLineIfNeededParallel(this.line);
@ -181,6 +196,7 @@ export default {
<div
data-testid="left-side"
class="diff-grid-left left-side"
v-bind="interopLeftAttributes"
@dragover.prevent
@dragenter="onDragEnter(line.left, index)"
@dragend="onDragEnd"
@ -286,6 +302,7 @@ export default {
v-if="!inline"
data-testid="right-side"
class="diff-grid-right right-side"
v-bind="interopRightAttributes"
@dragover.prevent
@dragenter="onDragEnter(line.right, index)"
@dragend="onDragEnd"

View File

@ -2,6 +2,7 @@
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import { CONTEXT_LINE_CLASS_NAME } from '../constants';
import { getInteropInlineAttributes } from '../utils/interoperability';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import {
isHighlighted,
@ -96,6 +97,9 @@ export default {
shouldShowAvatarsOnGutter() {
return this.line.hasDiscussions;
},
interopAttrs() {
return getInteropInlineAttributes(this.line);
},
},
mounted() {
this.scrollToLineIfNeededInline(this.line);
@ -124,6 +128,7 @@ export default {
:id="inlineRowId"
:class="classNameMap"
class="line_holder"
v-bind="interopAttrs"
@mouseover="handleMouseMove"
@mouseout="handleMouseMove"
>

View File

@ -3,6 +3,10 @@ import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gi
import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import { CONTEXT_LINE_CLASS_NAME, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
import {
getInteropOldSideAttributes,
getInteropNewSideAttributes,
} from '../utils/interoperability';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
@ -108,6 +112,12 @@ export default {
this.line.hasDiscussionsRight,
);
},
interopLeftAttributes() {
return getInteropOldSideAttributes(this.line.left);
},
interopRightAttributes() {
return getInteropNewSideAttributes(this.line.right);
},
},
mounted() {
this.scrollToLineIfNeededParallel(this.line);
@ -217,6 +227,7 @@ export default {
:key="line.left.line_code"
v-safe-html="line.left.rich_text"
:class="parallelViewLeftLineType"
v-bind="interopLeftAttributes"
class="line_content with-coverage parallel left-side"
@mousedown="handleParallelLineMouseDown"
></td>
@ -283,6 +294,7 @@ export default {
hll: isHighlighted,
},
]"
v-bind="interopRightAttributes"
class="line_content with-coverage parallel right-side"
@mousedown="handleParallelLineMouseDown"
></td>

View File

@ -1,4 +1,5 @@
import Cookies from 'js-cookie';
import Visibility from 'visibilityjs';
import Vue from 'vue';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { diffViewerModes } from '~/ide/constants';
@ -52,12 +53,15 @@ import {
prepareLineForRenamedFile,
} from './utils';
let eTagPoll;
export const setBaseConfig = ({ commit }, options) => {
const {
endpoint,
endpointMetadata,
endpointBatch,
endpointCoverage,
endpointCodequality,
endpointUpdateUser,
projectPath,
dismissEndpoint,
@ -71,6 +75,7 @@ export const setBaseConfig = ({ commit }, options) => {
endpointMetadata,
endpointBatch,
endpointCoverage,
endpointCodequality,
endpointUpdateUser,
projectPath,
dismissEndpoint,
@ -233,6 +238,48 @@ export const fetchCoverageFiles = ({ commit, state }) => {
coveragePoll.makeRequest();
};
export const clearEtagPoll = () => {
eTagPoll = null;
};
export const stopCodequalityPolling = () => {
if (eTagPoll) eTagPoll.stop();
};
export const restartCodequalityPolling = () => {
if (eTagPoll) eTagPoll.restart();
};
export const fetchCodequality = ({ commit, state, dispatch }) => {
eTagPoll = new Poll({
resource: {
getCodequalityDiffReports: (endpoint) => axios.get(endpoint),
},
data: state.endpointCodequality,
method: 'getCodequalityDiffReports',
successCallback: ({ status, data }) => {
if (status === httpStatusCodes.OK) {
commit(types.SET_CODEQUALITY_DATA, data);
eTagPoll.stop();
}
},
errorCallback: () => createFlash(__('Something went wrong on our end. Please try again!')),
});
if (!Visibility.hidden()) {
eTagPoll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
dispatch('restartCodequalityPolling');
} else {
dispatch('stopCodequalityPolling');
}
});
};
export const setHighlightedRow = ({ commit }, lineCode) => {
const fileHash = lineCode.split('_')[0];
commit(types.SET_HIGHLIGHTED_ROW, lineCode);

View File

@ -135,6 +135,16 @@ export const fileLineCoverage = (state) => (file, line) => {
return {};
};
/**
* Returns the codequality diff data for a given file
* @param {string} filePath
* @returns {Array}
*/
export const fileCodequalityDiff = (state) => (filePath) => {
if (!state.codequalityDiff.files || !state.codequalityDiff.files[filePath]) return [];
return state.codequalityDiff.files[filePath];
};
/**
* Returns index of a currently selected diff in diffFiles
* @returns {number}

View File

@ -9,7 +9,8 @@ import {
import { fileByFile } from '../../utils/preferences';
import { getDefaultWhitespace } from '../utils';
const viewTypeFromQueryString = getParameterValues('view')[0];
const getViewTypeFromQueryString = () => getParameterValues('view')[0];
const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
const defaultViewType = INLINE_DIFF_VIEW_TYPE;
const whiteSpaceFromQueryString = getParameterValues('w')[0];
@ -29,9 +30,10 @@ export default () => ({
startVersion: null, // Null unless a target diff is selected for comparison that is not the "base" diff
diffFiles: [],
coverageFiles: {},
codequalityDiff: {},
mergeRequestDiffs: [],
mergeRequestDiff: null,
diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
diffViewType: getViewTypeFromQueryString() || viewTypeFromCookie || defaultViewType,
tree: [],
treeEntries: {},
showTreeList: true,

View File

@ -1,7 +1,7 @@
import * as actions from 'ee_else_ce/diffs/store/actions';
import createState from 'ee_else_ce/diffs/store/modules/diff_state';
import mutations from 'ee_else_ce/diffs/store/mutations';
import * as actions from '../actions';
import * as getters from '../getters';
import mutations from '../mutations';
import createState from './diff_state';
export default () => ({
namespaced: true,

View File

@ -11,6 +11,7 @@ export const SET_MR_FILE_REVIEWS = 'SET_MR_FILE_REVIEWS';
export const SET_DIFF_VIEW_TYPE = 'SET_DIFF_VIEW_TYPE';
export const SET_COVERAGE_DATA = 'SET_COVERAGE_DATA';
export const SET_CODEQUALITY_DATA = 'SET_CODEQUALITY_DATA';
export const SET_MERGE_REQUEST_DIFFS = 'SET_MERGE_REQUEST_DIFFS';
export const TOGGLE_LINE_HAS_FORM = 'TOGGLE_LINE_HAS_FORM';
export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES';

View File

@ -33,6 +33,7 @@ export default {
endpointMetadata,
endpointBatch,
endpointCoverage,
endpointCodequality,
endpointUpdateUser,
projectPath,
dismissEndpoint,
@ -46,6 +47,7 @@ export default {
endpointMetadata,
endpointBatch,
endpointCoverage,
endpointCodequality,
endpointUpdateUser,
projectPath,
dismissEndpoint,
@ -89,6 +91,10 @@ export default {
Object.assign(state, { coverageFiles });
},
[types.SET_CODEQUALITY_DATA](state, codequalityDiffData) {
Object.assign(state, { codequalityDiff: codequalityDiffData });
},
[types.RENDER_FILE](state, file) {
renderFile(file);
},

View File

@ -0,0 +1,49 @@
const OLD = 'old';
const NEW = 'new';
const ATTR_PREFIX = 'data-interop-';
export const ATTR_TYPE = `${ATTR_PREFIX}type`;
export const ATTR_LINE = `${ATTR_PREFIX}line`;
export const ATTR_NEW_LINE = `${ATTR_PREFIX}new-line`;
export const ATTR_OLD_LINE = `${ATTR_PREFIX}old-line`;
export const getInteropInlineAttributes = (line) => {
if (!line) {
return null;
}
const interopType = line.type?.startsWith(OLD) ? OLD : NEW;
const interopLine = interopType === OLD ? line.old_line : line.new_line;
return {
[ATTR_TYPE]: interopType,
[ATTR_LINE]: interopLine,
[ATTR_NEW_LINE]: line.new_line,
[ATTR_OLD_LINE]: line.old_line,
};
};
export const getInteropOldSideAttributes = (line) => {
if (!line) {
return null;
}
return {
[ATTR_TYPE]: OLD,
[ATTR_LINE]: line.old_line,
[ATTR_OLD_LINE]: line.old_line,
};
};
export const getInteropNewSideAttributes = (line) => {
if (!line) {
return null;
}
return {
[ATTR_TYPE]: NEW,
[ATTR_LINE]: line.new_line,
[ATTR_NEW_LINE]: line.new_line,
};
};

View File

@ -20,7 +20,7 @@ export default {
</script>
<template>
<div class="d-flex-center flex-nowrap text-nowrap js-ide-status-mr">
<div class="d-flex-center gl-flex-nowrap text-nowrap js-ide-status-mr">
<gl-icon name="merge-request" />
<span class="ml-1 d-none d-sm-block">{{ s__('WebIDE|Merge request') }}</span>
<gl-link class="ml-1" :href="url">{{ text }}</gl-link>

View File

@ -31,7 +31,7 @@ export default {
<template>
<dropdown-button>
<span class="row flex-nowrap">
<span class="row gl-flex-nowrap">
<span class="col-auto flex-fill text-truncate">
<gl-icon :size="16" :aria-label="__('Current Branch')" name="branch" /> {{ branchLabel }}
</span>

View File

@ -111,7 +111,7 @@ export default {
</gl-link>
</td>
<td
class="gl-display-flex gl-flex-sm-wrap gl-p-4 gl-pt-5 gl-vertical-align-top"
class="gl-display-flex gl-sm-flex-wrap gl-p-4 gl-pt-5 gl-vertical-align-top"
data-testid="fullPath"
data-qa-selector="project_path_content"
>

View File

@ -15,7 +15,7 @@ export default {
<template>
<div
ref="linksSection"
class="gl-sm-display-flex gl-flex-sm-wrap gl-mt-5 gl-p-3 gl-bg-gray-10 border gl-rounded-base links-section"
class="gl-sm-display-flex gl-sm-flex-wrap gl-mt-5 gl-p-3 gl-bg-gray-10 border gl-rounded-base links-section"
>
<div
v-for="(link, key) in links"

View File

@ -96,6 +96,9 @@ export default {
<p class="gl-text-gray-700 gl-mb-6">{{ $options.i18n.plan.description }}</p>
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-4">
<div class="col gl-mb-6">
<learn-gitlab-info-card v-bind="infoProps('issueCreated')" />
</div>
<div class="col gl-mb-6">
<learn-gitlab-info-card v-bind="infoProps('mergeRequestCreated')" />
</div>

View File

@ -61,7 +61,7 @@ export default {
<div
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
<img :src="svg" />
<img :src="svg" :alt="actionLabel" />
<h6>{{ title }}</h6>
<p class="gl-font-sm gl-text-gray-700">{{ description }}</p>
<gl-link :href="url" target="_blank">{{ actionLabel }}</gl-link>

View File

@ -63,6 +63,15 @@ export const ACTION_LABELS = {
section: 'deploy',
position: 1,
},
issueCreated: {
title: s__('LearnGitLab|Create an issue'),
actionLabel: s__('LearnGitLab|Create an issue'),
description: s__(
'LearnGitLab|Create/import issues (tickets) to collaborate on ideas and plan work.',
),
section: 'plan',
position: 0,
},
};
export const ACTION_SECTIONS = {

View File

@ -13,5 +13,6 @@ export const GRAPHQL = 'graphql';
export const STAGE_VIEW = 'stage';
export const LAYER_VIEW = 'layer';
export const VIEW_TYPE_KEY = 'pipeline_graph_view_type';
export const IID_FAILURE = 'missing_iid';

View File

@ -2,11 +2,12 @@
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import { __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants';
import { reportToSentry } from '../../utils';
import { listByLayers } from '../parsing_utils';
import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW } from './constants';
import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants';
import PipelineGraph from './graph_component.vue';
import GraphViewSelector from './graph_view_selector.vue';
import {
@ -22,6 +23,7 @@ export default {
GlAlert,
GlLoadingIcon,
GraphViewSelector,
LocalStorageSync,
PipelineGraph,
},
mixins: [glFeatureFlagMixin()],
@ -143,7 +145,7 @@ export default {
return this.$apollo.queries.pipeline.loading && !this.pipeline;
},
showGraphViewSelector() {
return Boolean(this.glFeatures.pipelineGraphLayersView && this.pipeline);
return Boolean(this.glFeatures.pipelineGraphLayersView && this.pipeline?.usesNeeds);
},
},
mounted() {
@ -184,6 +186,7 @@ export default {
this.currentViewType = type;
},
},
viewTypeKey: VIEW_TYPE_KEY,
};
</script>
<template>
@ -191,11 +194,17 @@ export default {
<gl-alert v-if="showAlert" :variant="alert.variant" @dismiss="hideAlert">
{{ alert.text }}
</gl-alert>
<graph-view-selector
v-if="showGraphViewSelector"
:type="currentViewType"
@updateViewType="updateViewType"
/>
<local-storage-sync
:storage-key="$options.viewTypeKey"
:value="currentViewType"
@input="updateViewType"
>
<graph-view-selector
v-if="showGraphViewSelector"
:type="currentViewType"
@updateViewType="updateViewType"
/>
</local-storage-sync>
<gl-loading-icon v-if="showLoadingIcon" class="gl-mx-auto gl-my-4" size="lg" />
<pipeline-graph
v-if="pipeline"

View File

@ -57,7 +57,7 @@ export default {
<template>
<div class="gl-display-flex gl-align-items-center gl-my-4">
<span>{{ $options.i18n.labelText }}</span>
<gl-dropdown class="gl-ml-4">
<gl-dropdown data-testid="pipeline-view-selector" class="gl-ml-4">
<template #button-content>
<gl-sprintf :message="currentDropdownText">
<template #code="{ content }">

View File

@ -3,18 +3,13 @@
class Profiles::NotificationsController < Profiles::ApplicationController
feature_category :users
# rubocop: disable CodeReuse/ActiveRecord
def show
@user = current_user
@user_groups = user_groups
@group_notifications = UserGroupNotificationSettingsFinder.new(current_user, user_groups).execute
@project_notifications = current_user.notification_settings.for_projects.order(:id)
.preload_source_route
.select { |notification| current_user.can?(:read_project, notification.source) }
@project_notifications = project_notifications_with_preloaded_associations
@global_notification_setting = current_user.global_notification_setting
end
# rubocop: enable CodeReuse/ActiveRecord
def update
result = Users::UpdateService.new(current_user, user_params.merge(user: current_user)).execute
@ -37,4 +32,20 @@ class Profiles::NotificationsController < Profiles::ApplicationController
def user_groups
GroupsFinder.new(current_user, all_available: false).execute.order_name_asc.page(params[:page])
end
# rubocop: disable CodeReuse/ActiveRecord
def project_notifications_with_preloaded_associations
project_notifications = current_user
.notification_settings
.for_projects
.order_by_id_asc
.preload_source_route
projects = project_notifications.map(&:source)
ActiveRecord::Associations::Preloader.new.preload(projects, { namespace: [:route, :owner], group: [] })
Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects, current_user).execute
project_notifications.select { |notification| current_user.can?(:read_project, notification.source) }
end
# rubocop: enable CodeReuse/ActiveRecord
end

View File

@ -27,6 +27,7 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) {
__typename
id
iid
usesNeeds
downstream {
__typename
nodes {

View File

@ -18,6 +18,17 @@ module BoardIssueFilterable
end
def set_filter_values(filters)
filter_by_assignee(filters)
end
def filter_by_assignee(filters)
if filters[:assignee_username] && filters[:assignee_wildcard_id]
raise ::Gitlab::Graphql::Errors::ArgumentError, 'Incompatible arguments: assigneeUsername, assigneeWildcardId.'
end
if filters[:assignee_wildcard_id]
filters[:assignee_id] = filters.delete(:assignee_wildcard_id)
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Types
module Boards
class AssigneeWildcardIdEnum < BaseEnum
graphql_name 'AssigneeWildcardId'
description 'Assignee ID wildcard values'
value 'NONE', 'No assignee is assigned.'
value 'ANY', 'An assignee is assigned.'
end
end
end

View File

@ -18,6 +18,10 @@ module Types
argument :search, GraphQL::STRING_TYPE,
required: false,
description: 'Search query for issue title or description.'
argument :assignee_wildcard_id, ::Types::Boards::AssigneeWildcardIdEnum,
required: false,
description: 'Filter by assignee wildcard. Incompatible with assigneeUsername.'
end
end
end

View File

@ -84,3 +84,4 @@ module AppearancesHelper
end
AppearancesHelper.prepend_if_ee('EE::AppearancesHelper')
AppearancesHelper.prepend_if_jh('JH::AppearancesHelper')

View File

@ -194,10 +194,16 @@ module ApplicationHelper
end
end
def promo_host
# This needs to be used outside of Rails
def self.promo_host
'about.gitlab.com'
end
# Convenient method for Rails helper
def promo_host
ApplicationHelper.promo_host
end
def promo_url
'https://' + promo_host
end
@ -406,3 +412,4 @@ module ApplicationHelper
end
ApplicationHelper.prepend_if_ee('EE::ApplicationHelper')
ApplicationHelper.prepend_if_jh('JH::ApplicationHelper')

View File

@ -24,6 +24,7 @@ module LearnGitlabHelper
private
ACTION_ISSUE_IDS = {
issue_created: 4,
git_write: 6,
pipeline_created: 7,
merge_request_created: 9,

View File

@ -13,13 +13,7 @@ module BulkMemberAccessLoad
raise 'Block is mandatory' unless block_given?
resource_ids = resource_ids.uniq
key = max_member_access_for_resource_key(resource_klass, memoization_index)
access = {}
if Gitlab::SafeRequestStore.active?
Gitlab::SafeRequestStore[key] ||= {}
access = Gitlab::SafeRequestStore[key]
end
access = load_access_hash(resource_klass, memoization_index)
# Look up only the IDs we need
resource_ids -= access.keys
@ -39,10 +33,28 @@ module BulkMemberAccessLoad
access
end
def merge_value_to_request_store(resource_klass, resource_id, memoization_index, value)
max_member_access_for_resource_ids(resource_klass, [resource_id], memoization_index) do
{ resource_id => value }
end
end
private
def max_member_access_for_resource_key(klass, memoization_index)
"max_member_access_for_#{klass.name.underscore.pluralize}:#{memoization_index}"
end
def load_access_hash(resource_klass, memoization_index)
key = max_member_access_for_resource_key(resource_klass, memoization_index)
access = {}
if Gitlab::SafeRequestStore.active?
Gitlab::SafeRequestStore[key] ||= {}
access = Gitlab::SafeRequestStore[key]
end
access
end
end
end

View File

@ -25,7 +25,7 @@ module CascadingNamespaceSettingAttribute
class_methods do
def cascading_settings_feature_enabled?
::Feature.enabled?(:cascading_namespace_settings, default_enabled: false)
::Feature.enabled?(:cascading_namespace_settings, default_enabled: true)
end
private

View File

@ -77,9 +77,14 @@ module HasRepository
def default_branch_from_preferences
return unless empty_repo?
group_branch_default_name = group&.default_branch_name if respond_to?(:group)
(default_branch_from_group_preferences || Gitlab::CurrentSettings.default_branch_name).presence
end
(group_branch_default_name || Gitlab::CurrentSettings.default_branch_name).presence
def default_branch_from_group_preferences
return unless respond_to?(:group)
return unless group
group.default_branch_name || group.root_ancestor.default_branch_name
end
def reload_default_branch

View File

@ -30,6 +30,8 @@ class NotificationSetting < ApplicationRecord
scope :preload_source_route, -> { preload(source: [:route]) }
scope :order_by_id_asc, -> { order(id: :asc) }
# NOTE: Applicable unfound_translations.rb also needs to be updated when below events are changed.
EMAIL_EVENTS = [
:new_release,

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Preloaders
# This class preloads the max access level for the user within the given projects and
# stores the values in requests store via the ProjectTeam class.
class UserMaxAccessLevelInProjectsPreloader
def initialize(projects, user)
@projects = projects
@user = user
end
def execute
access_levels = @user
.project_authorizations
.where(project_id: @projects)
.group(:project_id)
.maximum(:access_level)
@projects.each do |project|
access_level = access_levels[project.id] || Gitlab::Access::NO_ACCESS
ProjectTeam.new(project).write_member_access_for_user_id(@user.id, access_level)
end
end
end
end

View File

@ -174,6 +174,10 @@ class ProjectTeam
end
end
def write_member_access_for_user_id(user_id, project_access_level)
merge_value_to_request_store(User, user_id, project.id, project_access_level)
end
def max_member_access(user_id)
max_member_access_for_user_ids([user_id])[user_id]
end

View File

@ -0,0 +1,58 @@
# frozen_string_literal: true
module Boards
module Lists
class BaseUpdateService < Boards::BaseService
def execute(list)
if execute_by_params(list)
success(list: list)
else
error(list.errors.messages, 422)
end
end
private
def execute_by_params(list)
update_preferences_result = update_preferences(list) if can_read?(list)
update_position_result = update_position(list) if can_admin?(list)
update_preferences_result || update_position_result
end
def update_preferences(list)
return unless preferences?
list.update_preferences_for(current_user, preferences)
end
def update_position(list)
return unless position?
move_service = Boards::Lists::MoveService.new(parent, current_user, params)
move_service.execute(list)
end
def preferences
{ collapsed: Gitlab::Utils.to_boolean(params[:collapsed]) }
end
def preferences?
params.has_key?(:collapsed)
end
def position?
params.has_key?(:position)
end
def can_read?(list)
raise NotImplementedError
end
def can_admin?(list)
raise NotImplementedError
end
end
end
end

View File

@ -2,50 +2,7 @@
module Boards
module Lists
class UpdateService < Boards::BaseService
def execute(list)
if execute_by_params(list)
success(list: list)
else
error(list.errors.messages, 422)
end
end
private
def execute_by_params(list)
update_preferences_result = update_preferences(list) if can_read?(list)
update_position_result = update_position(list) if can_admin?(list)
update_preferences_result || update_position_result
end
def update_preferences(list)
return unless preferences?
list.update_preferences_for(current_user, preferences)
end
def update_position(list)
return unless position?
move_service = Boards::Lists::MoveService.new(parent, current_user, params)
move_service.execute(list)
end
def preferences
{ collapsed: Gitlab::Utils.to_boolean(params[:collapsed]) }
end
def preferences?
params.has_key?(:collapsed)
end
def position?
params.has_key?(:position)
end
class UpdateService < Boards::Lists::BaseUpdateService
def can_read?(list)
Ability.allowed?(current_user, :read_issue_board_list, parent)
end

View File

@ -31,9 +31,9 @@ module Projects
# Create status notifying the deployment of pages
@status = create_status
@status.update_older_statuses_retried! if Feature.enabled?(:ci_fix_commit_status_retried, project, default_enabled: :yaml)
@status.enqueue!
@status.run!
@status.update_older_statuses_retried! if Feature.enabled?(:ci_fix_commit_status_retried, project, default_enabled: :yaml)
raise InvalidStateError, 'missing pages artifacts' unless build.artifacts?
raise InvalidStateError, 'build SHA is outdated for this ref' unless latest?

View File

@ -5,9 +5,9 @@
= render "devise/shared/error_messages", resource: resource
.form-group
= f.label :email
= f.email_field :email, class: "form-control gl-form-input", required: true, value: params[:user_email], autofocus: true, title: 'Please provide a valid email address.'
= f.email_field :email, class: "form-control gl-form-input", required: true, value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.')
.clearfix
= f.submit "Reset password", class: "gl-button btn-confirm btn"
= f.submit _("Reset password"), class: "gl-button btn-confirm btn"
.clearfix.prepend-top-20
= render 'devise/shared/sign_in_link'

View File

@ -5,7 +5,7 @@
.form-group.col-12.col-sm-6
= label_tag :namespace_id, _('Project URL'), class: 'label-bold'
.form-group
.input-group.flex-nowrap
.input-group.gl-flex-nowrap
- if current_user.can_select_namespace?
.input-group-prepend.flex-shrink-0.has-tooltip{ title: root_url }
.input-group-text

View File

@ -12,7 +12,7 @@
.form-group.project-path.col-sm-6
= f.label :namespace_id, class: 'label-bold' do
%span= s_("Project URL")
.input-group.flex-nowrap
.input-group.gl-flex-nowrap
- if current_user.can_select_namespace?
.input-group-prepend.flex-shrink-0.has-tooltip{ title: root_url }
.input-group-text

View File

@ -3,7 +3,7 @@
- flex_grow_and_shrink_xs = 'd-flex flex-xs-grow-1 flex-xs-shrink-1 flex-grow-0 flex-shrink-0'
.filtered-search-block.row-content-block.bt-0
.filtered-search-wrapper.d-flex.flex-nowrap.flex-column.flex-sm-wrap.flex-sm-row.flex-xl-nowrap
.filtered-search-wrapper.d-flex.gl-flex-nowrap.flex-column.flex-sm-wrap.flex-sm-row.flex-xl-nowrap
- unless project_tab_filter == :starred
.filtered-search-nav.mb-2.mb-lg-0{ class: flex_grow_and_shrink_xs }
= render 'dashboard/projects/nav', project_tab_filter: project_tab_filter

View File

@ -0,0 +1,6 @@
---
title: Eliminate N+1 database queries on the user notifications page within the project
notifications section
merge_request: 59029
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Support filtering by assignee wildcard in GraphQL board list issues query
merge_request: 58996
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Inherit default branch name for subgroups
merge_request: 57101
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Externalize strings in passwords/new.html.haml
merge_request: 58236
author: nuwe1
type: other

View File

@ -0,0 +1,5 @@
---
title: Default enable cascading settings feature flag
merge_request: 59026
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Update gon gem to 6.4.0
merge_request: 51210
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Bump rspec-rails to 5.0.1
merge_request: 59194
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Fix Rails/SaveBang Rubocop offenses for gitaly client models
merge_request: 58089
author: Huzaifa Iftikhar @huzaifaiftikhar
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Schedule artifact expiry backfill again.
merge_request: 59270
author:
type: changed

View File

@ -57,21 +57,29 @@ module Gitlab
config.generators.templates.push("#{config.root}/generator_templates")
if Gitlab.ee?
ee_paths = config.eager_load_paths.each_with_object([]) do |path, memo|
ee_path = config.root.join('ee', Pathname.new(path).relative_path_from(config.root))
memo << ee_path.to_s
load_paths = lambda do |dir:|
ext_paths = config.eager_load_paths.each_with_object([]) do |path, memo|
ext_path = config.root.join(dir, Pathname.new(path).relative_path_from(config.root))
memo << ext_path.to_s
end
ee_paths << "#{config.root}/ee/app/replicators"
ext_paths << "#{config.root}/#{dir}/app/replicators"
# Eager load should load CE first
config.eager_load_paths.push(*ee_paths)
config.helpers_paths.push "#{config.root}/ee/app/helpers"
config.eager_load_paths.push(*ext_paths)
config.helpers_paths.push "#{config.root}/#{dir}/app/helpers"
# Other than Ruby modules we load EE first
config.paths['lib/tasks'].unshift "#{config.root}/ee/lib/tasks"
config.paths['app/views'].unshift "#{config.root}/ee/app/views"
# Other than Ruby modules we load extensions first
config.paths['lib/tasks'].unshift "#{config.root}/#{dir}/lib/tasks"
config.paths['app/views'].unshift "#{config.root}/#{dir}/app/views"
end
Gitlab.ee do
load_paths.call(dir: 'ee')
end
Gitlab.jh do
load_paths.call(dir: 'jh')
end
# Rake tasks ignore the eager loading settings, so we need to set the

View File

@ -1,8 +1,8 @@
---
name: cascading_namespace_settings
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55678
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321724
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327230
milestone: '13.11'
type: development
group: group::access
default_enabled: false
default_enabled: true

View File

@ -31,6 +31,12 @@ module InjectEnterpriseEditionModule
include(ee_module) if Gitlab.ee?
end
def prepend_if_jh(constant, with_descendants: false)
return unless Gitlab.jh?
prepend_module(constant.constantize, with_descendants)
end
private
def prepend_module(mod, with_descendants)

View File

@ -1,10 +1,18 @@
# frozen_string_literal: true
Gitlab.ee do
load_license = lambda do |dir:, license_name:|
prefix = ENV['GITLAB_LICENSE_MODE'] == 'test' ? 'test_' : ''
public_key_file = File.read(Rails.root.join(".#{prefix}license_encryption_key.pub"))
public_key_file = File.read(Rails.root.join(dir, ".#{prefix}license_encryption_key.pub"))
public_key = OpenSSL::PKey::RSA.new(public_key_file)
Gitlab::License.encryption_key = public_key
rescue
warn "WARNING: No valid license encryption key provided."
warn "WARNING: No valid #{license_name} encryption key provided."
end
Gitlab.ee do
load_license.call(dir: '.', license_name: 'license')
end
Gitlab.jh do
load_license.call(dir: 'jh', license_name: 'JH license')
end

View File

@ -35,5 +35,6 @@ ActiveSupport::Inflector.inflections do |inflect|
vulnerability_feedback
)
inflect.acronym 'EE'
inflect.acronym 'JH'
inflect.acronym 'CSP'
end

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
class RescheduleArtifactExpiryBackfillAgain < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'BackfillArtifactExpiryDate'
SWITCH_DATE = Date.new(2020, 06, 22).freeze
disable_ddl_transaction!
class JobArtifact < ActiveRecord::Base
include EachBatch
self.inheritance_column = :_type_disabled
self.table_name = 'ci_job_artifacts'
scope :without_expiry_date, -> { where(expire_at: nil) }
scope :before_switch, -> { where("date(created_at AT TIME ZONE 'UTC') < ?::date", SWITCH_DATE) }
end
def up
Gitlab::BackgroundMigration.steal(MIGRATION) do |job|
job.delete
false
end
queue_background_migration_jobs_by_range_at_intervals(
JobArtifact.without_expiry_date.before_switch,
MIGRATION,
2.minutes,
batch_size: 200_000
)
end
def down
Gitlab::BackgroundMigration.steal(MIGRATION) do |job|
job.delete
false
end
end
end

View File

@ -0,0 +1 @@
407806cc168ef9859c9a4f1bd4db7a56aee01367e784ea0767889863b9ace35d

View File

@ -446,6 +446,134 @@ Now we need to make each **secondary** node listen to changes on the new **prima
to [initiate the replication process](../setup/database.md#step-3-initiate-the-replication-process) again but this time
for another **primary** node. All the old replication settings will be overwritten.
## Promoting a secondary Geo cluster in GitLab Cloud Native Helm Charts
When updating a Cloud Native Geo deployment, the process for updating any node that is external to the secondary Kubernetes cluster does not differ from the non Cloud Native approach. As such, you can always defer to [Promoting a secondary Geo node in single-secondary configurations](#promoting-a-secondary-geo-node-in-single-secondary-configurations) for more information.
The following sections assume you are using the `gitlab` namespace. If you used a different namespace when setting up your cluster, you should also replace `--namespace gitlab` with your namespace.
WARNING:
In GitLab 13.2 and 13.3, promoting a secondary site to a primary while the
secondary is paused fails. Do not pause replication before promoting a
secondary. If the site is paused, be sure to resume before promoting. This
issue has been fixed in GitLab 13.4 and later.
### Step 1. Permanently disable the **primary** cluster
WARNING:
If the **primary** site goes offline, there may be data saved on the **primary** site
that has not been replicated to the **secondary** site. This data should be treated
as lost if you proceed.
If an outage on the **primary** site happens, you should do everything possible to
avoid a split-brain situation where writes can occur in two different GitLab
instances, complicating recovery efforts. So to prepare for the failover, you
must disable the **primary** site:
- If you have access to the **primary** Kubernetes cluster, connect to it and disable the GitLab webservice and Sidekiq pods:
```shell
kubectl --namespace gitlab scale deploy gitlab-geo-webservice-default --replicas=0
kubectl --namespace gitlab scale deploy gitlab-geo-sidekiq-all-in-1-v1 --replicas=0
```
- If you do not have access to the **primary** Kubernetes cluster, take the cluster offline and
prevent it from coming back online by any means at your disposal.
Since there are many ways you may prefer to accomplish this, we will avoid a
single recommendation. You may need to:
- Reconfigure the load balancers.
- Change DNS records (for example, point the primary DNS record to the
**secondary** site to stop usage of the **primary** site).
- Stop the virtual servers.
- Block traffic through a firewall.
- Revoke object storage permissions from the **primary** site.
- Physically disconnect a machine.
### Step 2. Promote all **secondary** nodes external to the cluster
WARNING:
If the secondary site [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
a point-in-time recovery to the last known state.
Data that was created on the primary while the secondary was paused will be lost.
1. SSH in to the database node in the **secondary** and trigger PostgreSQL to
promote to read-write:
```shell
sudo gitlab-ctl promote-db
```
In GitLab 12.8 and earlier, see [Message: `sudo: gitlab-pg-ctl: command not found`](../replication/troubleshooting.md#message-sudo-gitlab-pg-ctl-command-not-found).
1. Edit `/etc/gitlab/gitlab.rb` on the database node in the **secondary** site to
reflect its new status as **primary** by removing any lines that enabled the
`geo_secondary_role`:
NOTE:
Depending on your architecture these steps will need to be run on any GitLab node that is external to the **secondary** Kubernetes cluster.
```ruby
## In pre-11.5 documentation, the role was enabled as follows. Remove this line.
geo_secondary_role['enable'] = true
## In 11.5+ documentation, the role was enabled as follows. Remove this line.
roles ['geo_secondary_role']
```
After making these changes, [reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) on the database node.
### Step 3. Promote the **secondary** cluster
1. Find the task runner pod:
```shell
kubectl --namespace gitlab get pods -lapp=task-runner
```
1. Promote the secondary:
```shell
kubectl --namespace gitlab exec -ti gitlab-geo-task-runner-XXX -- gitlab-rake geo:set_secondary_as_primary
```
1. Update the existing cluster configuration.
You can retrieve the existing config with Helm:
```shell
helm --namespace gitlab get values gitlab-geo > gitlab.yaml
```
The existing config will contain a section for Geo that should resemble:
```yaml
geo:
enabled: true
role: secondary
nodeName: secondary.example.com
psql:
host: geo-2.db.example.com
port: 5431
password:
secret: geo
key: geo-postgresql-password
```
To promote the **secondary** cluster to a **primary** cluster, update `role: secondary` to `role: primary`.
You can remove the entire `psql` section if the cluster will remain as a primary site, this refers to the tracking database and will be ignored whilst the cluster is acting as a primary site.
Update the cluster with the new config:
```shell
helm upgrade --install --version <current Chart version> gitlab-geo gitlab/gitlab --namespace gitlab -f gitlab.yaml
```
1. Verify you can connect to the newly promoted primary using the URL used previously for the secondary.
1. Success! The secondary has now been promoted to primary.
## Troubleshooting
This section was moved to [another location](../replication/troubleshooting.md#fixing-errors-during-a-failover-or-when-promoting-a-secondary-to-a-primary-node).

View File

@ -139,6 +139,18 @@ The following metrics can be controlled by feature flags:
| `gitlab_method_call_duration_seconds` | `prometheus_metrics_method_instrumentation` |
| `gitlab_view_rendering_duration_seconds` | `prometheus_metrics_view_instrumentation` |
## Praefect metrics
You can [configure Praefect to report metrics](../../gitaly/praefect.md#praefect).
These are some of the Praefect metrics served from the `/metrics` path on the [configured port](index.md#changing-the-port-and-address-prometheus-listens-on)
(9652 by default).
| Metric | Type | Since | Description | Labels |
| :----- | :--- | ----: | :---------- | :----- |
| `gitaly_praefect_replication_latency_bucket` | Histogram | 12.10 | The amount of time it takes for replication to complete once the replication job starts. | |
| `gitaly_praefect_replication_delay_bucket` | Histogram | 12.10 | A measure of how much time passes between when the replication job is created and when it starts. | |
| `gitaly_praefect_node_latency_bucket` | Histogram | 12.10 | The latency in Gitaly returning health check information to Praefect. This indicates Praefect connection saturation. | |
## Sidekiq metrics
Sidekiq jobs may also gather metrics, and these metrics can be accessed if the

View File

@ -7525,6 +7525,15 @@ The kind of an approval rule.
| `REGULAR` | A `regular` approval rule. |
| `REPORT_APPROVER` | A `report_approver` approval rule. |
### `AssigneeWildcardId`
Assignee ID wildcard values.
| Value | Description |
| ----- | ----------- |
| `ANY` | An assignee is assigned. |
| `NONE` | No assignee is assigned. |
### `AvailabilityEnum`
User availability status.

View File

@ -1,181 +1,8 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
redirect_to: 'https://about.gitlab.com/handbook/engineering/ux/technical-writing/workflow/#monthly-documentation-releases'
---
# Monthly release process
This file was moved to [another location](https://about.gitlab.com/handbook/engineering/ux/technical-writing/workflow/#monthly-documentation-releases).
When a new GitLab version is released on the 22nd, we release version-specific published
documentation for the new version.
We complete the process as soon as possible after the GitLab version is announced. The result is:
- The [online published documentation](https://docs.gitlab.com) includes:
- The three most recent minor releases of the current major version. For example 13.9, 13.8, and
13.7.
- The most recent minor releases of the last two major versions. For example 12.10, and 11.11.
- Documentation updates after the 22nd are for the next release. The versions drop down
should have the current milestone with `-pre` appended to it, for example `13.10-pre`.
Each documentation release:
- Has a dedicated branch, named in the format `XX.yy`.
- Has a Docker image that contains a build of that branch.
For example:
- For [GitLab 13.9](https://docs.gitlab.com/13.9/index.html), the
[stable branch](https://gitlab.com/gitlab-org/gitlab-docs/-/tree/13.9) and Docker image:
[`registry.gitlab.com/gitlab-org/gitlab-docs:13.9`](https://gitlab.com/gitlab-org/gitlab-docs/container_registry/631635).
- For [GitLab 13.8](https://docs.gitlab.com/13.8/index.html), the
[stable branch](https://gitlab.com/gitlab-org/gitlab-docs/-/tree/13.8) and Docker image:
[`registry.gitlab.com/gitlab-org/gitlab-docs:13.8`](https://gitlab.com/gitlab-org/gitlab-docs/container_registry/631635).
## Recommended timeline
To minimize problems during the documentation release process, use the following timeline:
- Any time before the 17th of the month:
[Add the charts version](#add-chart-version), so that the documentation is built using the
[version of the charts project that maps to](https://docs.gitlab.com/charts/installation/version_mappings.html)
the GitLab release. This step may have been completed already.
- Between the 17th and the 20th of the month:
1. [Create a stable branch and Docker image](#create-stable-branch-and-docker-image-for-release) for
the new version.
1. [Create a release merge request](#create-release-merge-request) for the new version, which
updates the version dropdown menu for the current documentation and adds the release to the
Docker configuration. For example, the
[release merge request for 13.9](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/1555).
1. [Update the three online versions](#update-dropdown-for-online-versions), so that they display the new release on their
version dropdown menus.
- On the 22nd of the month:
[Merge the release merge requests and run the necessary Docker image builds](#merge-merge-requests-and-run-docker-image-builds).
## Add chart version
To add a new charts version for the release:
1. Make sure you're in the root path of the `gitlab-docs` repository.
1. Open `content/_data/chart_versions.yaml` and add the new stable branch version using the
[version mapping](https://docs.gitlab.com/charts/installation/version_mappings.html). Only the
`major.minor` version is needed.
1. Create a new merge request and merge it.
NOTE:
If you have time, add anticipated future mappings to `content/_data/chart_versions.yaml`. This saves
a step for the next GitLab release.
## Create stable branch and Docker image for release
To create a stable branch and Docker image for the release:
1. Make sure you're in the root path of the `gitlab-docs` repository.
1. Run the Rake task to create the single version. For example, to create the 13.9 release branch
and perform others tasks:
```shell
./bin/rake "release:single[13.9]"
```
A branch for the release is created, a new `Dockerfile.13.9` is created, and `.gitlab-ci.yml`
has branches variables updated into a new branch. These files are automatically committed.
1. Push the newly created branch, but **don't create a merge request**. After you push, the
`image:docs-single` job creates a new Docker image tagged with the name of the branch you created
earlier. You can see the Docker image in the `registry` environment at
<https://gitlab.com/gitlab-org/gitlab-docs/-/environments/folders/registry>.
For example, see [the 13.9 release pipeline](https://gitlab.com/gitlab-org/gitlab-docs/-/pipelines/260288747).
Optionally, you can test locally by:
1. Building the image and running it. For example, for GitLab 13.9 documentation:
```shell
docker build -t docs:13.9 -f Dockerfile.13.9 .
docker run -it --rm -p 4000:4000 docs:13.9
```
1. Visiting <http://localhost:4000/13.9/> to see if everything works correctly.
## Create release merge request
NOTE:
An [epic is open](https://gitlab.com/groups/gitlab-org/-/epics/4361) to automate this step.
To create the release merge request for the release:
1. Make sure you're in the root path of the `gitlab-docs` repository.
1. Create a branch `release-X-Y`. For example:
```shell
git checkout master
git checkout -b release-13-9
```
1. Edit `content/_data/versions.yaml` and update the lists of versions to reflect the new release:
- Add the latest version to the `online:` section.
- Move the oldest version in `online:` to the `offline:` section. There should now be three
versions in `online:`.
1. Update these Dockerfiles:
- `dockerfiles/Dockerfile.archives`: Add the latest version to the top of the list.
- `Dockerfile.master`: Remove the oldest version, and add the newest version to the
top of the list.
1. Commit and push to create the merge request. For example:
```shell
git add content/ Dockerfile.master dockerfiles/Dockerfile.archives
git commit -m "Release 13.9"
git push origin release-13-9
```
Do not merge the release merge request yet.
## Update dropdown for online versions
To update `content/_data/versions.yaml` for all online versions (stable branches `X.Y` of the
`gitlab-docs` project). For example:
- The merge request to [update the 13.9 version dropdown menu for the 13.9 release](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/1556).
- The merge request to [update the 13.8 version dropdown menu for the 13.9 release](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/1557).
- The merge request to [update the 13.7 version dropdown menu for the 13.9 release](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/1558).
1. Run the Rake task that creates all of the necessary merge requests to update the dropdowns. For
example, for the 13.9 release:
```shell
git checkout release-13-9
./bin/rake release:dropdowns
```
1. [Visit the merge requests page](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests?label_name%5B%5D=release)
to check that their pipelines pass.
Do not merge these merge requests yet.
## Merge merge requests and run Docker image builds
The merge requests for the dropdowns should now all be merged into their respective stable branches.
Each merge triggers a new pipeline for each stable branch. Wait for the stable branch pipelines to
complete, then:
1. Check the [pipelines page](https://gitlab.com/gitlab-org/gitlab-docs/pipelines)
and make sure all stable branches have green pipelines.
1. After all the pipelines succeed:
1. Merge all of the [dropdown merge requests](#update-dropdown-for-online-versions).
1. Merge the [release merge request](#create-release-merge-request).
1. Finally, run the
[`Build docker images weekly` pipeline](https://gitlab.com/gitlab-org/gitlab-docs/pipeline_schedules)
that builds the `:latest` and `:archives` Docker images.
As the last step in the scheduled pipeline, the documentation site deploys with all new versions.
<!-- This redirect file can be deleted after <2021-07-12>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -23,6 +23,8 @@ when no license is active. So EE features always should be guarded by
`project.feature_available?` or `group.feature_available?` (or
`License.feature_available?` if it is a system-wide feature).
Frontend features should be guarded by pushing a flag from the backend by [using `push_licensed_feature`](licensed_feature_availability.md#restricting-frontend-features), and checked using `this.glFeatures.someFeature` in the frontend.
CE specs should remain untouched as much as possible and extra specs
should be added for EE. Licensed features can be stubbed using the
spec helper `stub_licensed_features` in `EE::LicenseHelpers`.

View File

@ -292,8 +292,7 @@ end
### Frontend
Use the `push_frontend_feature_flag` method for frontend code, which is
available to all controllers that inherit from `ApplicationController`. You can use
Use the `push_frontend_feature_flag` method which is available to all controllers that inherit from `ApplicationController`. You can use
this method to expose the state of a feature flag, for example:
```ruby

View File

@ -41,3 +41,16 @@ the instance license.
```ruby
License.feature_available?(:feature_symbol)
```
## Restricting frontend features
To restrict frontend features based on the license, use `push_licensed_feature`.
The frontend can then access this via `this.glFeatures`:
```ruby
before_action do
push_licensed_feature(:feature_symbol)
# or by project/namespace
push_licensed_feature(:feature_symbol, project)
end
```

View File

@ -58,6 +58,8 @@ To add a new application for your user:
## Group owned applications
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16227) in GitLab 13.11.
To add a new application for a group:
1. Navigate to the desired group.

View File

@ -70,6 +70,12 @@ breaking communication between **primary** and **secondary** nodes when using
HTTPS, customize your Internal URL to point to a load balancer with TLS
terminated at the load balancer.
WARNING:
Starting with GitLab 13.3 and [until 13.11](https://gitlab.com/gitlab-org/gitlab/-/issues/325522),
using an internal URL that is not accessible to the users will result in the
OAuth authorization flow not working properly, as the users will get redirected
to the internal URL instead of the external one.
## Multiple secondary nodes behind a load balancer
In GitLab 11.11, **secondary** nodes can use identical external URLs as long as

View File

@ -51,8 +51,10 @@ directory in your repository. Commit and push to your default branch.
To create a Markdown file:
1. Click the `+` button next to `master` and click **New file**.
1. Add the name of your issue template to the **File name** text field next to `master`.
1. In a project, go to **Repository**.
1. Next to the default branch, select the **{plus}** button.
1. Select **New file**.
1. Next to the default branch, in the **File name** field, add the name of your issue template.
Make sure that your file has the `.md` extension, for
example `feature_request.md` or `Feature Request.md`.
1. Commit and push to your default branch.
@ -61,9 +63,12 @@ If you don't have a `.gitlab/issue_templates` directory in your repository, you
To create the `.gitlab/issue_templates` directory:
1. Click the `+` button next to `master` and select **New directory**.
1. In a project, go to **Repository**.
1. Next to the default branch, select the **{plus}** button.
1. Select **New directory**.
1. Name this new directory `.gitlab` and commit to your default branch.
1. Click the `+` button next to `master` again and select **New directory**.
1. Next to the default branch, select the **{plus}** button.
1. Select **New directory**.
1. Name your directory `issue_templates` and commit to your default branch.
To check if this has worked correctly, [create a new issue](issues/managing_issues.md#create-a-new-issue)

View File

@ -65,6 +65,71 @@ can now create their own.
New compliance framework labels can be created and updated using GraphQL.
#### Compliance pipeline configuration **(ULTIMATE)**
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3156) in GitLab 13.9.
> - [Deployed behind a feature flag](../../feature_flags.md).
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/300324) in GitLab 13.11.
> - Enabled on GitLab.com.
> - Recommended for production use.
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
Group owners can use the compliance pipeline configuration to define compliance requirements
such as scans or tests, and enforce them in individual projects.
The [custom compliance framework](#custom-compliance-frameworks) feature allows group owners to specify the location
of a compliance pipeline configuration stored and managed in a dedicated project, distinct from a developer's project.
When you set up the compliance pipeline configuration field, use the
`file@group/project` format. For example, you can configure
`.compliance-gitlab-ci.yml@compliance-group/compliance-project`.
This field is inherited by projects where the compliance framework label is applied. The result
forces the project to run the compliance configurations.
When a project with a custom label executes a pipeline, it begins by evaluating the compliance pipeline configuration.
The custom pipeline configuration can then execute any included individual project configuration.
The user running the pipeline in the project should at least have Reporter access to the compliance project.
Example `.compliance-gitlab-ci.yml`
```yaml
stages: # Allows compliance team to control the ordering and interweaving of stages/jobs
- pre-compliance
- build
- test
- pre-deploy-compliance
- deploy
- post-compliance
variables: # can be overriden by a developer's local .gitlab-ci.yml
FOO: sast
sast: # none of these attributes can be overriden by a developer's local .gitlab-ci.yml
variables:
FOO: sast
stage: pre-compliance
script:
- echo "running $FOO"
sanity check:
stage: pre-deploy-compliance
script:
- echo "running $FOO"
audit trail:
stage: post-compliance
script:
- echo "running $FOO"
include: # Execute individual project's configuration
project: '$CI_PROJECT_PATH'
file: '$CI_PROJECT_CONFIG_PATH'
```
### Sharing and permissions
For your repository, you can set up features such as public access, repository features,

View File

@ -108,10 +108,21 @@ module Gitlab
!%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
end
def self.jh?
@is_jh ||=
ee? &&
root.join('jh').exist? &&
!%w[true 1].include?(ENV['EE_ONLY'].to_s)
end
def self.ee
yield if ee?
end
def self.jh
yield if jh?
end
def self.http_proxy_env?
HTTP_PROXY_ENV_VARS.any? { |name| ENV[name] }
end

View File

@ -6,6 +6,11 @@ module Gitlab
::Gitlab.dev_or_test_env? ? 'https://customers.stg.gitlab.com' : 'https://customers.gitlab.com'
end
SUBSCRIPTIONS_URL = ENV.fetch('CUSTOMER_PORTAL_URL', default_subscriptions_url).freeze
def self.subscriptions_url
ENV.fetch('CUSTOMER_PORTAL_URL', default_subscriptions_url)
end
end
end
Gitlab::SubscriptionPortal.prepend_if_jh('JH::Gitlab::SubscriptionPortal')
Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL = Gitlab::SubscriptionPortal.subscriptions_url.freeze

View File

@ -8804,9 +8804,24 @@ msgstr ""
msgid "Corpus Management|Are you sure you want to delete the corpus?"
msgstr ""
msgid "CorpusManagement|Actions"
msgstr ""
msgid "CorpusManagement|Corpus are used in fuzz testing as mutation source to Improve future testing."
msgstr ""
msgid "CorpusManagement|Corpus name"
msgstr ""
msgid "CorpusManagement|Fuzz testing corpus management"
msgstr ""
msgid "CorpusManagement|Last updated"
msgstr ""
msgid "CorpusManagement|Last used"
msgstr ""
msgid "CorpusManagement|Latest Job:"
msgstr ""
@ -8816,6 +8831,9 @@ msgstr ""
msgid "CorpusManagement|Not Set"
msgstr ""
msgid "CorpusManagement|Target"
msgstr ""
msgid "CorpusManagement|Total Size: %{totalSize}"
msgstr ""
@ -18397,6 +18415,9 @@ msgstr ""
msgid "Learn GitLab|Trial only"
msgstr ""
msgid "Learn More"
msgstr ""
msgid "Learn how to %{link_start}contribute to the built-in templates%{link_end}"
msgstr ""
@ -18475,12 +18496,18 @@ msgstr ""
msgid "LearnGitLab|Create a workflow for your new workspace, and learn how GitLab features work together:"
msgstr ""
msgid "LearnGitLab|Create an issue"
msgstr ""
msgid "LearnGitLab|Create or import a repository"
msgstr ""
msgid "LearnGitLab|Create or import your first repository into your new project."
msgstr ""
msgid "LearnGitLab|Create/import issues (tickets) to collaborate on ideas and plan work."
msgstr ""
msgid "LearnGitLab|Deploy"
msgstr ""
@ -26722,6 +26749,9 @@ msgstr ""
msgid "Reset key"
msgstr ""
msgid "Reset password"
msgstr ""
msgid "Reset registration token"
msgstr ""
@ -29023,9 +29053,6 @@ msgstr ""
msgid "Something went wrong on our end"
msgstr ""
msgid "Something went wrong on our end while loading the code quality diff."
msgstr ""
msgid "Something went wrong on our end."
msgstr ""
@ -30997,7 +31024,7 @@ msgstr ""
msgid "The merge request can now be merged."
msgstr ""
msgid "The merge request has made changes to this file that affect the number of code quality violations in it."
msgid "The merge request has been updated, and the number of code quality violations in this file has changed."
msgstr ""
msgid "The metric must be one of %{metrics}."

View File

@ -51,7 +51,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "1.188.0",
"@gitlab/tributejs": "1.0.0",
"@gitlab/ui": "29.3.0",
"@gitlab/ui": "29.4.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-4",
"@rails/ujs": "^6.0.3-4",
@ -155,7 +155,7 @@
"vuex": "^3.6.0",
"web-vitals": "^0.2.4",
"webpack": "^4.46.0",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-bundle-analyzer": "^4.4.1",
"webpack-cli": "^3.3.12",
"webpack-stats-plugin": "^0.3.1",
"worker-loader": "^2.0.0",

View File

@ -97,6 +97,10 @@ function rspec_paralellized_job() {
spec_folder_prefix="ee/"
fi
if [[ "${test_tool}" =~ "-jh" ]]; then
spec_folder_prefix="jh/"
fi
export KNAPSACK_LOG_LEVEL="debug"
export KNAPSACK_REPORT_PATH="knapsack/${report_name}_report.json"

View File

@ -21,6 +21,30 @@ RSpec.describe Profiles::NotificationsController do
expect(response).to render_template :show
end
context 'when personal projects are present', :request_store do
let!(:personal_project_1) { create(:project, namespace: user.namespace) }
context 'N+1 query check' do
render_views
it 'does not have an N+1' do
sign_in(user)
get :show
control = ActiveRecord::QueryRecorder.new do
get :show
end
create_list(:project, 2, namespace: user.namespace)
expect do
get :show
end.not_to exceed_query_limit(control)
end
end
end
context 'with groups that do not have notification preferences' do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }

View File

@ -56,7 +56,6 @@ module DeprecationToolkitEnv
def self.allowed_kwarg_warning_paths
%w[
activerecord-6.0.3.4/lib/active_record/migration.rb
devise-4.7.3/lib/devise/test/controller_helpers.rb
activesupport-6.0.3.4/lib/active_support/cache.rb
batch-loader-1.4.0/lib/batch_loader/graphql.rb
carrierwave-1.3.1/lib/carrierwave/sanitized_file.rb

View File

@ -596,7 +596,7 @@ RSpec.describe 'Admin updates settings' do
context 'Nav bar' do
it 'shows default help links in nav' do
default_support_url = 'https://about.gitlab.com/getting-help/'
default_support_url = "https://#{ApplicationHelper.promo_host}/getting-help/"
visit root_dashboard_path

View File

@ -211,21 +211,22 @@ describe('CompareVersions', () => {
});
describe('prev commit', () => {
const { location } = window;
beforeAll(() => {
delete window.location;
window.location = { href: `${TEST_HOST}?commit_id=${mrCommit.id}` };
global.jsdom.reconfigure({
url: `${TEST_HOST}?commit_id=${mrCommit.id}`,
});
});
afterAll(() => {
global.jsdom.reconfigure({
url: TEST_HOST,
});
});
beforeEach(() => {
jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
});
afterAll(() => {
window.location = location;
});
it('uses the correct href', () => {
const link = getPrevCommitNavElement();
@ -253,21 +254,22 @@ describe('CompareVersions', () => {
});
describe('next commit', () => {
const { location } = window;
beforeAll(() => {
delete window.location;
window.location = { href: `${TEST_HOST}?commit_id=${mrCommit.id}` };
global.jsdom.reconfigure({
url: `${TEST_HOST}?commit_id=${mrCommit.id}`,
});
});
afterAll(() => {
global.jsdom.reconfigure({
url: TEST_HOST,
});
});
beforeEach(() => {
jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
});
afterAll(() => {
window.location = location;
});
it('uses the correct href', () => {
const link = getNextCommitNavElement();

View File

@ -4,6 +4,7 @@ import Vuex from 'vuex';
import DiffRow from '~/diffs/components/diff_row.vue';
import { mapParallel } from '~/diffs/components/diff_row_utils';
import diffsModule from '~/diffs/store/modules';
import { findInteropAttributes } from '../find_interop_attributes';
import diffFileMockData from '../mock_data/diff_file';
describe('DiffRow', () => {
@ -211,4 +212,20 @@ describe('DiffRow', () => {
expect(coverage.classes('no-coverage')).toBeFalsy();
});
});
describe('interoperability', () => {
it.each`
desc | line | inline | leftSide | rightSide
${'with inline and new_line'} | ${{ left: { old_line: 3, new_line: 5, type: 'new' } }} | ${true} | ${{ type: 'new', line: '5', oldLine: '3', newLine: '5' }} | ${null}
${'with inline and no new_line'} | ${{ left: { old_line: 3, type: 'old' } }} | ${true} | ${{ type: 'old', line: '3', oldLine: '3' }} | ${null}
${'with parallel and no right side'} | ${{ left: { old_line: 3, new_line: 5 } }} | ${false} | ${{ type: 'old', line: '3', oldLine: '3' }} | ${null}
${'with parallel and no left side'} | ${{ right: { old_line: 3, new_line: 5 } }} | ${false} | ${null} | ${{ type: 'new', line: '5', newLine: '5' }}
${'with parallel and right side'} | ${{ left: { old_line: 3 }, right: { new_line: 5 } }} | ${false} | ${{ type: 'old', line: '3', oldLine: '3' }} | ${{ type: 'new', line: '5', newLine: '5' }}
`('$desc, sets interop data attributes', ({ line, inline, leftSide, rightSide }) => {
const wrapper = createWrapper({ props: { line, inline } });
expect(findInteropAttributes(wrapper, '[data-testid="left-side"]')).toEqual(leftSide);
expect(findInteropAttributes(wrapper, '[data-testid="right-side"]')).toEqual(rightSide);
});
});
});

View File

@ -3,6 +3,7 @@ import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
import { mapInline } from '~/diffs/components/diff_row_utils';
import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue';
import { createStore } from '~/mr_notes/stores';
import { findInteropAttributes } from '../find_interop_attributes';
import discussionsMockData from '../mock_data/diff_discussions';
import diffFileMockData from '../mock_data/diff_file';
@ -310,4 +311,16 @@ describe('InlineDiffTableRow', () => {
});
});
});
describe('interoperability', () => {
it.each`
desc | line | expectation
${'with type old'} | ${{ ...thisLine, type: 'old', old_line: 3, new_line: 5 }} | ${{ type: 'old', line: '3', oldLine: '3', newLine: '5' }}
${'with type new'} | ${{ ...thisLine, type: 'new', old_line: 3, new_line: 5 }} | ${{ type: 'new', line: '5', oldLine: '3', newLine: '5' }}
`('$desc, sets interop data attributes', ({ line, expectation }) => {
createComponent({ line });
expect(findInteropAttributes(wrapper)).toEqual(expectation);
});
});
});

View File

@ -5,6 +5,7 @@ import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
import { mapParallel } from '~/diffs/components/diff_row_utils';
import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
import { createStore } from '~/mr_notes/stores';
import { findInteropAttributes } from '../find_interop_attributes';
import discussionsMockData from '../mock_data/diff_discussions';
import diffFileMockData from '../mock_data/diff_file';
@ -418,5 +419,27 @@ describe('ParallelDiffTableRow', () => {
});
});
});
describe('interoperability', () => {
beforeEach(() => {
createComponent();
});
it('adds old side interoperability data attributes', () => {
expect(findInteropAttributes(wrapper, '.line_content.left-side')).toEqual({
type: 'old',
line: thisLine.left.old_line.toString(),
oldLine: thisLine.left.old_line.toString(),
});
});
it('adds new side interoperability data attributes', () => {
expect(findInteropAttributes(wrapper, '.line_content.right-side')).toEqual({
type: 'new',
line: thisLine.right.new_line.toString(),
newLine: thisLine.right.new_line.toString(),
});
});
});
});
});

View File

@ -0,0 +1,20 @@
export const findInteropAttributes = (parent, sel) => {
const target = sel ? parent.find(sel) : parent;
if (!target.exists()) {
return null;
}
const type = target.attributes('data-interop-type');
if (!type) {
return null;
}
return {
type,
line: target.attributes('data-interop-line'),
oldLine: target.attributes('data-interop-old-line'),
newLine: target.attributes('data-interop-new-line'),
};
};

View File

@ -17,6 +17,9 @@ import {
fetchDiffFilesBatch,
fetchDiffFilesMeta,
fetchCoverageFiles,
clearEtagPoll,
stopCodequalityPolling,
fetchCodequality,
assignDiscussionsToDiff,
removeDiscussionsFromDiff,
startRenderDiffsQueue,
@ -98,6 +101,7 @@ describe('DiffsStoreActions', () => {
const endpointMetadata = '/diffs/set/endpoint/metadata';
const endpointBatch = '/diffs/set/endpoint/batch';
const endpointCoverage = '/diffs/set/coverage_reports';
const endpointCodequality = '/diffs/set/codequality_diff';
const projectPath = '/root/project';
const dismissEndpoint = '/-/user_callouts';
const showSuggestPopover = false;
@ -109,6 +113,7 @@ describe('DiffsStoreActions', () => {
endpointBatch,
endpointMetadata,
endpointCoverage,
endpointCodequality,
projectPath,
dismissEndpoint,
showSuggestPopover,
@ -118,6 +123,7 @@ describe('DiffsStoreActions', () => {
endpointBatch: '',
endpointMetadata: '',
endpointCoverage: '',
endpointCodequality: '',
projectPath: '',
dismissEndpoint: '',
showSuggestPopover: true,
@ -130,6 +136,7 @@ describe('DiffsStoreActions', () => {
endpointMetadata,
endpointBatch,
endpointCoverage,
endpointCodequality,
projectPath,
dismissEndpoint,
showSuggestPopover,
@ -299,6 +306,47 @@ describe('DiffsStoreActions', () => {
});
});
describe('fetchCodequality', () => {
let mock;
const endpointCodequality = '/fetch';
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
stopCodequalityPolling();
clearEtagPoll();
});
it('should commit SET_CODEQUALITY_DATA with received response', (done) => {
const data = {
files: { 'app.js': [{ line: 1, description: 'Unexpected alert.', severity: 'minor' }] },
};
mock.onGet(endpointCodequality).reply(200, { data });
testAction(
fetchCodequality,
{},
{ endpointCodequality },
[{ type: types.SET_CODEQUALITY_DATA, payload: { data } }],
[],
done,
);
});
it('should show flash on API error', (done) => {
mock.onGet(endpointCodequality).reply(400);
testAction(fetchCodequality, {}, { endpointCodequality }, [], [], () => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith(expect.stringMatching('Something went wrong'));
done();
});
});
});
describe('setHighlightedRow', () => {
it('should mark currently selected diff and set lineHash and fileHash of highlightedRow', () => {
testAction(setHighlightedRow, 'ABC_123', {}, [

View File

@ -376,6 +376,26 @@ describe('Diffs Module Getters', () => {
});
});
describe('fileCodequalityDiff', () => {
beforeEach(() => {
Object.assign(localState.codequalityDiff, {
files: { 'app.js': [{ line: 1, description: 'Unexpected alert.', severity: 'minor' }] },
});
});
it('returns empty array when no codequality data is available', () => {
Object.assign(localState.codequalityDiff, {});
expect(getters.fileCodequalityDiff(localState)('test.js')).toEqual([]);
});
it('returns array when codequality data is available for given file', () => {
expect(getters.fileCodequalityDiff(localState)('app.js')).toEqual([
{ line: 1, description: 'Unexpected alert.', severity: 'minor' },
]);
});
});
describe('suggestionCommitMessage', () => {
let rootState;

View File

@ -115,6 +115,19 @@ describe('DiffsStoreMutations', () => {
});
});
describe('SET_CODEQUALITY_DATA', () => {
it('should set codequality data', () => {
const state = { codequalityDiff: {} };
const codequality = {
files: { 'app.js': [{ line: 1, description: 'Unexpected alert.', severity: 'minor' }] },
};
mutations[types.SET_CODEQUALITY_DATA](state, codequality);
expect(state.codequalityDiff).toEqual(codequality);
});
});
describe('SET_DIFF_VIEW_TYPE', () => {
it('should set diff view type properly', () => {
const state = {};

View File

@ -0,0 +1,67 @@
import {
getInteropInlineAttributes,
getInteropNewSideAttributes,
getInteropOldSideAttributes,
ATTR_TYPE,
ATTR_LINE,
ATTR_NEW_LINE,
ATTR_OLD_LINE,
} from '~/diffs/utils/interoperability';
describe('~/diffs/utils/interoperability', () => {
describe('getInteropInlineAttributes', () => {
it.each([
['with null input', { input: null, output: null }],
[
'with type=old input',
{
input: { type: 'old', old_line: 3, new_line: 5 },
output: { [ATTR_TYPE]: 'old', [ATTR_LINE]: 3, [ATTR_OLD_LINE]: 3, [ATTR_NEW_LINE]: 5 },
},
],
[
'with type=old-nonewline input',
{
input: { type: 'old-nonewline', old_line: 3, new_line: 5 },
output: { [ATTR_TYPE]: 'old', [ATTR_LINE]: 3, [ATTR_OLD_LINE]: 3, [ATTR_NEW_LINE]: 5 },
},
],
[
'with type=new input',
{
input: { type: 'new', old_line: 3, new_line: 5 },
output: { [ATTR_TYPE]: 'new', [ATTR_LINE]: 5, [ATTR_OLD_LINE]: 3, [ATTR_NEW_LINE]: 5 },
},
],
[
'with type=bogus input',
{
input: { type: 'bogus', old_line: 3, new_line: 5 },
output: { [ATTR_TYPE]: 'new', [ATTR_LINE]: 5, [ATTR_OLD_LINE]: 3, [ATTR_NEW_LINE]: 5 },
},
],
])('%s', (desc, { input, output }) => {
expect(getInteropInlineAttributes(input)).toEqual(output);
});
});
describe('getInteropOldSideAttributes', () => {
it.each`
input | output
${null} | ${null}
${{ old_line: 2 }} | ${{ [ATTR_TYPE]: 'old', [ATTR_LINE]: 2, [ATTR_OLD_LINE]: 2 }}
`('with input=$input', ({ input, output }) => {
expect(getInteropOldSideAttributes(input)).toEqual(output);
});
});
describe('getInteropNewSideAttributes', () => {
it.each`
input | output
${null} | ${null}
${{ new_line: 2 }} | ${{ [ATTR_TYPE]: 'new', [ATTR_LINE]: 2, [ATTR_NEW_LINE]: 2 }}
`('with input=$input', ({ input, output }) => {
expect(getInteropNewSideAttributes(input)).toEqual(output);
});
});
});

View File

@ -25,6 +25,10 @@ RSpec.describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)'
end
before do
# Create a user that matches the project.commit author
# This is so that the "author" information will be populated
create(:user, email: project.commit.author_email, name: project.commit.author_name)
sign_in(user)
end
@ -33,17 +37,21 @@ RSpec.describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)'
end
it 'merge_request_diffs/with_commit.json' do
# Create a user that matches the project.commit author
# This is so that the "author" information will be populated
create(:user, email: project.commit.author_email, name: project.commit.author_name)
render_merge_request(merge_request, commit_id: project.commit.sha)
end
it 'merge_request_diffs/diffs_metadata.json' do
render_merge_request(merge_request, action: :diffs_metadata)
end
it 'merge_request_diffs/diffs_batch.json' do
render_merge_request(merge_request, action: :diffs_batch, page: 1, per_page: 30)
end
private
def render_merge_request(merge_request, view: 'inline', **extra_params)
get :show, params: {
def render_merge_request(merge_request, action: :show, view: 'inline', **extra_params)
get action, params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.to_param,

View File

@ -29,21 +29,21 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
class="gl-text-gray-500 gl-mb-2"
data-testid="completion-percentage"
>
25% completed
22% completed
</p>
<div
class="progress"
max="8"
max="9"
value="2"
>
<div
aria-valuemax="8"
aria-valuemax="9"
aria-valuemin="0"
aria-valuenow="2"
class="progress-bar"
role="progressbar"
style="width: 25%;"
style="width: 22.22222222222222%;"
>
<!---->
</div>
@ -234,6 +234,20 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
</p>
</div>
<div
class="gl-mb-4"
>
<span>
<a
class="gl-link"
href="http://example.com/"
>
Create an issue
</a>
</span>
<!---->
</div>
<div
class="gl-mb-4"
>

View File

@ -29,21 +29,21 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-gray-500 gl-mb-2"
data-testid="completion-percentage"
>
25% completed
22% completed
</p>
<div
class="progress"
max="8"
max="9"
value="2"
>
<div
aria-valuemax="8"
aria-valuemax="9"
aria-valuemin="0"
aria-valuenow="2"
class="progress-bar"
role="progressbar"
style="width: 25%;"
style="width: 22.22222222222222%;"
>
<!---->
</div>
@ -94,6 +94,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
<img
alt="Invite your colleagues"
src="http://example.com/images/illustration.svg"
/>
@ -151,6 +152,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
<img
alt="Create or import a repository"
src="http://example.com/images/illustration.svg"
/>
@ -200,6 +202,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
<img
alt="Set-up CI/CD"
src="http://example.com/images/illustration.svg"
/>
@ -249,6 +252,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
<img
alt="Try GitLab Ultimate for free"
src="http://example.com/images/illustration.svg"
/>
@ -303,6 +307,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
<img
alt="Add code owners"
src="http://example.com/images/illustration.svg"
/>
@ -357,6 +362,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
<img
alt="Enable require merge approvals"
src="http://example.com/images/illustration.svg"
/>
@ -422,6 +428,57 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
<img
alt="Create an issue"
src="http://example.com/images/illustration.svg"
/>
<h6>
Create an issue
</h6>
<p
class="gl-font-sm gl-text-gray-700"
>
Create/import issues (tickets) to collaborate on ideas and plan work.
</p>
<a
class="gl-link"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Create an issue
</a>
</div>
</div>
<!---->
</div>
</div>
<div
class="col gl-mb-6"
>
<div
class="gl-card gl-pt-0"
>
<!---->
<div
class="gl-card-body"
>
<div
class="gl-text-right gl-h-5"
>
<!---->
</div>
<div
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
<img
alt="Submit a merge request (MR)"
src="http://example.com/images/illustration.svg"
/>
@ -487,6 +544,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
<img
alt="Run a Security scan using CI/CD"
src="http://example.com/images/illustration.svg"
/>

View File

@ -31,6 +31,10 @@ exports[`Learn GitLab Section Card renders correctly 1`] = `
action="userAdded"
value="[object Object]"
/>
<learn-gitlab-section-link-stub
action="issueCreated"
value="[object Object]"
/>
<learn-gitlab-section-link-stub
action="gitWrite"
value="[object Object]"

View File

@ -26,13 +26,13 @@ describe('Learn GitLab Design A', () => {
it('renders the progress percentage', () => {
const text = wrapper.find('[data-testid="completion-percentage"]').text();
expect(text).toEqual('25% completed');
expect(text).toBe('22% completed');
});
it('renders the progress bar with correct values', () => {
const progressBar = wrapper.find(GlProgressBar);
const progressBar = wrapper.findComponent(GlProgressBar);
expect(progressBar.attributes('value')).toBe('2');
expect(progressBar.attributes('max')).toBe('8');
expect(progressBar.attributes('max')).toBe('9');
});
});

View File

@ -26,13 +26,13 @@ describe('Learn GitLab Design B', () => {
it('renders the progress percentage', () => {
const text = wrapper.find('[data-testid="completion-percentage"]').text();
expect(text).toEqual('25% completed');
expect(text).toBe('22% completed');
});
it('renders the progress bar with correct values', () => {
const progressBar = wrapper.find(GlProgressBar);
const progressBar = wrapper.findComponent(GlProgressBar);
expect(progressBar.attributes('value')).toBe('2');
expect(progressBar.attributes('max')).toBe('8');
expect(progressBar.attributes('max')).toBe('9');
});
});

View File

@ -39,4 +39,9 @@ export const testActions = {
completed: false,
svg: 'http://example.com/images/illustration.svg',
},
issueCreated: {
url: 'http://example.com/',
completed: false,
svg: 'http://example.com/images/illustration.svg',
},
};

View File

@ -2,9 +2,15 @@ import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
import {
IID_FAILURE,
LAYER_VIEW,
STAGE_VIEW,
VIEW_TYPE_KEY,
} from '~/pipelines/components/graph/constants';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
@ -21,6 +27,7 @@ const defaultProvide = {
describe('Pipeline graph wrapper', () => {
Vue.use(VueApollo);
useLocalStorageSpy();
let wrapper;
const getAlert = () => wrapper.find(GlAlert);
@ -216,7 +223,7 @@ describe('Pipeline graph wrapper', () => {
});
describe('view dropdown', () => {
describe('when feature flag is off', () => {
describe('when pipelineGraphLayersView feature flag is off', () => {
beforeEach(async () => {
createComponentWithApollo();
jest.runOnlyPendingTimers();
@ -228,7 +235,7 @@ describe('Pipeline graph wrapper', () => {
});
});
describe('when feature flag is on', () => {
describe('when pipelineGraphLayersView feature flag is on', () => {
let layersFn;
beforeEach(async () => {
layersFn = jest.spyOn(parsingUtils, 'listByLayers');
@ -245,7 +252,7 @@ describe('Pipeline graph wrapper', () => {
await wrapper.vm.$nextTick();
});
it('appears', () => {
it('appears when pipeline uses needs', () => {
expect(getViewSelector().exists()).toBe(true);
});
@ -259,6 +266,11 @@ describe('Pipeline graph wrapper', () => {
expect(getStageColumnTitle().text()).toBe('');
});
it('saves the view type to local storage', async () => {
await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
expect(localStorage.setItem.mock.calls).toEqual([[VIEW_TYPE_KEY, LAYER_VIEW]]);
});
it('calls listByLayers only once no matter how many times view is switched', async () => {
expect(layersFn).not.toHaveBeenCalled();
await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
@ -269,5 +281,53 @@ describe('Pipeline graph wrapper', () => {
expect(layersFn).toHaveBeenCalledTimes(1);
});
});
describe('when feature flag is on and local storage is set', () => {
beforeEach(async () => {
localStorage.setItem(VIEW_TYPE_KEY, LAYER_VIEW);
createComponentWithApollo({
provide: {
glFeatures: {
pipelineGraphLayersView: true,
},
},
mountFn: mount,
});
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
});
it('reads the view type from localStorage when available', () => {
expect(wrapper.find('[data-testid="pipeline-view-selector"] code').text()).toContain(
'needs:',
);
});
});
describe('when feature flag is on but pipeline does not use needs', () => {
beforeEach(async () => {
const nonNeedsResponse = { ...mockPipelineResponse };
nonNeedsResponse.data.project.pipeline.usesNeeds = false;
createComponentWithApollo({
provide: {
glFeatures: {
pipelineGraphLayersView: true,
},
},
mountFn: mount,
getPipelineDetailsHandler: jest.fn().mockResolvedValue(nonNeedsResponse),
});
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
});
it('does not appear when pipeline does not use needs', () => {
expect(getViewSelector().exists()).toBe(false);
});
});
});
});

View File

@ -8,6 +8,7 @@ export const mockPipelineResponse = {
__typename: 'Pipeline',
id: 163,
iid: '22',
usesNeeds: true,
downstream: null,
upstream: null,
stages: {
@ -569,6 +570,7 @@ export const wrappedPipelineReturn = {
__typename: 'Pipeline',
id: 'gid://gitlab/Ci::Pipeline/175',
iid: '38',
usesNeeds: true,
downstream: {
__typename: 'PipelineConnection',
nodes: [],

Some files were not shown because too many files have changed in this diff Show More